This is an automated email from the ASF dual-hosted git repository.
kishoreg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 2e16aa4 updated cluster manage UI and added table details page and
segment details page (#5732)
2e16aa4 is described below
commit 2e16aa4676198f44c7a9532d1d229125069e567b
Author: Sanket Shah <[email protected]>
AuthorDate: Thu Jul 23 13:00:38 2020 +0530
updated cluster manage UI and added table details page and segment details
page (#5732)
* updated cluster manage UI and added table details page and segment
details page
* showing error message if sql query returns any exception
---
.../main/resources/app/components/Breadcrumbs.tsx | 11 +-
.../app/components/Homepage/ClusterConfig.tsx | 24 +-
.../app/components/Homepage/InstanceTable.tsx | 42 +-
.../app/components/Homepage/InstancesTables.tsx | 26 +-
.../app/components/Homepage/TenantsTable.tsx | 43 +-
.../src/main/resources/app/components/Layout.tsx | 7 +-
.../app/components/Query/QuerySideBar.tsx | 7 +-
.../main/resources/app/components/SearchBar.tsx | 14 +-
.../resources/app/components/SimpleAccordion.tsx | 96 +++++
.../app/components/SvgIcons/ClusterManagerIcon.tsx | 32 ++
.../app/components/SvgIcons/QueryConsoleIcon.tsx | 36 +-
.../src/main/resources/app/components/Table.tsx | 249 ++++++-----
.../{EnhancedTableToolbar.tsx => TableToolbar.tsx} | 12 +-
.../src/main/resources/app/interfaces/types.d.ts | 23 +-
.../src/main/resources/app/pages/Query.tsx | 298 +++++++------
.../main/resources/app/pages/SegmentDetails.tsx | 170 ++++++++
.../src/main/resources/app/pages/TenantDetails.tsx | 139 ++++++-
.../src/main/resources/app/pages/Tenants.tsx | 64 +--
.../src/main/resources/app/requests/index.ts | 16 +-
pinot-controller/src/main/resources/app/router.tsx | 6 +-
.../main/resources/app/utils/PinotMethodUtils.ts | 462 +++++++++++++++++++++
.../src/main/resources/app/utils/Utils.tsx | 25 ++
pinot-controller/src/main/resources/package.json | 3 +-
23 files changed, 1375 insertions(+), 430 deletions(-)
diff --git a/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
b/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
index 0cdf09a..0df6f25 100644
--- a/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
+++ b/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
@@ -85,13 +85,14 @@ const BreadcrumbsComponent = ({ ...props }) => {
const breadcrumbs = [getClickableLabel(breadcrumbNameMap['/'], '/')];
const paramsKeys = _.keys(props.match.params);
if(paramsKeys.length){
- const {tenantName, tableName} = props.match.params;
- if(!tableName && tenantName){
- breadcrumbs.push(getLabel(tenantName));
- } else {
+ const {tenantName, tableName, segmentName} = props.match.params;
+ if(tenantName && tableName){
breadcrumbs.push(getClickableLabel(tenantName,
`/tenants/${tenantName}`));
- breadcrumbs.push(getLabel(tableName));
}
+ if(tenantName && tableName && segmentName){
+ breadcrumbs.push(getClickableLabel(tableName,
`/tenants/${tenantName}/table/${tableName}`));
+ }
+ breadcrumbs.push(getLabel(segmentName || tableName || tenantName));
} else {
breadcrumbs.push(getLabel(breadcrumbNameMap[location.pathname]));
}
diff --git
a/pinot-controller/src/main/resources/app/components/Homepage/ClusterConfig.tsx
b/pinot-controller/src/main/resources/app/components/Homepage/ClusterConfig.tsx
index 8b65e59..1b5217c 100644
---
a/pinot-controller/src/main/resources/app/components/Homepage/ClusterConfig.tsx
+++
b/pinot-controller/src/main/resources/app/components/Homepage/ClusterConfig.tsx
@@ -19,9 +19,9 @@
import React, { useEffect, useState } from 'react';
import { TableData } from 'Models';
-import { getClusterConfig } from '../../requests';
import AppLoader from '../AppLoader';
import CustomizedTables from '../Table';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
const ClusterConfig = () => {
@@ -31,21 +31,23 @@ const ClusterConfig = () => {
records: []
});
+ const fetchData = async () => {
+ const result = await PinotMethodUtils.getClusterConfigData();
+ setTableData(result);
+ setFetching(false);
+ };
useEffect(() => {
- getClusterConfig().then(({ data }) => {
- setTableData({
- columns: ['Property', 'Value'],
- records: [
- ...Object.keys(data).map(key => [key, data[key]])
- ]
- });
- setFetching(false);
- });
+ fetchData();
}, []);
return (
fetching ? <AppLoader /> :
- <CustomizedTables title="Cluster configuration" data={tableData} />
+ <CustomizedTables
+ title="Cluster configuration"
+ data={tableData}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
);
};
diff --git
a/pinot-controller/src/main/resources/app/components/Homepage/InstanceTable.tsx
b/pinot-controller/src/main/resources/app/components/Homepage/InstanceTable.tsx
index a3657d9..e05d370 100644
---
a/pinot-controller/src/main/resources/app/components/Homepage/InstanceTable.tsx
+++
b/pinot-controller/src/main/resources/app/components/Homepage/InstanceTable.tsx
@@ -19,9 +19,9 @@
import React, { useEffect, useState } from 'react';
import { TableData } from 'Models';
-import { getInstance } from '../../requests';
import CustomizedTables from '../Table';
import AppLoader from '../AppLoader';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
type Props = {
name: string,
@@ -36,28 +36,34 @@ const InstaceTable = ({ name, instances }: Props) => {
records: []
});
- useEffect(() => {
+ const fetchClusterName = async () => {
+ const clusterName = await PinotMethodUtils.getClusterName();
+ fetchLiveInstance(clusterName);
+ }
+
+ const fetchLiveInstance = async (clusterName) => {
+ const liveInstanceArr = await
PinotMethodUtils.getLiveInstance(clusterName);
+ fetchData(liveInstanceArr.data);
+ }
- const promiseArr = [
- ...instances.map(inst => getInstance(inst))
- ];
+ const fetchData = async (liveInstanceArr) => {
+ const result = await PinotMethodUtils.getInstanceData(instances,
liveInstanceArr);
+ setTableData(result);
+ setFetching(false);
+ };
- Promise.all(promiseArr).then(result => {
- setTableData({
- columns: ['Name', 'Enabled', 'Hostname', 'Port', 'URI'],
- records: [
- ...result.map(({ data }) => (
- [data.instanceName, data.enabled, data.hostName, data.port,
`${data.hostName}:${data.port}`]
- ))
- ]
- });
- setFetching(false);
- });
- }, [instances]);
+ useEffect(() => {
+ fetchClusterName();
+ }, []);
return (
fetching ? <AppLoader /> :
- <CustomizedTables title={name} data={tableData} />
+ <CustomizedTables
+ title={name}
+ data={tableData}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
);
};
diff --git
a/pinot-controller/src/main/resources/app/components/Homepage/InstancesTables.tsx
b/pinot-controller/src/main/resources/app/components/Homepage/InstancesTables.tsx
index 0f86502..39f2ed5 100644
---
a/pinot-controller/src/main/resources/app/components/Homepage/InstancesTables.tsx
+++
b/pinot-controller/src/main/resources/app/components/Homepage/InstancesTables.tsx
@@ -19,9 +19,9 @@
import React, { useEffect, useState } from 'react';
import map from 'lodash/map';
-import { getInstances } from '../../requests';
import AppLoader from '../AppLoader';
-import InstaceTable from './InstanceTable';
+import InstanceTable from './InstanceTable';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
type DataTable = {
[name: string]: string[]
@@ -31,21 +31,13 @@ const Instances = () => {
const [fetching, setFetching] = useState(true);
const [instances, setInstances] = useState<DataTable>();
+ const fetchData = async () => {
+ const result = await PinotMethodUtils.getAllInstances();
+ setInstances(result);
+ setFetching(false);
+ };
useEffect(() => {
- getInstances().then(({ data }) => {
- const initialVal: DataTable = {};
- // It will create instances list array like
- // {Controller: ['Controller1', 'Controller2'], Broker: ['Broker1',
'Broker2']}
- const groupedData = data.instances.reduce((r, a) => {
- const y = a.split('_');
- const key = y[0].trim();
- r[key] = [...r[key] || [], a];
- return r;
- }, initialVal);
-
- setInstances(groupedData);
- setFetching(false);
- });
+ fetchData();
}, []);
return fetching ? (
@@ -54,7 +46,7 @@ const Instances = () => {
<>
{
map(instances, (value, key) => {
- return <InstaceTable key={key} name={key} instances={value} />;
+ return <InstanceTable key={key} name={key} instances={value} />;
})
}
</>
diff --git
a/pinot-controller/src/main/resources/app/components/Homepage/TenantsTable.tsx
b/pinot-controller/src/main/resources/app/components/Homepage/TenantsTable.tsx
index 04256cf..0696961 100644
---
a/pinot-controller/src/main/resources/app/components/Homepage/TenantsTable.tsx
+++
b/pinot-controller/src/main/resources/app/components/Homepage/TenantsTable.tsx
@@ -19,39 +19,36 @@
import React, { useEffect, useState } from 'react';
import { TableData } from 'Models';
-import union from 'lodash/union';
-import { getTenants } from '../../requests';
import AppLoader from '../AppLoader';
import CustomizedTables from '../Table';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
const TenantsTable = () => {
const [fetching, setFetching] = useState(true);
const [tableData, setTableData] = useState<TableData>({ records: [],
columns: [] });
+ const fetchData = async () => {
+ const result = await PinotMethodUtils.getTenantsData();
+ setTableData(result);
+ setFetching(false);
+ };
useEffect(() => {
- getTenants().then(({ data }) => {
- const records = union(
- data.SERVER_TENANTS,
- data.BROKER_TENANTS
- );
- setTableData({
- columns: ['Name', 'Server', 'Broker', 'Tables'],
- records: [
- ...records.map(record => [
- record,
- data.SERVER_TENANTS.indexOf(record) > -1 ? 1 : 0,
- data.BROKER_TENANTS.indexOf(record) > -1 ? 1 : 0,
- '-'
- ])
- ]
- });
- setFetching(false);
- });
+ fetchData();
}, []);
- return (
- fetching ? <AppLoader /> : <CustomizedTables title="Tenants"
data={tableData} addLinks isPagination baseURL="/tenants/" />
+ return fetching ? (
+ <AppLoader />
+ ) : (
+ <CustomizedTables
+ title="Tenants"
+ data={tableData}
+ addLinks
+ isPagination
+ baseURL="/tenants/"
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
);
};
-export default TenantsTable;
\ No newline at end of file
+export default TenantsTable;
diff --git a/pinot-controller/src/main/resources/app/components/Layout.tsx
b/pinot-controller/src/main/resources/app/components/Layout.tsx
index b958bdc..a11a292 100644
--- a/pinot-controller/src/main/resources/app/components/Layout.tsx
+++ b/pinot-controller/src/main/resources/app/components/Layout.tsx
@@ -23,11 +23,12 @@ import Sidebar from './SideBar';
import Header from './Header';
import QueryConsoleIcon from './SvgIcons/QueryConsoleIcon';
import SwaggerIcon from './SvgIcons/SwaggerIcon';
+import ClusterManagerIcon from './SvgIcons/ClusterManagerIcon';
const navigationItems = [
- // { id: 1, name: 'Cluster Manager', link: '/' },
- { id: 1, name: 'Query Console', link: '/', icon: <QueryConsoleIcon/> },
- { id: 2, name: 'Swagger REST API', link: 'help', target: '_blank', icon:
<SwaggerIcon/> }
+ { id: 1, name: 'Cluster Manager', link: '/', icon: <ClusterManagerIcon/> },
+ { id: 2, name: 'Query Console', link: '/query', icon: <QueryConsoleIcon/> },
+ { id: 3, name: 'Swagger REST API', link: 'help', target: '_blank', icon:
<SwaggerIcon/> }
];
const Layout = (props) => {
diff --git
a/pinot-controller/src/main/resources/app/components/Query/QuerySideBar.tsx
b/pinot-controller/src/main/resources/app/components/Query/QuerySideBar.tsx
index a3cbd3c..5e12d46 100644
--- a/pinot-controller/src/main/resources/app/components/Query/QuerySideBar.tsx
+++ b/pinot-controller/src/main/resources/app/components/Query/QuerySideBar.tsx
@@ -76,9 +76,10 @@ type Props = {
tableList: TableData;
fetchSQLData: Function;
tableSchema: TableData;
+ selectedTable: string;
};
-const Sidebar = ({ tableList, fetchSQLData, tableSchema }: Props) => {
+const Sidebar = ({ tableList, fetchSQLData, tableSchema, selectedTable }:
Props) => {
const classes = useStyles();
return (
@@ -101,15 +102,17 @@ const Sidebar = ({ tableList, fetchSQLData, tableSchema
}: Props) => {
noOfRows={tableList.records.length}
cellClickCallback={fetchSQLData}
isCellClickable
+ showSearchBox={false}
/>
{tableSchema.records.length ? (
<CustomizedTables
- title="Schema:"
+ title={`${selectedTable} schema`}
data={tableSchema}
isPagination={false}
noOfRows={tableSchema.records.length}
highlightBackground
+ showSearchBox={false}
/>
) : null}
</Grid>
diff --git a/pinot-controller/src/main/resources/app/components/SearchBar.tsx
b/pinot-controller/src/main/resources/app/components/SearchBar.tsx
index dcbfc4c..f647533 100644
--- a/pinot-controller/src/main/resources/app/components/SearchBar.tsx
+++ b/pinot-controller/src/main/resources/app/components/SearchBar.tsx
@@ -27,14 +27,18 @@ const useStyles = makeStyles((theme) => ({
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
- marginRight: theme.spacing(2),
- marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
- marginLeft: theme.spacing(3),
+ // marginLeft: theme.spacing(3),
width: 'auto',
},
},
+ searchOnRight:{
+ position: 'relative',
+ borderRadius: theme.shape.borderRadius,
+ width: '150px',
+ marginLeft: 'auto',
+ },
searchIcon: {
padding: theme.spacing(0, 2),
height: '100%',
@@ -58,10 +62,10 @@ const useStyles = makeStyles((theme) => ({
},
}));
-const SearchBar = (props: InputBaseProps) => {
+const SearchBar = (props) => {
const classes = useStyles();
return (
- <div className={classes.search}>
+ <div className={props.searchOnRight ? classes.searchOnRight :
classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
diff --git
a/pinot-controller/src/main/resources/app/components/SimpleAccordion.tsx
b/pinot-controller/src/main/resources/app/components/SimpleAccordion.tsx
new file mode 100644
index 0000000..3366f20
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/SimpleAccordion.tsx
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { Theme, createStyles, makeStyles } from '@material-ui/core/styles';
+import Accordion from '@material-ui/core/Accordion';
+import AccordionSummary from '@material-ui/core/AccordionSummary';
+import AccordionDetails from '@material-ui/core/AccordionDetails';
+import Typography from '@material-ui/core/Typography';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import SearchBar from './SearchBar';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ root: {
+ backgroundColor: 'rgba(66, 133, 244, 0.1)',
+ borderBottom: '1px #BDCCD9 solid',
+ minHeight: '0 !important',
+ '& .MuiAccordionSummary-content.Mui-expanded':{
+ margin: 0
+ }
+ },
+ heading: {
+ fontWeight: 600,
+ letterSpacing: '1px',
+ fontSize: '1rem',
+ color: '#4285f4'
+ },
+ details: {
+ flexDirection: 'column',
+ padding: '0'
+ }
+ }),
+);
+
+type Props = {
+ headerTitle: string;
+ showSearchBox: boolean;
+ searchValue?: string;
+ handleSearch?: Function;
+ recordCount?: number
+ children: any;
+};
+
+export default function SimpleAccordion({
+ headerTitle,
+ showSearchBox,
+ searchValue,
+ handleSearch,
+ recordCount,
+ children
+}: Props) {
+ const classes = useStyles();
+
+ return (
+ <Accordion
+ defaultExpanded={true}
+ >
+ <AccordionSummary
+ expandIcon={<ExpandMoreIcon />}
+ aria-controls={`panel1a-content-${headerTitle}`}
+ id={`panel1a-header-${headerTitle}`}
+ className={classes.root}
+ >
+ <Typography className={classes.heading}>{`${headerTitle.toUpperCase()}
${recordCount !== undefined ? ` - (${recordCount})` : ''}`}</Typography>
+ </AccordionSummary>
+ <AccordionDetails className={classes.details}>
+ {showSearchBox ?
+ <SearchBar
+ // searchOnRight={true}
+ value={searchValue}
+ onChange={(e) => handleSearch(e.target.value)}
+ />
+ : null
+ }
+ {children}
+ </AccordionDetails>
+ </Accordion>
+ );
+}
\ No newline at end of file
diff --git
a/pinot-controller/src/main/resources/app/components/SvgIcons/ClusterManagerIcon.tsx
b/pinot-controller/src/main/resources/app/components/SvgIcons/ClusterManagerIcon.tsx
new file mode 100644
index 0000000..71da8b3
--- /dev/null
+++
b/pinot-controller/src/main/resources/app/components/SvgIcons/ClusterManagerIcon.tsx
@@ -0,0 +1,32 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import * as React from 'react';
+import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
+
+export default (props: SvgIconProps) => (
+ <SvgIcon style={{ width: 24, height: 24, verticalAlign: 'middle' }}
viewBox="0 0 512 512" fill="none" {...props}>
+ <g>
+ <path d="m63.623
367.312h-62.815v144.688h148.689v-116.926h-68.752zm55.971
114.786h-88.884v-84.883h16.222l17.123 27.761h55.539z"/>
+ <path d="m244.47
367.312h-62.815v144.688h148.689v-116.926h-68.751zm55.972
114.786h-88.884v-84.883h16.222l17.123 27.761h55.539z"/>
+ <path d="m442.44
395.074-17.122-27.761h-62.815v144.687h148.689v-116.926zm38.85
87.024h-88.884v-84.883h16.222l17.122 27.761h55.54z"/>
+ <path d="m90.104
292.958h150.945v42.267h29.902v-42.267h150.945v42.267h29.902v-72.169h-180.847v-64.555h80.049c31.767
0 57.612-25.926 57.612-57.793
0-26.955-18.608-49.646-43.653-55.903-4.476-22.152-24.093-38.882-47.545-38.882-1.967
0-3.917.116-5.842.347-4.829-26.287-27.911-46.27-55.572-46.27s-50.744
19.983-55.572 46.269c-1.925-.23-3.876-.347-5.843-.347-23.452 0-43.069
16.73-47.544 38.882-25.044 6.256-43.653 28.948-43.653 55.903 0 31.867 25.844
57.793 57.612 57.793h80.049v64.555h-180. [...]
+ </g>
+ </SvgIcon>
+);
diff --git
a/pinot-controller/src/main/resources/app/components/SvgIcons/QueryConsoleIcon.tsx
b/pinot-controller/src/main/resources/app/components/SvgIcons/QueryConsoleIcon.tsx
index 81d41be..2870583 100644
---
a/pinot-controller/src/main/resources/app/components/SvgIcons/QueryConsoleIcon.tsx
+++
b/pinot-controller/src/main/resources/app/components/SvgIcons/QueryConsoleIcon.tsx
@@ -21,28 +21,18 @@ import * as React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
export default (props: SvgIconProps) => (
- <SvgIcon style={{ width: 24, height: 24, verticalAlign: 'middle' }}
viewBox="0 0 499.9 499.9" fill="none" {...props}>
- <g>
- <g>
- <path
d="M499.9,189.9c0-24.7-4.7-48.8-13.9-71.5c-9.5-23.6-23.6-44.7-41.7-62.8c-18.1-18.1-39.2-32.1-62.8-41.7
-
C358.8,4.7,334.7,0,310,0s-48.8,4.7-71.5,13.9c-23.6,9.5-44.7,23.6-62.8,41.7c-15.5,15.5-28.1,33.3-37.4,53
-
c-9,19-14.7,39.3-17,60.3c-4.6,42.1,5,84.9,27,120.7l2.1,3.4l-2.8,2.8L0,443.4l56.6,56.6l147.6-147.6l2.8-2.8l3.4,2.1
-
c29.9,18.4,64.4,28.2,99.7,28.2c24.7,0,48.8-4.7,71.6-13.9c23.6-9.5,44.7-23.6,62.8-41.6c18.1-18.1,32.1-39.2,41.7-62.8
- C495.3,238.7,499.9,214.6,499.9,189.9z
M186.7,341.5L60.1,468.1l-3.5,3.5l-3.5-3.5l-21.2-21.2l-3.5-3.5l3.5-3.5l126.6-126.6
-
l3.8-3.8l3.5,4.1c3.2,3.7,6.5,7.3,9.9,10.7c3.4,3.4,7,6.8,10.7,9.9l4.1,3.5L186.7,341.5z
M430.2,310.1
-
c-16.2,16.2-35.1,28.7-56.2,37.3c-20.4,8.2-41.9,12.4-64,12.4s-43.6-4.2-64-12.4c-21.1-8.5-40-21.1-56.2-37.3
-
s-28.7-35.1-37.3-56.2c-8.2-20.4-12.4-41.9-12.4-64c0-22.1,4.2-43.6,12.4-64c8.5-21.1,21.1-40,37.3-56.2
-
C206,53.5,224.9,41,246.1,32.4C266.4,24.2,288,20,310,20c0,0,0,0,0,0c22.1,0,43.6,4.2,64,12.4c21.1,8.5,40,21.1,56.2,37.3
- C496.5,136,496.5,243.8,430.2,310.1z"/>
- </g>
- <path d="M206.1,233.9h30v30h-30V233.9z"/>
- <path d="M266,233.9h30v30h-30V233.9z"/>
- <path d="M326,233.9h30v30h-30V233.9z"/>
- <path d="M386,233.9h30v30h-30V233.9z"/>
- <path d="M206.1,113.9h30v90h-30V113.9z"/>
- <path d="M266,113.9h30v90h-30V113.9z"/>
- <path d="M326,113.9h30v90h-30V113.9z"/>
- <path d="M386,113.9h30v90h-30V113.9z"/>
- </g>
+ <SvgIcon style={{ width: 24, height: 24, verticalAlign: 'middle' }}
viewBox="0 0 512 512" fill="none" {...props}>
+ <path
d="M401.6,246.5c17.3-24.9,27.5-55.1,27.5-87.7C429.1,74,360.1,5,275.3,5S121.5,74,121.5,158.8c0,20.4,4,39.9,11.2,57.7
+
c-32.8,0.3-63.7,5.1-87.3,13.6C7.9,243.5,0,261.8,0,274.8v86.8c0,0,0,0.1,0,0.1s0,0.1,0,0.1v86.8c0,13,7.9,31.4,45.5,44.8
+
c24.5,8.8,56.7,13.6,90.8,13.6s66.3-4.8,90.8-13.6c37.6-13.4,45.5-31.8,45.5-44.8v-86.8c0,0,0-0.1,0-0.1s0-0.1,0-0.1v-49.1
+
c0.9,0,1.8,0,2.7,0c18.1,0,35.4-3.1,51.5-8.9l109.3,148.8l75.8-55.7L401.6,246.5z
M55.6,258.3c21.3-7.6,50-11.8,80.7-11.8
+
c4.4,0,8.7,0.1,12.9,0.3c14.2,20.3,33.2,37,55.3,48.5c-19.5,5.2-43.1,8-68.2,8c-30.7,0-59.4-4.2-80.7-11.8
+ c-21-7.5-25.6-15.1-25.6-16.5S34.6,265.8,55.6,258.3z
M242.6,448.6c0,1.5-4.5,9-25.6,16.5c-21.3,7.6-50,11.8-80.7,11.8
+
c-30.7,0-59.4-4.2-80.7-11.8c-21-7.5-25.6-15.1-25.6-16.5v-48.7c4.5,2.3,9.6,4.5,15.5,6.6c24.5,8.8,56.7,13.6,90.8,13.6
+ s66.3-4.8,90.8-13.6c5.8-2.1,11-4.3,15.5-6.6V448.6z
M217,378.2c-21.3,7.6-50,11.8-80.7,11.8c-30.7,0-59.4-4.2-80.7-11.8
+
c-21-7.5-25.6-15.1-25.6-16.5v-48.5c4.5,2.3,9.6,4.5,15.5,6.6c24.5,8.8,56.7,13.6,90.8,13.6s66.3-4.8,90.8-13.6
+ c5.8-2.1,11-4.3,15.5-6.6v48.5h0C242.6,363.1,238,370.6,217,378.2z
M275.3,282.5c-68.2,0-123.8-55.5-123.8-123.8S207.1,35,275.3,35
+ s123.8,55.5,123.8,123.8C399.1,227,343.5,282.5,275.3,282.5z
M354.5,290.6c9.8-5.9,18.8-12.8,27-20.7L470,390.3l-27.4,20.2
+ L354.5,290.6z M200.3,143.8h30v30h-30V143.8z
M260.3,143.8h30v30h-30V143.8z M320.3,143.8h30v30h-30V143.8z"/>
</SvgIcon>
);
diff --git a/pinot-controller/src/main/resources/app/components/Table.tsx
b/pinot-controller/src/main/resources/app/components/Table.tsx
index dd95ad5..98b3e6c 100644
--- a/pinot-controller/src/main/resources/app/components/Table.tsx
+++ b/pinot-controller/src/main/resources/app/components/Table.tsx
@@ -45,7 +45,8 @@ import { NavLink } from 'react-router-dom';
import Chip from '@material-ui/core/Chip';
import _ from 'lodash';
import Utils from '../utils/Utils';
-import EnhancedTableToolbar from './EnhancedTableToolbar';
+import TableToolbar from './TableToolbar';
+import SimpleAccordion from './SimpleAccordion';
type Props = {
title?: string;
@@ -57,7 +58,10 @@ type Props = {
isCellClickable?: boolean,
highlightBackground?: boolean,
isSticky?: boolean
- baseURL?: string
+ baseURL?: string,
+ recordsCount?: number,
+ showSearchBox: boolean,
+ inAccordionFormat?: boolean
};
const StyledTableRow = withStyles((theme) =>
@@ -148,6 +152,10 @@ const useStyles = makeStyles((theme) => ({
cellStatusBad: {
color: '#f44336',
border: '1px solid #f44336',
+ },
+ cellStatusConsuming: {
+ color: '#ff9800',
+ border: '1px solid #ff9800',
}
}));
@@ -238,7 +246,10 @@ export default function CustomizedTables({
isCellClickable,
highlightBackground,
isSticky,
- baseURL
+ baseURL,
+ recordsCount,
+ showSearchBox,
+ inAccordionFormat
}: Props) {
const [finalData, setFinalData] = React.useState(Utils.tableFormat(data));
@@ -284,7 +295,7 @@ export default function CustomizedTables({
React.useEffect(() => {
clearTimeout(timeoutId.current);
timeoutId.current = setTimeout(() => {
- filterSearchResults(search);
+ filterSearchResults(search.toLowerCase());
}, 200);
return () => {
@@ -292,8 +303,8 @@ export default function CustomizedTables({
};
}, [search, timeoutId, filterSearchResults]);
- const styleCell = (str: string | number | boolean) => {
- if (str === 'Good') {
+ const styleCell = (str: string) => {
+ if (str === 'Good' || str.toLowerCase() === 'online' || str.toLowerCase()
=== 'alive') {
return (
<StyledChip
label={str}
@@ -302,7 +313,7 @@ export default function CustomizedTables({
/>
);
}
- if (str === 'Bad') {
+ if (str === 'Bad' || str.toLowerCase() === 'offline' || str.toLowerCase()
=== 'dead') {
return (
<StyledChip
label={str}
@@ -311,103 +322,147 @@ export default function CustomizedTables({
/>
);
}
+ if (str.toLowerCase() === 'consuming') {
+ return (
+ <StyledChip
+ label={str}
+ className={classes.cellStatusConsuming}
+ variant="outlined"
+ />
+ );
+ }
return str.toString();
};
- return (
- <div className={highlightBackground ? classes.highlightBackground :
classes.root}>
- {title ? (
- <EnhancedTableToolbar
- name={title}
- showSearchBox={true}
- searchValue={search}
- handleSearch={(val: string) => setSearch(val)}
- />
- ) : null}
- <TableContainer style={{ maxHeight: isSticky ? 400 : 600 }}>
- <Table className={classes.table} size="small" stickyHeader={isSticky}>
- <TableHead>
- <TableRow>
- {data.columns.map((column, index) => (
- <StyledTableCell
- className={classes.head}
- key={index}
- onClick={() => {
- setFinalData(_.orderBy(finalData, column, order ? 'asc' :
'desc'));
- setOrder(!order);
- setColumnClicked(column);
- }}
- >
- {column}
- {column === columnClicked ? order ? (
- <ArrowDropDownIcon
- color="primary"
- style={{ verticalAlign: 'middle' }}
- />
- ) : (
- <ArrowDropUpIcon
- color="primary"
- style={{ verticalAlign: 'middle' }}
- />
- ) : null}
- </StyledTableCell>
- ))}
- </TableRow>
- </TableHead>
- <TableBody className={classes.body}>
- {finalData.length === 0 ? (
+ const renderTableComponent = () => {
+ return (
+ <>
+ <TableContainer style={{ maxHeight: isSticky ? 400 : 500 }}>
+ <Table className={classes.table} size="small"
stickyHeader={isSticky}>
+ <TableHead>
<TableRow>
- <StyledTableCell
- className={classes.nodata}
- colSpan={data.columns.length}
- >
- No Record(s) found
- </StyledTableCell>
+ {data.columns.map((column, index) => (
+ <StyledTableCell
+ className={classes.head}
+ key={index}
+ onClick={() => {
+ setFinalData(_.orderBy(finalData, column, order ? 'asc'
: 'desc'));
+ setOrder(!order);
+ setColumnClicked(column);
+ }}
+ >
+ {column}
+ {column === columnClicked ? order ? (
+ <ArrowDropDownIcon
+ color="primary"
+ style={{ verticalAlign: 'middle' }}
+ />
+ ) : (
+ <ArrowDropUpIcon
+ color="primary"
+ style={{ verticalAlign: 'middle' }}
+ />
+ ) : null}
+ </StyledTableCell>
+ ))}
</TableRow>
- ) : (
- finalData
- .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
- .map((row, index) => (
- <StyledTableRow key={index} hover>
- {Object.values(row).map((cell, idx) =>
- addLinks && !idx ? (
- <StyledTableCell key={idx}>
- <NavLink
- className={classes.link}
- to={`${baseURL}${cell}`}
+ </TableHead>
+ <TableBody className={classes.body}>
+ {finalData.length === 0 ? (
+ <TableRow>
+ <StyledTableCell
+ className={classes.nodata}
+ colSpan={2}
+ >
+ No Record(s) found
+ </StyledTableCell>
+ </TableRow>
+ ) : (
+ finalData
+ .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
+ .map((row, index) => (
+ <StyledTableRow key={index} hover>
+ {Object.values(row).map((cell, idx) =>
+ addLinks && !idx ? (
+ <StyledTableCell key={idx}>
+ <NavLink
+ className={classes.link}
+ to={`${baseURL}${cell}`}
+ >
+ {cell}
+ </NavLink>
+ </StyledTableCell>
+ ) : (
+ <StyledTableCell
+ key={idx}
+ className={isCellClickable ?
classes.isCellClickable : (isSticky ? classes.isSticky : '')}
+ onClick={() => {cellClickCallback &&
cellClickCallback(cell);}}
>
- {cell}
- </NavLink>
- </StyledTableCell>
- ) : (
- <StyledTableCell
- key={idx}
- className={isCellClickable ? classes.isCellClickable
: (isSticky ? classes.isSticky : '')}
- onClick={() => {cellClickCallback &&
cellClickCallback(cell);}}
- >
- {styleCell(cell.toString())}
- </StyledTableCell>
- )
- )}
- </StyledTableRow>
- ))
- )}
- </TableBody>
- </Table>
- </TableContainer>
- {isPagination && finalData.length > 10 ? (
- <TablePagination
- rowsPerPageOptions={[5, 10, 25]}
- component="div"
- count={finalData.length}
- rowsPerPage={rowsPerPage}
- page={page}
- onChangePage={handleChangePage}
- onChangeRowsPerPage={handleChangeRowsPerPage}
- ActionsComponent={TablePaginationActions}
- classes={{ spacer: classes.spacer }}
+ {styleCell(cell.toString())}
+ </StyledTableCell>
+ )
+ )}
+ </StyledTableRow>
+ ))
+ )}
+ </TableBody>
+ </Table>
+ </TableContainer>
+ {isPagination && finalData.length > 10 ? (
+ <TablePagination
+ rowsPerPageOptions={[5, 10, 25]}
+ component="div"
+ count={finalData.length}
+ rowsPerPage={rowsPerPage}
+ page={page}
+ onChangePage={handleChangePage}
+ onChangeRowsPerPage={handleChangeRowsPerPage}
+ ActionsComponent={TablePaginationActions}
+ classes={{ spacer: classes.spacer }}
+ />
+ ) : null}
+ </>
+ );
+ };
+
+ const renderTable = () => {
+ return (
+ <>
+ <TableToolbar
+ name={title}
+ showSearchBox={showSearchBox}
+ searchValue={search}
+ handleSearch={(val: string) => setSearch(val)}
+ recordCount={recordsCount}
/>
- ) : null}
+ {renderTableComponent()}
+ </>
+ );
+ };
+
+ const renderTableInAccordion = () => {
+ return (
+ <>
+ <SimpleAccordion
+ headerTitle={title}
+ showSearchBox={showSearchBox}
+ searchValue={search}
+ handleSearch={(val: string) => setSearch(val)}
+ recordCount={recordsCount}
+ >
+ {renderTableComponent()}
+ </SimpleAccordion>
+ </>
+ );
+ }
+
+ return (
+ <div className={highlightBackground ? classes.highlightBackground :
classes.root}>
+ {inAccordionFormat ?
+ renderTableInAccordion()
+ :
+ renderTable()
+ }
</div>
);
}
diff --git
a/pinot-controller/src/main/resources/app/components/EnhancedTableToolbar.tsx
b/pinot-controller/src/main/resources/app/components/TableToolbar.tsx
similarity index 88%
rename from
pinot-controller/src/main/resources/app/components/EnhancedTableToolbar.tsx
rename to pinot-controller/src/main/resources/app/components/TableToolbar.tsx
index 9417900..9286b6b 100644
---
a/pinot-controller/src/main/resources/app/components/EnhancedTableToolbar.tsx
+++ b/pinot-controller/src/main/resources/app/components/TableToolbar.tsx
@@ -29,6 +29,7 @@ type Props = {
showSearchBox: boolean;
searchValue?: string;
handleSearch?: Function;
+ recordCount?: number
};
const useToolbarStyles = makeStyles((theme) => ({
@@ -36,20 +37,23 @@ const useToolbarStyles = makeStyles((theme) => ({
paddingLeft: '15px',
paddingRight: '15px',
minHeight: 48,
+ backgroundColor: 'rgba(66, 133, 244, 0.1)'
},
title: {
flex: '1 1 auto',
fontWeight: 600,
letterSpacing: '1px',
- fontSize: '1rem'
+ fontSize: '1rem',
+ color: '#4285f4'
},
}));
-export default function EnhancedTableToolbar({
+export default function TableToolbar({
name,
showSearchBox,
searchValue,
- handleSearch
+ handleSearch,
+ recordCount
}: Props) {
const classes = useToolbarStyles();
@@ -66,7 +70,7 @@ export default function EnhancedTableToolbar({
{showSearchBox ? <SearchBar
value={searchValue}
onChange={(e) => handleSearch(e.target.value)}
- /> : null}
+ /> : <strong>{(recordCount)}</strong>}
</Toolbar>
);
}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/interfaces/types.d.ts
b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
index 056ae41..7d42884 100644
--- a/pinot-controller/src/main/resources/app/interfaces/types.d.ts
+++ b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
@@ -82,6 +82,7 @@ declare module 'Models' {
type schema = {
name: string,
dataType: string
+ fieldType?: string
};
export type SQLResult = {
@@ -91,6 +92,26 @@ declare module 'Models' {
columnNames: Array<string>;
}
rows: Array<Array<number | string>>;
- };
+ },
+ timeUsedMs: number
+ numDocsScanned: number
+ totalDocs: number
+ numServersQueried: number
+ numServersResponded: number
+ numSegmentsQueried: number
+ numSegmentsProcessed: number
+ numSegmentsMatched: number
+ numConsumingSegmentsQueried: number
+ numEntriesScannedInFilter: number
+ numEntriesScannedPostFilter: number
+ numGroupsLimitReached: boolean
+ partialResponse?: number
+ minConsumingFreshnessTimeMs: number
};
+
+ export type ClusterName = {
+ clusterName: string
+ }
+
+ export type LiveInstances = Array<string>
}
diff --git a/pinot-controller/src/main/resources/app/pages/Query.tsx
b/pinot-controller/src/main/resources/app/pages/Query.tsx
index c9e5460..0464866 100644
--- a/pinot-controller/src/main/resources/app/pages/Query.tsx
+++ b/pinot-controller/src/main/resources/app/pages/Query.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-nested-ternary */
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -22,20 +23,23 @@ import { makeStyles } from '@material-ui/core/styles';
import { Grid, Checkbox, Button } from '@material-ui/core';
import Alert from '@material-ui/lab/Alert';
import FileCopyIcon from '@material-ui/icons/FileCopy';
-import { TableData, SQLResult } from 'Models';
+import { TableData } from 'Models';
import { UnControlled as CodeMirror } from 'react-codemirror2';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/sql/sql';
import _ from 'lodash';
+import FormControlLabel from '@material-ui/core/FormControlLabel';
+import Switch from '@material-ui/core/Switch';
import exportFromJSON from 'export-from-json';
import Utils from '../utils/Utils';
-import { getQueryTables, getTableSchema, getQueryResult } from '../requests';
import AppLoader from '../components/AppLoader';
import CustomizedTables from '../components/Table';
import QuerySideBar from '../components/Query/QuerySideBar';
-import EnhancedTableToolbar from '../components/EnhancedTableToolbar';
+import TableToolbar from '../components/TableToolbar';
+import SimpleAccordion from '../components/SimpleAccordion';
+import PinotMethodUtils from '../utils/PinotMethodUtils';
const useStyles = makeStyles((theme) => ({
title: {
@@ -49,7 +53,7 @@ const useStyles = makeStyles((theme) => ({
'& .CodeMirror': { height: 100, border: '1px solid #BDCCD9' },
},
queryOutput: {
- border: '1px solid #BDCCD9',
+ '& .CodeMirror': { height: 430, border: '1px solid #BDCCD9' },
},
btn: {
margin: '10px 10px 0 0',
@@ -70,6 +74,9 @@ const useStyles = makeStyles((theme) => ({
border: '1px #BDCCD9 solid',
borderRadius: 4,
marginBottom: '20px',
+ },
+ sqlError: {
+ whiteSpace: 'pre'
}
}));
@@ -94,6 +101,7 @@ const sqloptions = {
const QueryPage = () => {
const classes = useStyles();
const [fetching, setFetching] = useState(true);
+ const [queryLoader, setQueryLoader] = useState(false);
const [tableList, setTableList] = useState<TableData>({
columns: [],
records: [],
@@ -114,9 +122,17 @@ const QueryPage = () => {
const [outputResult, setOutputResult] = useState('');
+ const [resultError, setResultError] = useState('');
+
+ const [queryStats, setQueryStats] = useState<TableData>({
+ columns: [],
+ records: []
+ });
+
const [checked, setChecked] = React.useState({
tracing: false,
querySyntaxPQL: false,
+ showResultJSON: false
});
const [copyMsg, showCopyMsg] = React.useState(false);
@@ -129,15 +145,8 @@ const QueryPage = () => {
setInputQuery(value);
};
- const getAsObject = (str: SQLResult) => {
- if (typeof str === 'string' || str instanceof String) {
- return JSON.parse(JSON.stringify(str));
- }
- return str;
- };
-
- const handleRunNow = (query?: string) => {
- setFetching(true);
+ const handleRunNow = async (query?: string) => {
+ setQueryLoader(true);
let url;
let params;
if (checked.querySyntaxPQL) {
@@ -154,80 +163,17 @@ const QueryPage = () => {
});
}
- getQueryResult(params, url).then(({ data }) => {
- let queryResponse = null;
-
- queryResponse = getAsObject(data);
-
- let dataArray = [];
- let columnList = [];
- if (checked.querySyntaxPQL === true) {
- if (queryResponse) {
- if (queryResponse.selectionResults) {
- // Selection query
- columnList = queryResponse.selectionResults.columns;
- dataArray = queryResponse.selectionResults.results;
- } else if (!queryResponse.aggregationResults[0]?.groupByResult) {
- // Simple aggregation query
- columnList = _.map(
- queryResponse.aggregationResults,
- (aggregationResult) => {
- return { title: aggregationResult.function };
- }
- );
-
- dataArray.push(
- _.map(queryResponse.aggregationResults, (aggregationResult) => {
- return aggregationResult.value;
- })
- );
- } else if (queryResponse.aggregationResults[0]?.groupByResult) {
- // Aggregation group by query
- // TODO - Revisit
- const columns = queryResponse.aggregationResults[0].groupByColumns;
- columns.push(queryResponse.aggregationResults[0].function);
- columnList = _.map(columns, (columnName) => {
- return columnName;
- });
-
- dataArray = _.map(
- queryResponse.aggregationResults[0].groupByResult,
- (aggregationGroup) => {
- const row = aggregationGroup.group;
- row.push(aggregationGroup.value);
- return row;
- }
- );
- }
- }
- } else if (queryResponse.resultTable?.dataSchema?.columnNames?.length) {
- columnList = queryResponse.resultTable.dataSchema.columnNames;
- dataArray = queryResponse.resultTable.rows;
- }
-
- setResultData({
- columns: columnList,
- records: dataArray,
- });
- setFetching(false);
-
- setOutputResult(JSON.stringify(data, null, 2));
- });
+ const results = await PinotMethodUtils.getQueryResults(params, url,
checked);
+ setResultError(results.error || '');
+ setResultData(results.result || {columns: [], records: []});
+ setQueryStats(results.queryStats || {columns: [], records: []});
+ setOutputResult(JSON.stringify(results.data, null, 2) || '');
+ setQueryLoader(false);
};
- const fetchSQLData = (tableName) => {
- getTableSchema(tableName).then(({ data }) => {
- const dimensionFields = data.dimensionFieldSpecs || [];
- const metricFields = data.metricFieldSpecs || [];
- const dateTimeField = data.dateTimeFieldSpecs || [];
- const columnList = [...dimensionFields, ...metricFields,
...dateTimeField];
- setTableSchema({
- columns: ['column', 'type'],
- records: columnList.map((field) => {
- return [field.name, field.dataType];
- }),
- });
- });
+ const fetchSQLData = async (tableName) => {
+ const result = await PinotMethodUtils.getTableSchemaData(tableName, false);
+ setTableSchema(result);
const query = `select * from ${tableName} limit 10`;
setInputQuery(query);
@@ -268,16 +214,14 @@ const QueryPage = () => {
}, 3000);
};
+ const fetchData = async () => {
+ const result = await PinotMethodUtils.getQueryTablesList();
+ setTableList(result);
+ setFetching(false);
+ };
+
useEffect(() => {
- getQueryTables().then(({ data }) => {
- setTableList({
- columns: ['Tables'],
- records: data.tables.map((table) => {
- return [table];
- }),
- });
- setFetching(false);
- });
+ fetchData();
}, []);
return fetching ? (
@@ -289,13 +233,14 @@ const QueryPage = () => {
tableList={tableList}
fetchSQLData={fetchSQLData}
tableSchema={tableSchema}
+ selectedTable={selectedTable}
/>
</Grid>
<Grid item xs style={{ padding: 20, backgroundColor: 'white', maxHeight:
'calc(100vh - 70px)', overflowY: 'auto' }}>
<Grid container>
<Grid item xs={12} className={classes.rightPanel}>
<div className={classes.sqlDiv}>
- <EnhancedTableToolbar name="SQL Editor" showSearchBox={false} />
+ <TableToolbar name="SQL Editor" showSearchBox={false} />
<CodeMirror
options={sqloptions}
value={inputQuery}
@@ -337,64 +282,111 @@ const QueryPage = () => {
</Grid>
</Grid>
- <Grid item xs style={{ backgroundColor: 'white' }}>
- {resultData.records.length ? (
- <>
- <Grid container className={classes.actionBtns}>
- <Button
- variant="contained"
- color="primary"
- size="small"
- className={classes.btn}
- onClick={() => downloadData('xls')}
- >
- Excel
- </Button>
- <Button
- variant="contained"
- color="primary"
- size="small"
- className={classes.btn}
- onClick={() => downloadData('csv')}
- >
- CSV
- </Button>
- <Button
- variant="contained"
- color="primary"
- size="small"
- className={classes.btn}
- onClick={() => copyToClipboard()}
- >
- Copy
- </Button>
- {copyMsg ? (
- <Alert
- icon={<FileCopyIcon fontSize="inherit" />}
- severity="info"
- >
- Copied {resultData.records.length} rows to Clipboard
- </Alert>
- ) : null}
- </Grid>
- <CustomizedTables
- title={selectedTable}
- data={resultData}
- isPagination
- isSticky={true}
- />
- </>
- ) : null}
- </Grid>
-
- {resultData.records.length ? (
- <CodeMirror
- options={jsonoptions}
- value={outputResult}
- className={classes.queryOutput}
- autoCursor={false}
- />
- ) : null}
+ {queryLoader ?
+ <AppLoader />
+ :
+ <>
+ {
+ resultError ?
+ <Alert severity="error"
className={classes.sqlError}>{resultError}</Alert>
+ :
+ <>
+ {queryStats.records.length ?
+ <Grid item xs style={{ backgroundColor: 'white' }}>
+ <CustomizedTables
+ title="Query Response Stats"
+ data={queryStats}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
+ </Grid>
+ : null
+ }
+
+ <Grid item xs style={{ backgroundColor: 'white' }}>
+ {resultData.records.length ? (
+ <>
+ <Grid container className={classes.actionBtns}>
+ <Button
+ variant="contained"
+ color="primary"
+ size="small"
+ className={classes.btn}
+ onClick={() => downloadData('xls')}
+ >
+ Excel
+ </Button>
+ <Button
+ variant="contained"
+ color="primary"
+ size="small"
+ className={classes.btn}
+ onClick={() => downloadData('csv')}
+ >
+ CSV
+ </Button>
+ <Button
+ variant="contained"
+ color="primary"
+ size="small"
+ className={classes.btn}
+ onClick={() => copyToClipboard()}
+ >
+ Copy
+ </Button>
+ {copyMsg ? (
+ <Alert
+ icon={<FileCopyIcon fontSize="inherit" />}
+ severity="info"
+ >
+ Copied {resultData.records.length} rows to
Clipboard
+ </Alert>
+ ) : null}
+
+ <FormControlLabel
+ control={
+ <Switch
+ checked={checked.showResultJSON}
+ onChange={handleChange}
+ name="showResultJSON"
+ color="primary"
+ />
+ }
+ label="Show JSON format"
+ className={classes.runNowBtn}
+ />
+ </Grid>
+ {!checked.showResultJSON
+ ?
+ <CustomizedTables
+ title="Query Result"
+ data={resultData}
+ isPagination
+ isSticky={true}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
+ :
+ resultData.records.length ? (
+ <SimpleAccordion
+ headerTitle="Query Result (JSON Format)"
+ showSearchBox={false}
+ >
+ <CodeMirror
+ options={jsonoptions}
+ value={outputResult}
+ className={classes.queryOutput}
+ autoCursor={false}
+ />
+ </SimpleAccordion>
+ ) : null}
+ </>
+ ) : null}
+ </Grid>
+ </>
+ }
+ </>
+ }
</Grid>
</Grid>
</Grid>
diff --git a/pinot-controller/src/main/resources/app/pages/SegmentDetails.tsx
b/pinot-controller/src/main/resources/app/pages/SegmentDetails.tsx
new file mode 100644
index 0000000..731b8b4
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/pages/SegmentDetails.tsx
@@ -0,0 +1,170 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState, useEffect } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import { Grid } from '@material-ui/core';
+import { RouteComponentProps } from 'react-router-dom';
+import { UnControlled as CodeMirror } from 'react-codemirror2';
+import AppLoader from '../components/AppLoader';
+import TableToolbar from '../components/TableToolbar';
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/theme/material.css';
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/mode/sql/sql';
+import SimpleAccordion from '../components/SimpleAccordion';
+import CustomizedTables from '../components/Table';
+import PinotMethodUtils from '../utils/PinotMethodUtils';
+
+const useStyles = makeStyles((theme) => ({
+ root: {
+ border: '1px #BDCCD9 solid',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ highlightBackground: {
+ border: '1px #4285f4 solid',
+ backgroundColor: 'rgba(66, 133, 244, 0.05)',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ body: {
+ borderTop: '1px solid #BDCCD9',
+ fontSize: '16px',
+ lineHeight: '3rem',
+ paddingLeft: '15px',
+ },
+ queryOutput: {
+ border: '1px solid #BDCCD9',
+ '& .CodeMirror': { height: 532 },
+ },
+ sqlDiv: {
+ border: '1px #BDCCD9 solid',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+}));
+
+const jsonoptions = {
+ lineNumbers: true,
+ mode: 'application/json',
+ styleActiveLine: true,
+ gutters: ['CodeMirror-lint-markers'],
+ lint: true,
+ theme: 'default',
+};
+
+type Props = {
+ tenantName: string;
+ tableName: string;
+ segmentName: string;
+};
+
+type Summary = {
+ segmentName: string;
+ totalDocs: string | number;
+ createTime: unknown;
+};
+
+const SegmentDetails = ({ match }: RouteComponentProps<Props>) => {
+ const classes = useStyles();
+ const { tableName, segmentName } = match.params;
+
+ const [fetching, setFetching] = useState(true);
+ const [segmentSummary, setSegmentSummary] = useState<Summary>({
+ segmentName,
+ totalDocs: '',
+ createTime: '',
+ });
+
+ const [replica, setReplica] = useState({
+ columns: [],
+ records: []
+ });
+
+ const [value, setValue] = useState('');
+ const fetchData = async () => {
+ const result = await PinotMethodUtils.getSegmentDetails(tableName,
segmentName);
+ setSegmentSummary(result.summary);
+ setReplica(result.replicaSet);
+ setValue(JSON.stringify(result.JSON, null, 2));
+ setFetching(false);
+ };
+ useEffect(() => {
+ fetchData();
+ }, []);
+ return fetching ? (
+ <AppLoader />
+ ) : (
+ <Grid
+ item
+ xs
+ style={{
+ padding: 20,
+ backgroundColor: 'white',
+ maxHeight: 'calc(100vh - 70px)',
+ overflowY: 'auto',
+ }}
+ >
+ <div className={classes.highlightBackground}>
+ <TableToolbar name="Summary" showSearchBox={false} />
+ <Grid container className={classes.body}>
+ <Grid item xs={6}>
+ <strong>Segment Name:</strong> {segmentSummary.segmentName}
+ </Grid>
+ <Grid item xs={3}>
+ <strong>Total Docs:</strong> {segmentSummary.totalDocs}
+ </Grid>
+ <Grid item xs={3}>
+ <strong>Create Time:</strong> {segmentSummary.createTime}
+ </Grid>
+ </Grid>
+ </div>
+
+ <Grid container spacing={2}>
+ <Grid item xs={6}>
+ <CustomizedTables
+ title="Replica Set"
+ data={replica}
+ isPagination={true}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
+ </Grid>
+ <Grid item xs={6}>
+ <div className={classes.sqlDiv}>
+ <SimpleAccordion
+ headerTitle="Metadata"
+ showSearchBox={false}
+ >
+ <CodeMirror
+ options={jsonoptions}
+ value={value}
+ className={classes.queryOutput}
+ autoCursor={false}
+ />
+ </SimpleAccordion>
+ </div>
+ </Grid>
+ </Grid>
+ </Grid>
+ );
+};
+
+export default SegmentDetails;
diff --git a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
index fe65ce0..edfefa7 100644
--- a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
+++ b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
@@ -22,18 +22,39 @@ import { makeStyles } from '@material-ui/core/styles';
import { Grid } from '@material-ui/core';
import { RouteComponentProps } from 'react-router-dom';
import { UnControlled as CodeMirror } from 'react-codemirror2';
+import { TableData } from 'Models';
+import _ from 'lodash';
import AppLoader from '../components/AppLoader';
-import { getTenantTableDetails } from '../requests';
-import EnhancedTableToolbar from '../components/EnhancedTableToolbar';
+import CustomizedTables from '../components/Table';
+import TableToolbar from '../components/TableToolbar';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/sql/sql';
+import SimpleAccordion from '../components/SimpleAccordion';
+import PinotMethodUtils from '../utils/PinotMethodUtils';
const useStyles = makeStyles((theme) => ({
+ root: {
+ border: '1px #BDCCD9 solid',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ highlightBackground: {
+ border: '1px #4285f4 solid',
+ backgroundColor: 'rgba(66, 133, 244, 0.05)',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ body: {
+ borderTop: '1px solid #BDCCD9',
+ fontSize: '16px',
+ lineHeight: '3rem',
+ paddingLeft: '15px',
+ },
queryOutput: {
border: '1px solid #BDCCD9',
- '& .CodeMirror': { height: 800 },
+ '& .CodeMirror': { height: 532 },
},
sqlDiv: {
border: '1px #BDCCD9 solid',
@@ -56,18 +77,58 @@ type Props = {
tableName: string;
};
+type Summary = {
+ tableName: string;
+ reportedSize: string | number;
+ estimatedSize: string | number;
+};
+
const TenantPageDetails = ({ match }: RouteComponentProps<Props>) => {
+ const { tenantName, tableName } = match.params;
const classes = useStyles();
const [fetching, setFetching] = useState(true);
+ const [tableSummary, setTableSummary] = useState<Summary>({
+ tableName: match.params.tableName,
+ reportedSize: '',
+ estimatedSize: '',
+ });
+
+ const [segmentList, setSegmentList] = useState<TableData>({
+ columns: [],
+ records: [],
+ });
+
+ const [tableSchema, setTableSchema] = useState<TableData>({
+ columns: [],
+ records: [],
+ });
const [value, setValue] = useState('');
+ const fetchTableData = async () => {
+ const result = await PinotMethodUtils.getTableSummaryData(tableName);
+ setTableSummary(result);
+ fetchSegmentData();
+ };
+
+ const fetchSegmentData = async () => {
+ const result = await PinotMethodUtils.getSegmentList(tableName);
+ setSegmentList(result);
+ fetchTableSchema();
+ };
+
+ const fetchTableSchema = async () => {
+ const result = await PinotMethodUtils.getTableSchemaData(tableName, true);
+ setTableSchema(result);
+ fetchTableJSON();
+ };
+
+ const fetchTableJSON = async () => {
+ const result = await PinotMethodUtils.getTableDetails(tableName);
+ setValue(JSON.stringify(result, null, 2));
+ setFetching(false);
+ };
useEffect(() => {
- getTenantTableDetails(match.params.tableName).then(
- ({ data }) => {
- setValue(JSON.stringify(data, null, 2));
- setFetching(false);
- }
- );
+ fetchTableData();
}, []);
return fetching ? (
<AppLoader />
@@ -82,15 +143,59 @@ const TenantPageDetails = ({ match }:
RouteComponentProps<Props>) => {
overflowY: 'auto',
}}
>
- <div className={classes.sqlDiv}>
- <EnhancedTableToolbar name={match.params.tableName}
showSearchBox={false} />
- <CodeMirror
- options={jsonoptions}
- value={value}
- className={classes.queryOutput}
- autoCursor={false}
- />
+ <div className={classes.highlightBackground}>
+ <TableToolbar name="Summary" showSearchBox={false} />
+ <Grid container className={classes.body}>
+ <Grid item xs={4}>
+ <strong>Table Name:</strong> {tableSummary.tableName}
+ </Grid>
+ <Grid item xs={4}>
+ <strong>Reported Size:</strong> {tableSummary.reportedSize}
+ </Grid>
+ <Grid item xs={4}>
+ <strong>Estimated Size: </strong>
+ {tableSummary.estimatedSize}
+ </Grid>
+ </Grid>
</div>
+
+ <Grid container spacing={2}>
+ <Grid item xs={6}>
+ <div className={classes.sqlDiv}>
+ <SimpleAccordion
+ headerTitle="Table Config"
+ showSearchBox={false}
+ >
+ <CodeMirror
+ options={jsonoptions}
+ value={value}
+ className={classes.queryOutput}
+ autoCursor={false}
+ />
+ </SimpleAccordion>
+ </div>
+ <CustomizedTables
+ title="Segments"
+ data={segmentList}
+ isPagination={false}
+ noOfRows={segmentList.records.length}
+ baseURL={`/tenants/${tenantName}/table/${tableName}/`}
+ addLinks
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
+ </Grid>
+ <Grid item xs={6}>
+ <CustomizedTables
+ title="Table Schema"
+ data={tableSchema}
+ isPagination={false}
+ noOfRows={tableSchema.records.length}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
+ </Grid>
+ </Grid>
</Grid>
);
};
diff --git a/pinot-controller/src/main/resources/app/pages/Tenants.tsx
b/pinot-controller/src/main/resources/app/pages/Tenants.tsx
index 30fef01..f425f47 100644
--- a/pinot-controller/src/main/resources/app/pages/Tenants.tsx
+++ b/pinot-controller/src/main/resources/app/pages/Tenants.tsx
@@ -23,7 +23,7 @@ import { TableData } from 'Models';
import { RouteComponentProps } from 'react-router-dom';
import CustomizedTables from '../components/Table';
import AppLoader from '../components/AppLoader';
-import { getTenantTable, getTableSize, getIdealState } from '../requests';
+import PinotMethodUtils from '../utils/PinotMethodUtils';
type Props = {
tenantName: string
@@ -32,61 +32,33 @@ type Props = {
const TenantPage = ({ match }: RouteComponentProps<Props>) => {
const tenantName = match.params.tenantName;
+ const columnHeaders = ['Table Name', 'Reported Size', 'Estimated Size',
'Number of Segments', 'Status'];
const [fetching, setFetching] = useState(true);
const [tableData, setTableData] = useState<TableData>({
- columns: [],
+ columns: columnHeaders,
records: []
});
+ const fetchData = async () => {
+ const result = await PinotMethodUtils.getTenantTableData(tenantName);
+ setTableData(result);
+ setFetching(false);
+ };
useEffect(() => {
- getTenantTable(tenantName).then(({ data }) => {
- const tableArr = data.tables.map(table => table);
- if(tableArr.length){
- const promiseArr = tableArr.map(name => getTableSize(name));
- const promiseArr2 = tableArr.map(name => getIdealState(name));
-
- Promise.all(promiseArr).then(results => {
- Promise.all(promiseArr2).then(response => {
- setTableData({
- columns: ['Table Name', 'Reported Size', 'Estimated Size',
'Number of Segments', 'Status'],
- records: [
- ...results.map(( result ) => {
- let actualValue; let idealValue;
- const tableSizeObj = result.data;
- response.forEach((res) => {
- const idealStateObj = res.data;
- if(tableSizeObj.realtimeSegments !== null &&
idealStateObj.REALTIME !== null){
- const { segments } = tableSizeObj.realtimeSegments;
- actualValue = Object.keys(segments).length;
- idealValue = Object.keys(idealStateObj.REALTIME).length;
- }else
- if(tableSizeObj.offlineSegments !== null &&
idealStateObj.OFFLINE !== null){
- const { segments } = tableSizeObj.offlineSegments;
- actualValue = Object.keys(segments).length;
- idealValue = Object.keys(idealStateObj.OFFLINE).length;
- }
- });
- return [tableSizeObj.tableName,
tableSizeObj.reportedSizeInBytes, tableSizeObj.estimatedSizeInBytes,
- `${actualValue} / ${idealValue}`, actualValue ===
idealValue ? 'Good' : 'Bad'];
- })
- ]
- });
- setFetching(false);
- });
- });
- }else {
- setTableData({
- columns: ['Table Name', 'Reported Size', 'Estimated Size', 'Number
of Segments', 'Status'],
- records: []
- });
- setFetching(false);
- }
- });
+ fetchData();
}, []);
return (
fetching ? <AppLoader /> :
<Grid item xs style={{ padding: 20, backgroundColor: 'white', maxHeight:
'calc(100vh - 70px)', overflowY: 'auto' }}>
- <CustomizedTables title={tenantName} data={tableData} isPagination
addLinks baseURL={`/tenants/${tenantName}/table/`} />
+ <CustomizedTables
+ title={tenantName}
+ data={tableData}
+ isPagination
+ addLinks
+ baseURL={`/tenants/${tenantName}/table/`}
+ showSearchBox={true}
+ inAccordionFormat={true}
+ />
</Grid>
);
};
diff --git a/pinot-controller/src/main/resources/app/requests/index.ts
b/pinot-controller/src/main/resources/app/requests/index.ts
index cf22df4..80d9eed 100644
--- a/pinot-controller/src/main/resources/app/requests/index.ts
+++ b/pinot-controller/src/main/resources/app/requests/index.ts
@@ -18,7 +18,7 @@
*/
import { AxiosResponse } from 'axios';
-import { TableData, Instances, Instance, Tenants, ClusterConfig, TableName,
TableSize, IdealState, QueryTables, TableSchema, SQLResult } from 'Models';
+import { TableData, Instances, Instance, Tenants, ClusterConfig, TableName,
TableSize, IdealState, QueryTables, TableSchema, SQLResult, ClusterName,
LiveInstances } from 'Models';
import { baseApi } from '../utils/axios-config';
export const getTenants = (): Promise<AxiosResponse<Tenants>> =>
@@ -33,12 +33,18 @@ export const getTenantTable = (name: string):
Promise<AxiosResponse<TableName>>
export const getTenantTableDetails = (tableName: string):
Promise<AxiosResponse<IdealState>> =>
baseApi.get(`/tables/${tableName}`);
+export const getSegmentMetadata = (tableName: string, segmentName: string):
Promise<AxiosResponse<IdealState>> =>
+ baseApi.get(`/segments/${tableName}/${segmentName}/metadata`);
+
export const getTableSize = (name: string): Promise<AxiosResponse<TableSize>>
=>
baseApi.get(`/tables/${name}/size`);
export const getIdealState = (name: string):
Promise<AxiosResponse<IdealState>> =>
baseApi.get(`/tables/${name}/idealstate`);
+export const getExternalView = (name: string):
Promise<AxiosResponse<IdealState>> =>
+ baseApi.get(`/tables/${name}/externalview`);
+
export const getInstances = (): Promise<AxiosResponse<Instances>> =>
baseApi.get('/instances');
@@ -55,4 +61,10 @@ export const getTableSchema = (name: string):
Promise<AxiosResponse<TableSchema>
baseApi.get(`/tables/${name}/schema`);
export const getQueryResult = (params: Object, url: string):
Promise<AxiosResponse<SQLResult>> =>
- baseApi.post(`/${url}`, params, { headers: { 'Content-Type':
'application/json; charset=UTF-8', 'Accept': 'text/plain, */*; q=0.01' } });
\ No newline at end of file
+ baseApi.post(`/${url}`, params, { headers: { 'Content-Type':
'application/json; charset=UTF-8', 'Accept': 'text/plain, */*; q=0.01' } });
+
+export const getClusterInfo = (): Promise<AxiosResponse<ClusterName>> =>
+ baseApi.get('/cluster/info');
+
+ export const getLiveInstancesFromClusterName = (params: string):
Promise<AxiosResponse<LiveInstances>> =>
+ baseApi.get(`/zk/ls?path=${params}`);
diff --git a/pinot-controller/src/main/resources/app/router.tsx
b/pinot-controller/src/main/resources/app/router.tsx
index 7a9a7ca..7023647 100644
--- a/pinot-controller/src/main/resources/app/router.tsx
+++ b/pinot-controller/src/main/resources/app/router.tsx
@@ -21,10 +21,12 @@ import HomePage from './pages/HomePage';
import TenantsPage from './pages/Tenants';
import TenantPageDetails from './pages/TenantDetails';
import QueryPage from './pages/Query';
+import SegmentDetails from './pages/SegmentDetails';
export default [
- { path: "/cluster", Component: HomePage },
+ { path: "/", Component: HomePage },
{ path: "/tenants/:tenantName", Component: TenantsPage },
{ path: "/tenants/:tenantName/table/:tableName", Component:
TenantPageDetails },
- { path: "/", Component: QueryPage }
+ { path: "/query", Component: QueryPage },
+ { path: "/tenants/:tenantName/table/:tableName/:segmentName", Component:
SegmentDetails }
];
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
new file mode 100644
index 0000000..2b01b09
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -0,0 +1,462 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import _ from 'lodash';
+import { SQLResult } from 'Models';
+import moment from 'moment';
+import {
+ getTenants,
+ getInstances,
+ getInstance,
+ getClusterConfig,
+ getQueryTables,
+ getTableSchema,
+ getQueryResult,
+ getTenantTable,
+ getTableSize,
+ getIdealState,
+ getExternalView,
+ getTenantTableDetails,
+ getSegmentMetadata,
+ getClusterInfo,
+ getLiveInstancesFromClusterName
+} from '../requests';
+import Utils from './Utils';
+
+// This method is used to display tenants listing on cluster manager home page
+// API: /tenants
+// Expected Output: {columns: [], records: []}
+const getTenantsData = () => {
+ return getTenants().then(({ data }) => {
+ const records = _.union(data.SERVER_TENANTS, data.BROKER_TENANTS);
+ return {
+ columns: ['Tenant Name', 'Server', 'Broker', 'Tables'],
+ records: [
+ ...records.map((record) => [
+ record,
+ data.SERVER_TENANTS.indexOf(record) > -1 ? 1 : 0,
+ data.BROKER_TENANTS.indexOf(record) > -1 ? 1 : 0,
+ '-',
+ ]),
+ ],
+ };
+ });
+};
+
+type DataTable = {
+ [name: string]: string[];
+};
+
+// This method is used to fetch all instances on cluster manager home page
+// API: /instances
+// Expected Output: {Controller: ['Controller1', 'Controller2'], Broker:
['Broker1', 'Broker2']}
+const getAllInstances = () => {
+ return getInstances().then(({ data }) => {
+ const initialVal: DataTable = {};
+ // It will create instances list array like
+ // {Controller: ['Controller1', 'Controller2'], Broker: ['Broker1',
'Broker2']}
+ const groupedData = data.instances.reduce((r, a) => {
+ const y = a.split('_');
+ const key = y[0].trim();
+ r[key] = [...(r[key] || []), a];
+ return r;
+ }, initialVal);
+ return groupedData;
+ });
+};
+
+// This method is used to display instance data on cluster manager home page
+// API: /instances/:instaneName
+// Expected Output: {columns: [], records: []}
+const getInstanceData = (instances, liveInstanceArr) => {
+ const promiseArr = [...instances.map((inst) => getInstance(inst))];
+
+ return Promise.all(promiseArr).then((result) => {
+ return {
+ columns: ['Insance Name', 'Enabled', 'Hostname', 'Port', 'Status'],
+ records: [
+ ...result.map(({ data }) => [
+ data.instanceName,
+ data.enabled,
+ data.hostName,
+ data.port,
+ liveInstanceArr.indexOf(data.instanceName) > -1 ? 'Alive' : 'Dead'
+ ]),
+ ],
+ };
+ });
+};
+
+// This method is used to fetch cluster name
+// API: /cluster/info
+// Expected Output: {clusterName: ''}
+const getClusterName = () => {
+ return getClusterInfo().then(({ data }) => {
+ return data.clusterName;
+ })
+}
+
+// This method is used to fetch array of live instances name
+// API: /zk/ls?path=:ClusterName/LIVEINSTANCES
+// Expected Output: []
+const getLiveInstance = (clusterName) => {
+ const params = encodeURIComponent(`/${clusterName}/LIVEINSTANCES`)
+ return getLiveInstancesFromClusterName(params).then((data) => {
+ return data;
+ })
+}
+
+// This method is used to diaplay cluster congifuration on cluster manager
home page
+// API: /cluster/configs
+// Expected Output: {columns: [], records: []}
+const getClusterConfigData = () => {
+ return getClusterConfig().then(({ data }) => {
+ return {
+ columns: ['Property', 'Value'],
+ records: [...Object.keys(data).map((key) => [key, data[key]])],
+ };
+ });
+};
+
+// This method is used to display table listing on query page
+// API: /tables
+// Expected Output: {columns: [], records: []}
+const getQueryTablesList = () => {
+ return getQueryTables().then(({ data }) => {
+ return {
+ columns: ['Tables'],
+ records: data.tables.map((table) => {
+ return [table];
+ }),
+ };
+ });
+};
+
+// This method is used to display particular table schema on query page
+// API: /tables/:tableName/schema
+// Expected Output: {columns: [], records: []}
+const getTableSchemaData = (tableName, showFieldType) => {
+ return getTableSchema(tableName).then(({ data }) => {
+ const dimensionFields = data.dimensionFieldSpecs || [];
+ const metricFields = data.metricFieldSpecs || [];
+ const dateTimeField = data.dateTimeFieldSpecs || [];
+
+ dimensionFields.map((field) => {
+ field.fieldType = 'Dimension';
+ });
+
+ metricFields.map((field) => {
+ field.fieldType = 'Metric';
+ });
+
+ dateTimeField.map((field) => {
+ field.fieldType = 'Date-Time';
+ });
+ const columnList = [...dimensionFields, ...metricFields, ...dateTimeField];
+ if (showFieldType) {
+ return {
+ columns: ['column', 'type', 'Field Type'],
+ records: columnList.map((field) => {
+ return [field.name, field.dataType, field.fieldType];
+ }),
+ };
+ }
+ return {
+ columns: ['column', 'type'],
+ records: columnList.map((field) => {
+ return [field.name, field.dataType];
+ }),
+ };
+ });
+};
+
+const getAsObject = (str: SQLResult) => {
+ if (typeof str === 'string' || str instanceof String) {
+ return JSON.parse(JSON.stringify(str));
+ }
+ return str;
+};
+
+// This method is used to display query output in tabular format as well as
JSON format on query page
+// API: /:urlName (Eg: sql or pql)
+// Expected Output: {columns: [], records: []}
+const getQueryResults = (params, url, checkedOptions) => {
+ return getQueryResult(params, url).then(({ data }) => {
+ let queryResponse = null;
+
+ queryResponse = getAsObject(data);
+
+ // if sql api throws error, handle here
+ if(typeof queryResponse === 'string'){
+ return {error: queryResponse};
+ } else if(queryResponse.exceptions.length){
+ return {error: JSON.stringify(queryResponse.exceptions, null, 2)};
+ }
+
+ let dataArray = [];
+ let columnList = [];
+ if (checkedOptions.querySyntaxPQL === true) {
+ if (queryResponse) {
+ if (queryResponse.selectionResults) {
+ // Selection query
+ columnList = queryResponse.selectionResults.columns;
+ dataArray = queryResponse.selectionResults.results;
+ } else if (!queryResponse.aggregationResults[0]?.groupByResult) {
+ // Simple aggregation query
+ columnList = _.map(
+ queryResponse.aggregationResults,
+ (aggregationResult) => {
+ return { title: aggregationResult.function };
+ }
+ );
+
+ dataArray.push(
+ _.map(queryResponse.aggregationResults, (aggregationResult) => {
+ return aggregationResult.value;
+ })
+ );
+ } else if (queryResponse.aggregationResults[0]?.groupByResult) {
+ // Aggregation group by query
+ // TODO - Revisit
+ const columns = queryResponse.aggregationResults[0].groupByColumns;
+ columns.push(queryResponse.aggregationResults[0].function);
+ columnList = _.map(columns, (columnName) => {
+ return columnName;
+ });
+
+ dataArray = _.map(
+ queryResponse.aggregationResults[0].groupByResult,
+ (aggregationGroup) => {
+ const row = aggregationGroup.group;
+ row.push(aggregationGroup.value);
+ return row;
+ }
+ );
+ }
+ }
+ } else if (queryResponse.resultTable?.dataSchema?.columnNames?.length) {
+ columnList = queryResponse.resultTable.dataSchema.columnNames;
+ dataArray = queryResponse.resultTable.rows;
+ }
+
+ const columnStats = [ 'timeUsedMs',
+ 'numDocsScanned',
+ 'totalDocs',
+ 'numServersQueried',
+ 'numServersResponded',
+ 'numSegmentsQueried',
+ 'numSegmentsProcessed',
+ 'numSegmentsMatched',
+ 'numConsumingSegmentsQueried',
+ 'numEntriesScannedInFilter',
+ 'numEntriesScannedPostFilter',
+ 'numGroupsLimitReached',
+ 'partialResponse',
+ 'minConsumingFreshnessTimeMs'];
+
+ return {
+ result: {
+ columns: columnList,
+ records: dataArray,
+ },
+ queryStats: {
+ columns: columnStats,
+ records: [[data.timeUsedMs, data.numDocsScanned, data.totalDocs,
data.numServersQueried, data.numServersResponded,
+ data.numSegmentsQueried, data.numSegmentsProcessed,
data.numSegmentsMatched, data.numConsumingSegmentsQueried,
+ data.numEntriesScannedInFilter, data.numEntriesScannedPostFilter,
data.numGroupsLimitReached,
+ data.partialResponse ? data.partialResponse : '-',
data.minConsumingFreshnessTimeMs]]
+ },
+ data,
+ };
+ });
+};
+
+// This method is used to display table data of a particular tenant
+// API: /tenants/:tenantName/tables
+// /tables/:tableName/size
+// /tables/:tableName/idealstate
+// /tables/:tableName/externalview
+// Expected Output: {columns: [], records: []}
+const getTenantTableData = (tenantName) => {
+ const columnHeaders = [
+ 'Table Name',
+ 'Reported Size',
+ 'Estimated Size',
+ 'Number of Segments',
+ 'Status',
+ ];
+ return getTenantTable(tenantName).then(({ data }) => {
+ const tableArr = data.tables.map((table) => table);
+ if (tableArr.length) {
+ const promiseArr = [];
+ tableArr.map((name) => {
+ promiseArr.push(getTableSize(name));
+ promiseArr.push(getIdealState(name));
+ promiseArr.push(getExternalView(name));
+ });
+
+ return Promise.all(promiseArr).then((results) => {
+ let finalRecordsArr = [];
+ let singleTableData = [];
+ let idealStateObj = null;
+ let externalViewObj = null;
+ results.map((result, index) => {
+ // since we have 3 promises, we are using mod 3 below
+ if (index % 3 === 0) {
+ // response of getTableSize API
+ const {
+ tableName,
+ reportedSizeInBytes,
+ estimatedSizeInBytes,
+ } = result.data;
+ singleTableData.push(
+ tableName,
+ reportedSizeInBytes,
+ estimatedSizeInBytes
+ );
+ } else if (index % 3 === 1) {
+ // response of getIdealState API
+ idealStateObj = result.data.OFFLINE || result.data.REALTIME;
+ } else if (index % 3 === 2) {
+ // response of getExternalView API
+ externalViewObj = result.data.OFFLINE || result.data.REALTIME;
+ const externalSegmentCount = Object.keys(externalViewObj).length;
+ const idealSegmentCount = Object.keys(idealStateObj).length;
+ // Generating data for the record
+ singleTableData.push(
+ `${externalSegmentCount} / ${idealSegmentCount}`,
+ Utils.getSegmentStatus(idealStateObj, externalViewObj)
+ );
+ // saving into records array
+ finalRecordsArr.push(singleTableData);
+ // resetting the required variables
+ singleTableData = [];
+ idealStateObj = null;
+ externalViewObj = null;
+ }
+ });
+ return {
+ columns: columnHeaders,
+ records: finalRecordsArr,
+ };
+ });
+ }
+ });
+};
+
+// This method is used to display summary of a particular tenant table
+// API: /tables/:tableName/size
+// Expected Output: {tableName: '', reportedSize: '', estimatedSize: ''}
+const getTableSummaryData = (tableName) => {
+ return getTableSize(tableName).then(({ data }) => {
+ return {
+ tableName: data.tableName,
+ reportedSize: data.reportedSizeInBytes,
+ estimatedSize: data.estimatedSizeInBytes,
+ };
+ });
+};
+
+// This method is used to display segment list of a particular tenant table
+// API: /tables/:tableName/idealstate
+// /tables/:tableName/externalview
+// Expected Output: {columns: [], records: []}
+const getSegmentList = (tableName) => {
+ const promiseArr = [];
+ promiseArr.push(getIdealState(tableName));
+ promiseArr.push(getExternalView(tableName));
+
+ return Promise.all(promiseArr).then((results) => {
+ const idealStateObj = results[0].data.OFFLINE || results[0].data.REALTIME;
+ const externalViewObj = results[1].data.OFFLINE ||
results[1].data.REALTIME;
+
+ return {
+ columns: ['Segment Name', 'Status'],
+ records: Object.keys(idealStateObj).map((key) => {
+ return [
+ key,
+ _.isEqual(idealStateObj[key], externalViewObj[key]) ? 'Good' : 'Bad',
+ ];
+ }),
+ };
+ });
+};
+
+// This method is used to display JSON format of a particular tenant table
+// API: /tables/:tableName/idealstate
+// /tables/:tableName/externalview
+// Expected Output: {columns: [], records: []}
+const getTableDetails = (tableName) => {
+ return getTenantTableDetails(tableName).then(({ data }) => {
+ return data;
+ });
+};
+
+// This method is used to display summary of a particular segment, replia set
as well as JSON format of a tenant table
+// API: /tables/tableName/externalview
+// /segments/:tableName/:segmentName/metadata
+// Expected Output: {columns: [], records: []}
+const getSegmentDetails = (tableName, segmentName) => {
+ const promiseArr = [];
+ promiseArr.push(getExternalView(tableName));
+ promiseArr.push(getSegmentMetadata(tableName, segmentName));
+
+ return Promise.all(promiseArr).then((results) => {
+ const obj = results[0].data.OFFLINE || results[0].data.REALTIME;
+ const segmentMetaData = results[1].data;
+
+ let result = [];
+ for (const prop in obj[segmentName]) {
+ if (obj[segmentName]) {
+ result.push([prop, obj[segmentName][prop]]);
+ }
+ }
+
+ return {
+ replicaSet: {
+ columns: ['Server Name', 'Status'],
+ records: [...result],
+ },
+ summary: {
+ segmentName,
+ totalDocs: segmentMetaData['segment.total.docs'],
+ createTime: moment(+segmentMetaData['segment.creation.time']).format(
+ 'MMMM Do YYYY, h:mm:ss'
+ ),
+ },
+ JSON: segmentMetaData
+ };
+ });
+};
+export default {
+ getTenantsData,
+ getAllInstances,
+ getInstanceData,
+ getClusterConfigData,
+ getQueryTablesList,
+ getTableSchemaData,
+ getQueryResults,
+ getTenantTableData,
+ getTableSummaryData,
+ getSegmentList,
+ getTableDetails,
+ getSegmentDetails,
+ getClusterName,
+ getLiveInstance
+};
diff --git a/pinot-controller/src/main/resources/app/utils/Utils.tsx
b/pinot-controller/src/main/resources/app/utils/Utils.tsx
index 4af560c..ad45b28 100644
--- a/pinot-controller/src/main/resources/app/utils/Utils.tsx
+++ b/pinot-controller/src/main/resources/app/utils/Utils.tsx
@@ -17,6 +17,8 @@
* under the License.
*/
+import _ from 'lodash';
+
const sortArray = function (sortingArr, keyName, ascendingFlag) {
if (ascendingFlag) {
return sortingArr.sort(function (a, b) {
@@ -55,7 +57,30 @@ const tableFormat = (data) => {
return results;
};
+const getSegmentStatus = (idealStateObj, externalViewObj) => {
+ const idealSegmentKeys = Object.keys(idealStateObj);
+ const idealSegmentCount = idealSegmentKeys.length;
+
+ const externalSegmentKeys = Object.keys(externalViewObj);
+ const externalSegmentCount = externalSegmentKeys.length;
+
+ if(idealSegmentCount !== externalSegmentCount){
+ return 'Bad';
+ }
+
+ let segmentStatus = 'Good';
+ idealSegmentKeys.map((segmentKey) => {
+ if(segmentStatus === 'Good'){
+ if( !_.isEqual( idealStateObj[segmentKey], externalViewObj[segmentKey] )
){
+ segmentStatus = 'Bad';
+ }
+ }
+ });
+ return segmentStatus;
+};
+
export default {
sortArray,
tableFormat,
+ getSegmentStatus
};
diff --git a/pinot-controller/src/main/resources/package.json
b/pinot-controller/src/main/resources/package.json
index e298c97..0634b4a 100644
--- a/pinot-controller/src/main/resources/package.json
+++ b/pinot-controller/src/main/resources/package.json
@@ -55,7 +55,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.29",
"@fortawesome/free-solid-svg-icons": "^5.13.1",
"@fortawesome/react-fontawesome": "^0.1.11",
- "@material-ui/core": "^4.9.11",
+ "@material-ui/core": "4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.51",
"@types/react-router-dom": "^5.1.5",
@@ -67,6 +67,7 @@
"html-loader": "0.5.5",
"html-webpack-plugin": "^4.2.1",
"lodash": "^4.17.17",
+ "moment": "^2.27.0",
"prop-types": "^15.7.2",
"react": "16.13.1",
"react-codemirror2": "^7.2.1",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]