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 <shahsan...@users.noreply.github.com> 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: commits-unsubscr...@pinot.apache.org For additional commands, e-mail: commits-h...@pinot.apache.org