diff --git a/pinot-controller/src/main/resources/app/components/AsyncInstanceTable.tsx b/pinot-controller/src/main/resources/app/components/AsyncInstanceTable.tsx new file mode 100644 index 000000000000..c64e49b6417b --- /dev/null +++ b/pinot-controller/src/main/resources/app/components/AsyncInstanceTable.tsx @@ -0,0 +1,122 @@ +/** + * 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 { get, lowerCase, mapKeys, startCase } from 'lodash'; +import { InstanceType, TableData } from 'Models'; +import CustomizedTables from './Table'; +import PinotMethodUtils from '../utils/PinotMethodUtils'; +import Utils from '../utils/Utils'; + +type BaseProps = { + instanceType: InstanceType; + showInstanceDetails?: boolean; +}; + +type ClusterProps = BaseProps & { + cluster: string; + tenant?: never; +}; + +type TenantProps = BaseProps & { + tenant: string; + cluster?: never; +}; + +type Props = ClusterProps | TenantProps; + +export const AsyncInstanceTable = ({ + instanceType, + cluster, + tenant, + showInstanceDetails = false, +}: Props) => { + const instanceColumns = showInstanceDetails + ? ['Instance Name', 'Enabled', 'Hostname', 'Port', 'Status'] + : ['Instance Name']; + const [instanceData, setInstanceData] = useState( + Utils.getLoadingTableData(instanceColumns) + ); + + const fetchInstances = async ( + instanceType: InstanceType, + tenant?: string + ): Promise => { + if (tenant) { + if (instanceType === InstanceType.BROKER) { + return PinotMethodUtils.getBrokerOfTenant(tenant).then( + (brokersData) => { + return Array.isArray(brokersData) ? brokersData : []; + } + ); + } else if (instanceType === InstanceType.SERVER) { + return PinotMethodUtils.getServerOfTenant(tenant).then( + (serversData) => { + return Array.isArray(serversData) ? serversData : []; + } + ); + } + } else { + return fetchInstancesOfType(instanceType); + } + }; + + const fetchInstancesOfType = async (instanceType: InstanceType) => { + return PinotMethodUtils.getAllInstances().then((instancesData) => { + const lowercaseInstanceData = mapKeys(instancesData, (value, key) => + lowerCase(key) + ); + return get(lowercaseInstanceData, lowerCase(instanceType)); + }); + }; + + useEffect(() => { + const instances = fetchInstances(instanceType, tenant); + if (showInstanceDetails) { + const instanceDetails = instances.then(async (instancesData) => { + const liveInstanceArr = await PinotMethodUtils.getLiveInstance(cluster); + return PinotMethodUtils.getInstanceData( + instancesData, + liveInstanceArr.data + ); + }); + instanceDetails.then((instanceDetailsData) => { + setInstanceData(instanceDetailsData); + }); + } else { + instances.then((instancesData) => { + setInstanceData({ + columns: instanceColumns, + records: instancesData.map((instance) => [instance]), + }); + }); + } + }, [instanceType, cluster, tenant, showInstanceDetails]); + + return ( + + ); +}; diff --git a/pinot-controller/src/main/resources/app/components/AsyncPinotSchemas.tsx b/pinot-controller/src/main/resources/app/components/AsyncPinotSchemas.tsx new file mode 100644 index 000000000000..accca7b33696 --- /dev/null +++ b/pinot-controller/src/main/resources/app/components/AsyncPinotSchemas.tsx @@ -0,0 +1,76 @@ +/** + * 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 { TableData } from 'Models'; +import PinotMethodUtils from '../utils/PinotMethodUtils'; +import Loading from './Loading'; +import CustomizedTables from './Table'; +import { getSchemaList } from '../requests'; +import Utils from '../utils/Utils'; + +type Props = { + onSchemaNamesLoaded?: Function; +}; + +export const AsyncPinotSchemas = ({ onSchemaNamesLoaded }: Props) => { + const [schemaDetails, setSchemaDetails] = useState( + Utils.getLoadingTableData(PinotMethodUtils.allSchemaDetailsColumnHeader) + ); + + const fetchSchemas = async () => { + getSchemaList().then((result) => { + if (onSchemaNamesLoaded) { + onSchemaNamesLoaded(result.data); + } + + const schemas = result.data; + const records = schemas.map((schema) => [ + ...[schema], + ...Array(PinotMethodUtils.allSchemaDetailsColumnHeader.length - 1) + .fill(null) + .map((_) => Loading), + ]); + setSchemaDetails({ + columns: PinotMethodUtils.allSchemaDetailsColumnHeader, + records: records, + }); + + // Schema details are typically fast to fetch, so we fetch them all at once. + PinotMethodUtils.getAllSchemaDetails(schemas).then((result) => { + setSchemaDetails(result); + }); + }); + }; + + useEffect(() => { + fetchSchemas(); + }, []); + + return ( + + ); +}; diff --git a/pinot-controller/src/main/resources/app/components/AsyncPinotTables.tsx b/pinot-controller/src/main/resources/app/components/AsyncPinotTables.tsx new file mode 100644 index 000000000000..e5505f9be01a --- /dev/null +++ b/pinot-controller/src/main/resources/app/components/AsyncPinotTables.tsx @@ -0,0 +1,220 @@ +/** + * 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 { flatten, uniq } from 'lodash'; +import CustomizedTables from './Table'; +import { PinotTableDetails, TableData } from 'Models'; +import { getQueryTables, getTenantTable } from '../requests'; +import Utils from '../utils/Utils'; +import PinotMethodUtils from '../utils/PinotMethodUtils'; + +type BaseProps = { + title: string; + baseUrl: string; + onTableNamesLoaded?: Function; +}; + +type InstanceProps = BaseProps & { + tenants?: never; + instance?: string; +}; + +type TenantProps = BaseProps & { + tenants?: string[]; + instance?: never; +}; + +type Props = InstanceProps | TenantProps; + +const TableTooltipData = [ + null, + 'Uncompressed size of all data segments with replication', + 'Estimated size of all data segments with replication, in case any servers are not reachable for actual size', + null, + 'GOOD if all replicas of all segments are up', +]; + +export const AsyncPinotTables = ({ + title, + instance, + tenants, + baseUrl, + onTableNamesLoaded, +}: Props) => { + const columnHeaders = [ + 'Table Name', + 'Reported Size', + 'Estimated Size', + 'Number of Segments', + 'Status', + ]; + const [tableData, setTableData] = useState( + Utils.getLoadingTableData(columnHeaders) + ); + + const fetchTenantData = async (tenants: string[]) => { + Promise.all( + tenants.map((tenant) => { + return getTenantTable(tenant); + }) + ).then((results) => { + let allTableDetails = results.map((result) => { + return result.data.tables.map((tableName) => { + const tableDetails: PinotTableDetails = { + name: tableName, + estimated_size: null, + reported_size: null, + segment_status: null, + number_of_segments: null, + }; + return tableDetails; + }); + }); + const allTableDetailsFlattened: PinotTableDetails[] = flatten( + allTableDetails + ); + + const loadingTableData: TableData = { + columns: columnHeaders, + records: allTableDetailsFlattened.map((td) => + Utils.pinotTableDetailsFormat(td) + ), + }; + setTableData(loadingTableData); + if (onTableNamesLoaded) { + onTableNamesLoaded(); + } + + results.forEach((result) => { + fetchAllTableDetails(result.data.tables); + }); + }); + }; + + const fetchInstanceTenants = async (instance: string): Promise => { + return PinotMethodUtils.getInstanceDetails(instance).then( + (instanceDetails) => { + const tenants = instanceDetails.tags + .filter((tag) => { + return ( + tag.search('_BROKER') !== -1 || + tag.search('_REALTIME') !== -1 || + tag.search('_OFFLINE') !== -1 + ); + }) + .map((tag) => { + return Utils.splitStringByLastUnderscore(tag)[0]; + }); + return uniq(tenants); + } + ); + }; + + const fetchAllTablesData = async () => { + Promise.all([getQueryTables('realtime'), getQueryTables('offline')]).then( + (results) => { + const realtimeTables = results[0].data.tables; + const offlineTables = results[1].data.tables; + const allTables = realtimeTables.concat(offlineTables); + setTableData({ + columns: columnHeaders, + records: allTables.map((tableName) => { + const tableDetails: PinotTableDetails = { + name: tableName, + estimated_size: null, + reported_size: null, + segment_status: null, + number_of_segments: null, + }; + return Utils.pinotTableDetailsFormat(tableDetails); + }), + }); + if (onTableNamesLoaded) { + onTableNamesLoaded(); + } + fetchAllTableDetails(allTables); + } + ); + }; + + const fetchAllTableDetails = async (tables: string[]) => { + return tables.forEach((tableName) => { + PinotMethodUtils.getSegmentCountAndStatus(tableName).then( + ({ segment_count, segment_status }) => { + setTableData((prevState) => { + const newRecords = [...prevState.records]; + const index = newRecords.findIndex( + (record) => record[0] === tableName + ); + newRecords[index] = Utils.pinotTableDetailsFormat({ + ...Utils.pinotTableDetailsFromArray(newRecords[index]), + number_of_segments: segment_count, + segment_status: segment_status, + }); + return { ...prevState, records: newRecords }; + }); + } + ); + + PinotMethodUtils.getTableSizes(tableName).then( + ({ reported_size, estimated_size }) => { + setTableData((prevState) => { + const newRecords = [...prevState.records]; + const index = newRecords.findIndex( + (record) => record[0] === tableName + ); + newRecords[index] = Utils.pinotTableDetailsFormat({ + ...Utils.pinotTableDetailsFromArray(newRecords[index]), + estimated_size: estimated_size, + reported_size: reported_size, + }); + return { ...prevState, records: newRecords }; + }); + } + ); + }); + }; + + useEffect(() => { + if (instance) { + fetchInstanceTenants(instance).then((tenants) => { + fetchTenantData(tenants); + }); + } else if (tenants) { + fetchTenantData(tenants); + } else { + fetchAllTablesData(); + } + }, [instance, tenants]); + + return ( + + ); +}; + +export default AsyncPinotTables; 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 733beb7357f6..334dc0d39ecb 100644 --- a/pinot-controller/src/main/resources/app/components/Homepage/InstancesTables.tsx +++ b/pinot-controller/src/main/resources/app/components/Homepage/InstancesTables.tsx @@ -18,24 +18,34 @@ */ import React from 'react'; -import get from 'lodash/get'; -import has from 'lodash/has'; -import InstanceTable from './InstanceTable'; +import { startCase } from 'lodash'; +import { AsyncInstanceTable } from '../AsyncInstanceTable'; +import { InstanceType } from 'Models'; -const Instances = ({ instances, clusterName }) => { - const order = ['Controller', 'Broker', 'Server', 'Minion']; +type Props = { + clusterName: string; + instanceType?: InstanceType; +}; + + +const Instances = ({ clusterName, instanceType }: Props) => { + const order = [ + InstanceType.CONTROLLER, + InstanceType.BROKER, + InstanceType.SERVER, + InstanceType.MINION, + ]; return ( <> {order - .filter((key) => has(instances, key)) + .filter((key) => !instanceType || instanceType === key) .map((key) => { - const value = get(instances, key, []); return ( - ); })} diff --git a/pinot-controller/src/main/resources/app/components/Homepage/TenantsListing.tsx b/pinot-controller/src/main/resources/app/components/Homepage/TenantsListing.tsx index 40f7aa868a7c..e5e245479d7a 100644 --- a/pinot-controller/src/main/resources/app/components/Homepage/TenantsListing.tsx +++ b/pinot-controller/src/main/resources/app/components/Homepage/TenantsListing.tsx @@ -17,11 +17,70 @@ * under the License. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { union } from 'lodash'; import CustomizedTables from '../Table'; +import { TableData } from 'Models'; +import Loading from '../Loading'; +import { getTenants, getTenantTable } from '../../requests'; +import PinotMethodUtils from '../../utils/PinotMethodUtils'; + +const TenantsTable = () => { + const columns = ['Tenant Name', 'Server', 'Broker', 'Tables']; + const [tenantsData, setTenantsData] = useState({ + records: [columns.map((_) => Loading)], + columns: columns, + }); + + const fetchData = async () => { + getTenants().then((res) => { + const tenantNames = union( + res.data.SERVER_TENANTS, + res.data.BROKER_TENANTS + ); + setTenantsData({ + columns: columns, + records: tenantNames.map((tenantName) => { + return [tenantName, Loading, Loading, Loading]; + }), + }); + + tenantNames.forEach((tenantName) => { + Promise.all([ + PinotMethodUtils.getServerOfTenant(tenantName).then((res) => { + return res?.length || 0; + }), + PinotMethodUtils.getBrokerOfTenant(tenantName).then((res) => { + return Array.isArray(res) ? res?.length || 0 : 0; + }), + getTenantTable(tenantName).then((res) => { + return res?.data?.tables?.length || 0; + }), + ]).then((res) => { + const numServers = res[0]; + const numBrokers = res[1]; + const numTables = res[2]; + setTenantsData((prev) => { + const newRecords = prev.records.map((record) => { + if (record[0] === tenantName) { + return [tenantName, numServers, numBrokers, numTables]; + } + return record; + }); + return { + columns: prev.columns, + records: newRecords, + }; + }); + }); + }); + }); + }; + + useEffect(() => { + fetchData(); + }, []); -const TenantsTable = ({tenantsData}) => { - return ( }; + +export default Loading; diff --git a/pinot-controller/src/main/resources/app/components/NotFound.tsx b/pinot-controller/src/main/resources/app/components/NotFound.tsx new file mode 100644 index 000000000000..93af7854721b --- /dev/null +++ b/pinot-controller/src/main/resources/app/components/NotFound.tsx @@ -0,0 +1,69 @@ +/** + * 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 TableToolbar from './TableToolbar'; +import { Grid } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => ({ + root: { + border: '1px #BDCCD9 solid', + borderRadius: 4, + marginBottom: '20px', + }, + background: { + padding: 20, + backgroundColor: 'white', + maxHeight: 'calc(100vh - 70px)', + overflowY: 'auto', + }, + 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', + }, +})); + +type Props = { + message: String; +}; + +export default function NotFound(props: Props) { + const classes = useStyles(); + return ( + +
+ + + +

{props.message}

+
+
+
+
+ ); +} diff --git a/pinot-controller/src/main/resources/app/components/Table.tsx b/pinot-controller/src/main/resources/app/components/Table.tsx index b145bfab6d21..09226e28d582 100644 --- a/pinot-controller/src/main/resources/app/components/Table.tsx +++ b/pinot-controller/src/main/resources/app/components/Table.tsx @@ -537,7 +537,7 @@ export default function CustomizedTables({ const matches = baseURL.match(regex); url = baseURL.replace(matches[0], row[matches[0].replace(/:/g, '')]); } - return addLinks && !idx ? ( + return addLinks && typeof cell === 'string' && !idx ? ( {cell} 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 3025166c9b1f..39fab3d18a51 100644 --- a/pinot-controller/src/main/resources/app/interfaces/types.d.ts +++ b/pinot-controller/src/main/resources/app/interfaces/types.d.ts @@ -18,13 +18,31 @@ */ declare module 'Models' { + export type SegmentStatus = { + value: DISPLAY_SEGMENT_STATUS, + tooltip: string, + component?: JSX.Element + } + + export type LoadingRecord = { + customRenderer: JSX.Element + } + export type TableData = { - records: Array>; + records: Array>; columns: Array; error?: string; isLoading? : boolean }; + export type PinotTableDetails = { + name: string, + reported_size: string, + estimated_size: string, + number_of_segments: string, + segment_status: SegmentStatus, + } + type SchemaDetails = { schemaName: string, totalColumns: number, @@ -79,6 +97,12 @@ declare module 'Models' { segments: Object; }; + type SegmentMetadata = { + indexes?: any; + columns: Array; + code?: number; + }; + export type IdealState = { OFFLINE: Object | null; REALTIME: Object | null; @@ -97,6 +121,7 @@ declare module 'Models' { metricFieldSpecs?: Array; dateTimeFieldSpecs?: Array; error?: string; + code?: number; }; type schema = { @@ -145,7 +170,8 @@ declare module 'Models' { export type ZKConfig = { ctime: any, - mtime: any + mtime: any, + code?: number }; export type OperationResponse = any; @@ -253,6 +279,13 @@ declare module 'Models' { DISABLE = "disable" } + export const enum InstanceType { + BROKER = "broker", + CONTROLLER = "controller", + MINION = "minion", + SERVER = "server" + } + export const enum TableType { REALTIME = "realtime", OFFLINE = "offline" diff --git a/pinot-controller/src/main/resources/app/pages/HomePage.tsx b/pinot-controller/src/main/resources/app/pages/HomePage.tsx index 6733de697940..472fa01c7e60 100644 --- a/pinot-controller/src/main/resources/app/pages/HomePage.tsx +++ b/pinot-controller/src/main/resources/app/pages/HomePage.tsx @@ -17,19 +17,24 @@ * under the License. */ -import React, {useState, useEffect} from 'react'; +import React, { useState, useEffect } from 'react'; +import { get, union } from 'lodash'; import { Grid, makeStyles, Paper, Box } from '@material-ui/core'; -import { TableData, DataTable } from 'Models'; import { Link } from 'react-router-dom'; -import AppLoader from '../components/AppLoader'; import PinotMethodUtils from '../utils/PinotMethodUtils'; import TenantsListing from '../components/Homepage/TenantsListing'; import Instances from '../components/Homepage/InstancesTables'; import ClusterConfig from '../components/Homepage/ClusterConfig'; import useTaskTypesTable from '../components/Homepage/useTaskTypesTable'; +import Skeleton from '@material-ui/lab/Skeleton'; +import { getTenants } from '../requests'; const useStyles = makeStyles((theme) => ({ - paper:{ + paper: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', padding: '10px 0', height: '100%', color: '#4285f4', @@ -43,70 +48,98 @@ const useStyles = makeStyles((theme) => ({ '& h2, h4': { margin: 0, }, - '& h4':{ + '& h4': { textTransform: 'uppercase', letterSpacing: 1, - fontWeight: 600 + fontWeight: 600, }, '&:hover': { - borderColor: '#4285f4' - } + borderColor: '#4285f4', + }, }, gridContainer: { padding: 20, backgroundColor: 'white', maxHeight: 'calc(100vh - 70px)', - overflowY: 'auto' + overflowY: 'auto', }, paperLinks: { textDecoration: 'none', - height: '100%' - } + height: '100%', + }, })); const HomePage = () => { const classes = useStyles(); - const [fetching, setFetching] = useState(true); - const [tenantsData, setTenantsData] = useState({ records: [], columns: [] }); - const [instances, setInstances] = useState(); const [clusterName, setClusterName] = useState(''); - const [tables, setTables] = useState([]); + + const [fetchingTenants, setFetchingTenants] = useState(true); + const [tenantsCount, setTenantscount] = useState(0); + + const [fetchingInstances, setFetchingInstances] = useState(true); + const [controllerCount, setControllerCount] = useState(0); + const [brokerCount, setBrokerCount] = useState(0); + const [serverCount, setServerCount] = useState(0); + const [minionCount, setMinionCount] = useState(0); + // const [instances, setInstances] = useState(); + + const [fetchingTables, setFetchingTables] = useState(true); + const [tablesCount, setTablesCount] = useState(0); const { taskTypes, taskTypesTable } = useTaskTypesTable(); const fetchData = async () => { - const tenantsDataResponse = await PinotMethodUtils.getTenantsData(); - const instanceResponse = await PinotMethodUtils.getAllInstances(); - const tablesResponse = await PinotMethodUtils.getQueryTablesList({bothType: true}); - const tablesList = []; - tablesResponse.records.map((record)=>{ - tablesList.push(...record); + PinotMethodUtils.getAllInstances().then((res) => { + setControllerCount(get(res, 'Controller', []).length); + setBrokerCount(get(res, 'Broker', []).length); + setServerCount(get(res, 'Server', []).length); + setMinionCount(get(res, 'Minion', []).length); + setFetchingInstances(false); + }); + + PinotMethodUtils.getQueryTablesList({ bothType: true }).then((res) => { + setTablesCount(res.records.length); + setFetchingTables(false); }); - setTenantsData(tenantsDataResponse); - setInstances(instanceResponse); - setTables(tablesList); + + getTenants().then((res) => { + const tenantNames = union( + res.data.SERVER_TENANTS, + res.data.BROKER_TENANTS + ); + setTenantscount(tenantNames.length); + setFetchingTenants(false); + }); + + fetchClusterName().then((clusterNameRes) => { + setClusterName(clusterNameRes); + }); + }; + + const fetchClusterName = () => { let clusterNameRes = localStorage.getItem('pinot_ui:clusterName'); - if(!clusterNameRes){ - clusterNameRes = await PinotMethodUtils.getClusterName(); + if (!clusterNameRes) { + return PinotMethodUtils.getClusterName(); + } else { + return Promise.resolve(clusterNameRes); } - setClusterName(clusterNameRes); - setFetching(false); }; + useEffect(() => { fetchData(); }, []); - - return fetching ? ( - - ) : ( + + const loading = ; + + return (

Controllers

-

{Array.isArray(instances.Controller) ? instances.Controller.length : 0}

+

{fetchingInstances ? loading : controllerCount}

@@ -114,7 +147,8 @@ const HomePage = () => {

Brokers

-

{Array.isArray(instances.Broker) ? instances.Broker.length : 0}

+

{fetchingInstances ? loading : brokerCount}

+ {/*

{Array.isArray(instances.Broker) ? instances.Broker.length : 0}

*/}
@@ -122,7 +156,8 @@ const HomePage = () => {

Servers

-

{Array.isArray(instances.Server) ? instances.Server.length : 0}

+

{fetchingInstances ? loading : serverCount}

+ {/*

{Array.isArray(instances.Server) ? instances.Server.length : 0}

*/}
@@ -130,7 +165,8 @@ const HomePage = () => {

Minions

-

{Array.isArray(instances.Minion) ? instances.Minion.length : 0}

+

{fetchingInstances ? loading : minionCount}

+ {/*

{Array.isArray(instances.Minion) ? instances.Minion.length : 0}

*/}
@@ -138,7 +174,8 @@ const HomePage = () => {

Tenants

-

{Array.isArray(tenantsData.records) ? tenantsData.records.length : 0}

+

{fetchingTenants ? loading : tenantsCount}

+ {/*

{Array.isArray(tenantsData.records) ? tenantsData.records.length : 0}

*/}
@@ -146,7 +183,8 @@ const HomePage = () => {

Tables

-

{Array.isArray(tables) ? tables.length : 0}

+

{fetchingTables ? loading : tablesCount}

+ {/*

{Array.isArray(tables) ? tables.length : 0}

*/}
@@ -154,14 +192,18 @@ const HomePage = () => {

Minion Task Manager

-

{Array.isArray(taskTypes.records) ? taskTypes?.records?.length : 0}

+

+ {Array.isArray(taskTypes.records) + ? taskTypes?.records?.length + : 0} +

- - + + {taskTypesTable} @@ -169,4 +211,4 @@ const HomePage = () => { ); }; -export default HomePage; \ No newline at end of file +export default HomePage; diff --git a/pinot-controller/src/main/resources/app/pages/InstanceDetails.tsx b/pinot-controller/src/main/resources/app/pages/InstanceDetails.tsx index c9aa979f163f..8aaabd4a64e5 100644 --- a/pinot-controller/src/main/resources/app/pages/InstanceDetails.tsx +++ b/pinot-controller/src/main/resources/app/pages/InstanceDetails.tsx @@ -17,31 +17,32 @@ * under the License. */ -import React, { useState, useEffect } from 'react'; -import { Button, FormControlLabel, Grid, makeStyles, Switch, Tooltip } from '@material-ui/core'; +import React, { useEffect, useState } from 'react'; +import { + FormControlLabel, + Grid, + makeStyles, + Switch, + Tooltip, +} from '@material-ui/core'; import { UnControlled as CodeMirror } from 'react-codemirror2'; import 'codemirror/lib/codemirror.css'; import 'codemirror/theme/material.css'; import 'codemirror/mode/javascript/javascript'; -import { InstanceState, TableData } from 'Models'; +import { InstanceState, InstanceType } from 'Models'; import { RouteComponentProps } from 'react-router-dom'; import PinotMethodUtils from '../utils/PinotMethodUtils'; import AppLoader from '../components/AppLoader'; -import CustomizedTables from '../components/Table'; import SimpleAccordion from '../components/SimpleAccordion'; import CustomButton from '../components/CustomButton'; import EditTagsOp from '../components/Homepage/Operations/EditTagsOp'; import EditConfigOp from '../components/Homepage/Operations/EditConfigOp'; import { NotificationContext } from '../components/Notification/NotificationContext'; -import { uniq, startCase } from 'lodash'; +import { startCase } from 'lodash'; import Confirm from '../components/Confirm'; -import Utils from "../utils/Utils"; - -const instanceTypes = { - broker: 'BROKER', - minion: 'MINION', - server: 'SERVER', -} +import { getInstanceTypeFromInstanceName } from '../utils/Utils'; +import AsyncPinotTables from '../components/AsyncPinotTables'; +import NotFound from '../components/NotFound'; const useStyles = makeStyles((theme) => ({ codeMirrorDiv: { @@ -56,7 +57,7 @@ const useStyles = makeStyles((theme) => ({ border: '1px #BDCCD9 solid', borderRadius: 4, marginBottom: 20, - } + }, })); const jsonoptions = { @@ -65,38 +66,31 @@ const jsonoptions = { styleActiveLine: true, gutters: ['CodeMirror-lint-markers'], theme: 'default', - readOnly: true + readOnly: true, }; type Props = { - instanceName: string + instanceName: string; }; const InstanceDetails = ({ match }: RouteComponentProps) => { const classes = useStyles(); - const {instanceName} = match.params; - let instanceType; - if (instanceName.toLowerCase().startsWith(instanceTypes.broker.toLowerCase())) { - instanceType = instanceTypes.broker; - } else if (instanceName.toLowerCase().startsWith(instanceTypes.minion.toLowerCase())) { - instanceType = instanceTypes.minion; - } else { - instanceType = instanceTypes.server; - } - const clutserName = localStorage.getItem('pinot_ui:clusterName'); + const { instanceName } = match.params; + const instanceType = getInstanceTypeFromInstanceName(instanceName); + const clusterName = localStorage.getItem('pinot_ui:clusterName'); const [fetching, setFetching] = useState(true); + const [instanceNotFound, setInstanceNotFound] = useState(false); const [confirmDialog, setConfirmDialog] = React.useState(false); const [dialogDetails, setDialogDetails] = React.useState(null); const [instanceConfig, setInstanceConfig] = useState(null); const [liveConfig, setLiveConfig] = useState(null); const [instanceDetails, setInstanceDetails] = useState(null); - const [tableData, setTableData] = useState({ - columns: [], - records: [] - }); const [tagsList, setTagsList] = useState([]); - const [tagsErrorObj, setTagsErrorObj] = useState({isError: false, errorMessage: null}) + const [tagsErrorObj, setTagsErrorObj] = useState({ + isError: false, + errorMessage: null, + }); const [config, setConfig] = useState('{}'); const [state, setState] = React.useState({ @@ -105,123 +99,102 @@ const InstanceDetails = ({ match }: RouteComponentProps) => { const [showEditTag, setShowEditTag] = useState(false); const [showEditConfig, setShowEditConfig] = useState(false); - const {dispatch} = React.useContext(NotificationContext); + const { dispatch } = React.useContext(NotificationContext); const fetchData = async () => { - const configResponse = await PinotMethodUtils.getInstanceConfig(clutserName, instanceName); - const liveConfigResponse = await PinotMethodUtils.getLiveInstanceConfig(clutserName, instanceName); - const instanceDetails = await PinotMethodUtils.getInstanceDetails(instanceName); - const tenantListResponse = getTenants(instanceDetails); - setInstanceConfig(JSON.stringify(configResponse, null, 2)); - const instanceHost = instanceDetails.hostName.replace(`${startCase(instanceType.toLowerCase())}_`, ''); - const instancePutObj = { - host: instanceHost, - port: instanceDetails.port, - type: instanceType, - tags: instanceDetails.tags, - pools: instanceDetails.pools, - grpcPort: instanceDetails.grpcPort, - adminPort: instanceDetails.adminPort, - queryServicePort: instanceDetails.queryServicePort, - queryMailboxPort: instanceDetails.queryMailboxPort, - queriesDisabled: instanceDetails.queriesDisabled, - }; - setState({enabled: instanceDetails.enabled}); - setInstanceDetails(JSON.stringify(instancePutObj, null, 2)); - setLiveConfig(JSON.stringify(liveConfigResponse, null, 2)); - if(tenantListResponse){ - fetchTableDetails(tenantListResponse); + const configResponse = await PinotMethodUtils.getInstanceConfig( + clusterName, + instanceName + ); + + if (configResponse?.code === 404) { + setInstanceNotFound(true); } else { - setFetching(false); + const liveConfigResponse = await PinotMethodUtils.getLiveInstanceConfig( + clusterName, + instanceName + ); + const instanceDetails = await PinotMethodUtils.getInstanceDetails( + instanceName + ); + setInstanceConfig(JSON.stringify(configResponse, null, 2)); + const instanceHost = instanceDetails.hostName.replace( + `${startCase(instanceType.toLowerCase())}_`, + '' + ); + const instancePutObj = { + host: instanceHost, + port: instanceDetails.port, + type: instanceType, + tags: instanceDetails.tags, + pools: instanceDetails.pools, + grpcPort: instanceDetails.grpcPort, + adminPort: instanceDetails.adminPort, + queryServicePort: instanceDetails.queryServicePort, + queryMailboxPort: instanceDetails.queryMailboxPort, + queriesDisabled: instanceDetails.queriesDisabled, + }; + setState({ enabled: instanceDetails.enabled }); + setInstanceDetails(JSON.stringify(instancePutObj, null, 2)); + setLiveConfig(JSON.stringify(liveConfigResponse, null, 2)); } + setFetching(false); }; useEffect(() => { fetchData(); }, []); - const fetchTableDetails = (tenantList) => { - const promiseArr = []; - tenantList.map((tenantName) => { - promiseArr.push(PinotMethodUtils.getTenantTableData(tenantName)); - }); - const tenantTableData = { - columns: [], - records: [] - }; - Promise.all(promiseArr).then((results)=>{ - results.map((result)=>{ - tenantTableData.columns = result.columns; - tenantTableData.records.push(...result.records); - }); - setTableData(tenantTableData); - setFetching(false); - }); - }; - - const getTenants = (instanceDetails) => { - const tenantsList = []; - instanceDetails.tags.forEach((tag) => { - if(tag.search('_BROKER') !== -1 || - tag.search('_REALTIME') !== -1 || - tag.search('_OFFLINE') !== -1 - ){ - let [baseTag, ] = Utils.splitStringByLastUnderscore(tag); - tenantsList.push(baseTag); - } - }); - return uniq(tenantsList); - }; - - const handleTagsChange = (e: React.ChangeEvent, tags: Array|null) => { + const handleTagsChange = ( + e: React.ChangeEvent, + tags: Array | null + ) => { isTagsValid(tags); setTagsList(tags); }; const isTagsValid = (_tagsList) => { let isValid = true; - setTagsErrorObj({isError: false, errorMessage: null}); - _tagsList.map((tag)=>{ - if(!isValid){ + setTagsErrorObj({ isError: false, errorMessage: null }); + _tagsList.map((tag) => { + if (!isValid) { return; } - if(instanceType === 'BROKER'){ - if(!tag.endsWith('_BROKER')){ + if (instanceType === InstanceType.BROKER) { + if (!tag.endsWith('_BROKER')) { isValid = false; setTagsErrorObj({ isError: true, - errorMessage: "Tags should end with _BROKER." + errorMessage: 'Tags should end with _BROKER.', }); } - } else if(instanceType === 'SERVER'){ - if(!tag.endsWith('_REALTIME') && - !tag.endsWith('_OFFLINE') - ){ + } else if (instanceType === InstanceType.SERVER) { + if (!tag.endsWith('_REALTIME') && !tag.endsWith('_OFFLINE')) { isValid = false; setTagsErrorObj({ isError: true, - errorMessage: "Tags should end with _OFFLINE or _REALTIME." + errorMessage: 'Tags should end with _OFFLINE or _REALTIME.', }); } } }); return isValid; - } + }; const saveTagsAction = async (event, typedTag) => { let newTagsList = [...tagsList]; - if(typedTag.length > 0){ + if (typedTag.length > 0) { newTagsList.push(typedTag); } - if(!isTagsValid(newTagsList)){ + if (!isTagsValid(newTagsList)) { return; } const result = await PinotMethodUtils.updateTags(instanceName, newTagsList); - if(result.status){ - dispatch({type: 'success', message: result.status, show: true}); + if (result.status) { + dispatch({ type: 'success', message: result.status, show: true }); fetchData(); } else { - dispatch({type: 'error', message: result.error, show: true}); + dispatch({ type: 'error', message: result.error, show: true }); } setShowEditTag(false); }; @@ -230,18 +203,18 @@ const InstanceDetails = ({ match }: RouteComponentProps) => { setDialogDetails({ title: 'Drop Instance', content: 'Are you sure want to drop this instance?', - successCb: () => dropInstance() + successCb: () => dropInstance(), }); setConfirmDialog(true); }; const dropInstance = async () => { const result = await PinotMethodUtils.deleteInstance(instanceName); - if(result.status){ - dispatch({type: 'success', message: result.status, show: true}); + if (result.status) { + dispatch({ type: 'success', message: result.status, show: true }); fetchData(); } else { - dispatch({type: 'error', message: result.error, show: true}); + dispatch({ type: 'error', message: result.error, show: true }); } closeDialog(); }; @@ -249,19 +222,24 @@ const InstanceDetails = ({ match }: RouteComponentProps) => { const handleSwitchChange = (event) => { setDialogDetails({ title: state.enabled ? 'Disable Instance' : 'Enable Instance', - content: `Are you sure want to ${state.enabled ? 'disable' : 'enable'} this instance?`, - successCb: () => toggleInstanceState() + content: `Are you sure want to ${ + state.enabled ? 'disable' : 'enable' + } this instance?`, + successCb: () => toggleInstanceState(), }); setConfirmDialog(true); }; const toggleInstanceState = async () => { - const result = await PinotMethodUtils.toggleInstanceState(instanceName, state.enabled ? InstanceState.DISABLE : InstanceState.ENABLE); - if(result.status){ - dispatch({type: 'success', message: result.status, show: true}); + const result = await PinotMethodUtils.toggleInstanceState( + instanceName, + state.enabled ? InstanceState.DISABLE : InstanceState.ENABLE + ); + if (result.status) { + dispatch({ type: 'success', message: result.status, show: true }); fetchData(); } else { - dispatch({type: 'error', message: result.error, show: true}); + dispatch({ type: 'error', message: result.error, show: true }); } setState({ enabled: !state.enabled }); closeDialog(); @@ -272,13 +250,16 @@ const InstanceDetails = ({ match }: RouteComponentProps) => { }; const saveConfigAction = async () => { - if(JSON.parse(config)){ - const result = await PinotMethodUtils.updateInstanceDetails(instanceName, config); - if(result.status){ - dispatch({type: 'success', message: result.status, show: true}); + if (JSON.parse(config)) { + const result = await PinotMethodUtils.updateInstanceDetails( + instanceName, + config + ); + if (result.status) { + dispatch({ type: 'success', message: result.status, show: true }); fetchData(); } else { - dispatch({type: 'error', message: result.error, show: true}); + dispatch({ type: 'error', message: result.error, show: true }); } setShowEditConfig(false); } @@ -289,137 +270,154 @@ const InstanceDetails = ({ match }: RouteComponentProps) => { setDialogDetails(null); }; - return ( - fetching ? : - - {!instanceName.toLowerCase().startsWith('controller') && -
- -
- { - setTagsList(JSON.parse(instanceConfig)?.listFields?.TAG_LIST || []); - setShowEditTag(true); - }} - tooltipTitle="Add/remove tags from this node" - enableTooltip={true} - > - Edit Tags - - { - setConfig(instanceDetails); - setShowEditConfig(true); - }} - enableTooltip={true} - > - Edit Config - - - Drop - - - ; + } else if (instanceNotFound) { + return ; + } else { + return ( + + {!instanceName.toLowerCase().startsWith('controller') && ( +
+ +
+ { + setTagsList( + JSON.parse(instanceConfig)?.listFields?.TAG_LIST || [] + ); + setShowEditTag(true); + }} + tooltipTitle="Add/remove tags from this node" + enableTooltip={true} + > + Edit Tags + + { + setConfig(instanceDetails); + setShowEditConfig(true); + }} + enableTooltip={true} + > + Edit Config + + + Drop + + + + } + label="Enable" /> - } - label="Enable" - /> - -
-
-
} - - -
- - + +
- - {liveConfig ? - + )} + +
- : null} -
- {tableData.columns.length ? - +
+ + + +
+
+ ) : null} + + {instanceType == InstanceType.BROKER || + instanceType == InstanceType.SERVER ? ( + + ) : null} + { + setShowEditTag(false); + }} + saveTags={saveTagsAction} + tags={tagsList} + handleTagsChange={handleTagsChange} + error={tagsErrorObj} /> - : null} - {setShowEditTag(false);}} - saveTags={saveTagsAction} - tags={tagsList} - handleTagsChange={handleTagsChange} - error={tagsErrorObj} - /> - {setShowEditConfig(false);}} - saveConfig={saveConfigAction} - config={config} - handleConfigChange={handleConfigChange} - /> - {confirmDialog && dialogDetails && } - - ); + { + setShowEditConfig(false); + }} + saveConfig={saveConfigAction} + config={config} + handleConfigChange={handleConfigChange} + /> + {confirmDialog && dialogDetails && ( + + )} + + ); + } }; -export default InstanceDetails; \ No newline at end of file +export default InstanceDetails; diff --git a/pinot-controller/src/main/resources/app/pages/InstanceListingPage.tsx b/pinot-controller/src/main/resources/app/pages/InstanceListingPage.tsx index d224ef7a25ad..9d0070725ce4 100644 --- a/pinot-controller/src/main/resources/app/pages/InstanceListingPage.tsx +++ b/pinot-controller/src/main/resources/app/pages/InstanceListingPage.tsx @@ -20,10 +20,11 @@ import React, {useState, useEffect} from 'react'; import { Grid, makeStyles } from '@material-ui/core'; import { startCase, pick } from 'lodash'; -import { DataTable } from 'Models'; +import { DataTable, InstanceType } from 'Models'; import AppLoader from '../components/AppLoader'; import PinotMethodUtils from '../utils/PinotMethodUtils'; import Instances from '../components/Homepage/InstancesTables'; +import { getInstanceTypeFromString } from '../utils/Utils'; const useStyles = makeStyles(() => ({ gridContainer: { @@ -38,13 +39,9 @@ const InstanceListingPage = () => { const classes = useStyles(); const [fetching, setFetching] = useState(true); - const [instances, setInstances] = useState(); const [clusterName, setClusterName] = useState(''); const fetchData = async () => { - const instanceResponse = await PinotMethodUtils.getAllInstances(); - const instanceType = startCase(window.location.hash.split('/')[1].slice(0, -1)); - setInstances(pick(instanceResponse, instanceType)); let clusterNameRes = localStorage.getItem('pinot_ui:clusterName'); if(!clusterNameRes){ clusterNameRes = await PinotMethodUtils.getClusterName(); @@ -57,11 +54,13 @@ const InstanceListingPage = () => { fetchData(); }, []); + const instanceType = getInstanceTypeFromString(window.location.hash.split('/')[1].slice(0, -1)); + return fetching ? ( ) : ( - + ); }; diff --git a/pinot-controller/src/main/resources/app/pages/SchemaPageDetails.tsx b/pinot-controller/src/main/resources/app/pages/SchemaPageDetails.tsx index c02a84340e35..b44b3e1f7b26 100644 --- a/pinot-controller/src/main/resources/app/pages/SchemaPageDetails.tsx +++ b/pinot-controller/src/main/resources/app/pages/SchemaPageDetails.tsx @@ -19,7 +19,14 @@ import React, { useState, useEffect } from 'react'; import { makeStyles } from '@material-ui/core/styles'; -import { Checkbox, DialogContent, FormControlLabel, Grid, IconButton, Tooltip } from '@material-ui/core'; +import { + Checkbox, + DialogContent, + FormControlLabel, + Grid, + IconButton, + Tooltip, +} from '@material-ui/core'; import { RouteComponentProps, useHistory } from 'react-router-dom'; import { UnControlled as CodeMirror } from 'react-codemirror2'; import { TableData } from 'Models'; @@ -38,6 +45,7 @@ import Confirm from '../components/Confirm'; import CustomCodemirror from '../components/CustomCodemirror'; import CustomDialog from '../components/CustomDialog'; import { HelpOutlineOutlined } from '@material-ui/icons'; +import NotFound from '../components/NotFound'; const useStyles = makeStyles(() => ({ root: { @@ -69,8 +77,8 @@ const useStyles = makeStyles(() => ({ operationDiv: { border: '1px #BDCCD9 solid', borderRadius: 4, - marginBottom: 20 - } + marginBottom: 20, + }, })); const jsonoptions = { @@ -79,7 +87,7 @@ const jsonoptions = { styleActiveLine: true, gutters: ['CodeMirror-lint-markers'], theme: 'default', - readOnly: true + readOnly: true, }; type Props = { @@ -89,7 +97,7 @@ type Props = { }; type Summary = { - schemaName: string; + schemaName: string; reportedSize: string | number; estimatedSize: string | number; }; @@ -99,6 +107,7 @@ const SchemaPageDetails = ({ match }: RouteComponentProps) => { const classes = useStyles(); const history = useHistory(); const [fetching, setFetching] = useState(true); + const [schemaNotFound, setSchemaNotFound] = useState(false); const [] = useState({ schemaName: match.params.schemaName, reportedSize: '', @@ -111,7 +120,7 @@ const SchemaPageDetails = ({ match }: RouteComponentProps) => { const [confirmDialog, setConfirmDialog] = React.useState(false); const [dialogDetails, setDialogDetails] = React.useState(null); - const {dispatch} = React.useContext(NotificationContext); + const { dispatch } = React.useContext(NotificationContext); const [showEditConfig, setShowEditConfig] = useState(false); const [config, setConfig] = useState('{}'); @@ -122,24 +131,28 @@ const SchemaPageDetails = ({ match }: RouteComponentProps) => { }); const [tableConfig, setTableConfig] = useState(''); const [schemaJSON, setSchemaJSON] = useState(null); - const [actionType,setActionType] = useState(null); + const [actionType, setActionType] = useState(null); const [schemaJSONFormat, setSchemaJSONFormat] = useState(false); const [reloadSegmentsOnUpdate, setReloadSegmentsOnUpdate] = useState(false); const fetchTableSchema = async () => { const result = await PinotMethodUtils.getTableSchemaData(schemaName); - if(result.error){ + if (result?.code === 404) { + setSchemaNotFound(true); + setFetching(false); + } else if (result.error) { setSchemaJSON(null); setTableSchema({ columns: ['Column', 'Type', 'Field Type', 'Multi Value'], - records: [] + records: [], }); + setFetching(false); } else { setSchemaJSON(JSON.parse(JSON.stringify(result))); const tableSchema = Utils.syncTableSchemaData(result, true); setTableSchema(tableSchema); + fetchTableJSON(); } - fetchTableJSON(); }; const fetchTableJSON = async () => { @@ -148,9 +161,9 @@ const SchemaPageDetails = ({ match }: RouteComponentProps) => { setFetching(false); }; - useEffect(()=>{ + useEffect(() => { fetchTableSchema(); - },[]) + }, []); const handleConfigChange = (value: string) => { setConfig(value); @@ -158,34 +171,39 @@ const SchemaPageDetails = ({ match }: RouteComponentProps) => { const saveConfigAction = async () => { let configObj = JSON.parse(config); - if(actionType === 'editTable'){ - if(configObj.OFFLINE || configObj.REALTIME){ + if (actionType === 'editTable') { + if (configObj.OFFLINE || configObj.REALTIME) { configObj = configObj.OFFLINE || configObj.REALTIME; } const result = await PinotMethodUtils.updateTable(schemaName, configObj); syncResponse(result); - } else if(actionType === 'editSchema'){ - const result = await PinotMethodUtils.updateSchema(schemaJSON.schemaName, configObj, reloadSegmentsOnUpdate); + } else if (actionType === 'editSchema') { + const result = await PinotMethodUtils.updateSchema( + schemaJSON.schemaName, + configObj, + reloadSegmentsOnUpdate + ); syncResponse(result); } }; const syncResponse = (result) => { - if(result.status){ - dispatch({type: 'success', message: result.status, show: true}); + if (result.status) { + dispatch({ type: 'success', message: result.status, show: true }); setShowEditConfig(false); fetchTableJSON(); setReloadSegmentsOnUpdate(false); } else { - dispatch({type: 'error', message: result.error, show: true}); + dispatch({ type: 'error', message: result.error, show: true }); } }; const handleDeleteSchemaAction = () => { setDialogDetails({ title: 'Delete Schema', - content: 'Are you sure want to delete this schema? Any tables using this schema might not function correctly.', - successCb: () => deleteSchema() + content: + 'Are you sure want to delete this schema? Any tables using this schema might not function correctly.', + successCb: () => deleteSchema(), }); setConfirmDialog(true); }; @@ -204,143 +222,146 @@ const SchemaPageDetails = ({ match }: RouteComponentProps) => { const handleSegmentDialogHide = () => { setShowEditConfig(false); setReloadSegmentsOnUpdate(false); - } + }; - return fetching ? ( - - ) : ( - -
- -
- { - setActionType('editSchema'); - setConfig(JSON.stringify(schemaJSON, null, 2)); - setShowEditConfig(true); - }} - tooltipTitle="Edit Schema" - enableTooltip={true} - > - Edit Schema - - - Delete Schema - + if (fetching) { + return ; + } else if (schemaNotFound) { + return ; + } else { + return ( + +
+ +
+ { + setActionType('editSchema'); + setConfig(JSON.stringify(schemaJSON, null, 2)); + setShowEditConfig(true); + }} + tooltipTitle="Edit Schema" + enableTooltip={true} + > + Edit Schema + + + Delete Schema +
-
-
- - -
- - +
+ + +
+ + + +
+
+ + {!schemaJSONFormat ? ( + - -
+ ) : ( +
+ { + setSchemaJSONFormat(!schemaJSONFormat); + }, + }} + > + + +
+ )} + - - {!schemaJSONFormat ? - + + setReloadSegmentsOnUpdate(e.target.checked)} + name="reload" + /> + } + label="Reload all segments" /> - : -
- {setSchemaJSONFormat(!schemaJSONFormat);} + + + + + + { + handleConfigChange(newValue); }} - > - - -
- } -
- - {/* Segment config edit dialog */} - - - setReloadSegmentsOnUpdate(e.target.checked)} - name="reload" - /> - } - label="Reload all segments" - /> - - - - - - { - handleConfigChange(newValue); - }} - /> - - + /> + + - {confirmDialog && dialogDetails && } - - ); + {confirmDialog && dialogDetails && ( + + )} + + ); + } }; export default SchemaPageDetails; diff --git a/pinot-controller/src/main/resources/app/pages/SegmentDetails.tsx b/pinot-controller/src/main/resources/app/pages/SegmentDetails.tsx index 94294bbd4d7a..8ba9ed969648 100644 --- a/pinot-controller/src/main/resources/app/pages/SegmentDetails.tsx +++ b/pinot-controller/src/main/resources/app/pages/SegmentDetails.tsx @@ -18,11 +18,12 @@ */ import React, { useState, useEffect } from 'react'; +import moment from 'moment'; +import { keys } from 'lodash'; import { makeStyles } from '@material-ui/core/styles'; import { Grid } from '@material-ui/core'; import { RouteComponentProps, useHistory, useLocation } 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'; @@ -35,6 +36,15 @@ import CustomButton from '../components/CustomButton'; import Confirm from '../components/Confirm'; import { NotificationContext } from '../components/Notification/NotificationContext'; import Utils from '../utils/Utils'; +import { + getExternalView, + getSegmentDebugInfo, + getSegmentMetadata, +} from '../requests'; +import { SegmentMetadata } from 'Models'; +import Skeleton from '@material-ui/lab/Skeleton'; +import NotFound from '../components/NotFound'; +import AppLoader from '../components/AppLoader'; const useStyles = makeStyles((theme) => ({ root: { @@ -66,8 +76,8 @@ const useStyles = makeStyles((theme) => ({ operationDiv: { border: '1px #BDCCD9 solid', borderRadius: 4, - marginBottom: 20 - } + marginBottom: 20, + }, })); const jsonoptions = { @@ -76,7 +86,7 @@ const jsonoptions = { styleActiveLine: true, gutters: ['CodeMirror-lint-markers'], theme: 'default', - readOnly: true + readOnly: true, }; type Props = { @@ -95,38 +105,181 @@ const SegmentDetails = ({ match }: RouteComponentProps) => { const classes = useStyles(); const history = useHistory(); const location = useLocation(); - const { tableName, segmentName: encodedSegmentName} = match.params; + const { tableName, segmentName: encodedSegmentName } = match.params; const segmentName = Utils.encodeString(encodedSegmentName); - const [fetching, setFetching] = useState(true); const [confirmDialog, setConfirmDialog] = React.useState(false); const [dialogDetails, setDialogDetails] = React.useState(null); - const {dispatch} = React.useContext(NotificationContext); + const { dispatch } = React.useContext(NotificationContext); - const [segmentSummary, setSegmentSummary] = useState({ + const [initialLoad, setInitialLoad] = useState(true); + const [segmentNotFound, setSegmentNotFound] = useState(false); + + const initialSummary = { segmentName, - totalDocs: '', - createTime: '', - }); - - const [replica, setReplica] = useState({ - columns: [], - records: [] - }); - - const [indexes, setIndexes] = useState({ - columns: [], - records: [] - }); - const [value, setValue] = useState(''); + totalDocs: null, + createTime: null, + }; + const [segmentSummary, setSegmentSummary] = useState(initialSummary); + + const replicaColumns = ['Server Name', 'Status']; + const initialReplica = Utils.getLoadingTableData(replicaColumns); + const [replica, setReplica] = useState(initialReplica); + + const indexColumns = [ + 'Field Name', + 'Bloom Filter', + 'Dictionary', + 'Forward Index', + 'Sorted', + 'Inverted Index', + 'JSON Index', + 'Null Value Vector Reader', + 'Range Index', + ]; + const initialIndexes = Utils.getLoadingTableData(indexColumns); + const [indexes, setIndexes] = useState(initialIndexes); + const initialSegmentMetadataJson = 'Loading...'; + const [segmentMetadataJson, setSegmentMetadataJson] = useState( + initialSegmentMetadataJson + ); + const fetchData = async () => { - const result = await PinotMethodUtils.getSegmentDetails(tableName, segmentName); - setSegmentSummary(result.summary); - setIndexes(result.indexes); - setReplica(result.replicaSet); - setValue(JSON.stringify(result.JSON, null, 2)); - setFetching(false); + // reset all state in case the segment was reloaded or deleted. + setInitialData(); + + getSegmentMetadata(tableName, segmentName).then((result) => { + if (result.data?.code === 404) { + setSegmentNotFound(true); + setInitialLoad(false); + } else { + setInitialLoad(false); + setSummary(result.data); + setSegmentMetadata(result.data); + setSegmentIndexes(result.data); + } + }); + setSegmentReplicas(); + }; + + const setInitialData = () => { + setInitialLoad(true); + setSegmentSummary(initialSummary); + setReplica(initialReplica); + setIndexes(initialIndexes); + setSegmentMetadataJson(initialSegmentMetadataJson); + }; + + const setSummary = (segmentMetadata: SegmentMetadata) => { + const segmentMetaDataJson = { ...segmentMetadata }; + setSegmentSummary({ + segmentName, + totalDocs: segmentMetaDataJson['segment.total.docs'] || 0, + createTime: moment(+segmentMetaDataJson['segment.creation.time']).format( + 'MMMM Do YYYY, h:mm:ss' + ), + }); + }; + + const setSegmentMetadata = (segmentMetadata: SegmentMetadata) => { + const segmentMetaDataJson = { ...segmentMetadata }; + delete segmentMetaDataJson.indexes; + delete segmentMetaDataJson.columns; + setSegmentMetadataJson(JSON.stringify(segmentMetaDataJson, null, 2)); + }; + + const setSegmentIndexes = (segmentMetadata: SegmentMetadata) => { + setIndexes({ + columns: indexColumns, + records: Object.keys(segmentMetadata.indexes).map((fieldName) => [ + fieldName, + segmentMetadata.indexes?.[fieldName]?.['bloom-filter'] === 'YES', + segmentMetadata.indexes?.[fieldName]?.['dictionary'] === 'YES', + segmentMetadata.indexes?.[fieldName]?.['forward-index'] === 'YES', + ( + (segmentMetadata.columns || []).filter( + (row) => row.columnName === fieldName + )[0] || { sorted: false } + ).sorted, + segmentMetadata.indexes?.[fieldName]?.['inverted-index'] === 'YES', + segmentMetadata.indexes?.[fieldName]?.['json-index'] === 'YES', + segmentMetadata.indexes?.[fieldName]?.['null-value-vector-reader'] === + 'YES', + segmentMetadata.indexes?.[fieldName]?.['range-index'] === 'YES', + ]), + }); + }; + + const setSegmentReplicas = () => { + let [baseTableName, tableType] = Utils.splitStringByLastUnderscore( + tableName + ); + + getExternalView(tableName).then((results) => { + const externalView = results.data.OFFLINE || results.data.REALTIME; + + const records = keys(externalView?.[segmentName] || {}).map((prop) => { + const status = externalView?.[segmentName]?.[prop]; + return [ + prop, + { value: status, tooltip: `Segment is ${status.toLowerCase()}` }, + ]; + }); + + setReplica({ + columns: replicaColumns, + records: records, + }); + + getSegmentDebugInfo(baseTableName, tableType.toLowerCase()).then( + (debugInfo) => { + const segmentDebugInfo = debugInfo.data; + + let debugInfoObj = {}; + if (segmentDebugInfo && segmentDebugInfo[0]) { + const debugInfosObj = segmentDebugInfo[0].segmentDebugInfos?.find( + (o) => { + return o.segmentName === segmentName; + } + ); + if (debugInfosObj) { + const serverNames = keys(debugInfosObj?.serverState || {}); + serverNames?.map((serverName) => { + debugInfoObj[serverName] = + debugInfosObj.serverState[ + serverName + ]?.errorInfo?.errorMessage; + }); + } + } + + const records = keys(externalView?.[segmentName] || {}).map( + (prop) => { + const status = externalView?.[segmentName]?.[prop]; + return [ + prop, + status === 'ERROR' + ? { + value: status, + tooltip: debugInfoObj?.[prop] || 'testing', + } + : { + value: status, + tooltip: `Segment is ${status.toLowerCase()}`, + }, + ]; + } + ); + + setReplica({ + columns: replicaColumns, + records: records, + }); + } + ); + }); }; + useEffect(() => { fetchData(); }, []); @@ -139,22 +292,26 @@ const SegmentDetails = ({ match }: RouteComponentProps) => { const handleDeleteSegmentClick = () => { setDialogDetails({ title: 'Delete Segment', - content: 'Are you sure want to delete this instance? Data from this segment will be permanently deleted.', - successCb: () => handleDeleteSegment() + content: + 'Are you sure want to delete this instance? Data from this segment will be permanently deleted.', + successCb: () => handleDeleteSegment(), }); setConfirmDialog(true); }; const handleDeleteSegment = async () => { - const result = await PinotMethodUtils.deleteSegmentOp(tableName, segmentName); - if(result && result.status){ - dispatch({type: 'success', message: result.status, show: true}); + const result = await PinotMethodUtils.deleteSegmentOp( + tableName, + segmentName + ); + if (result && result.status) { + dispatch({ type: 'success', message: result.status, show: true }); fetchData(); } else { - dispatch({type: 'error', message: result.error, show: true}); + dispatch({ type: 'error', message: result.error, show: true }); } closeDialog(); - setTimeout(()=>{ + setTimeout(() => { history.push(Utils.navigateToPreviousPage(location, false)); }, 1000); }; @@ -163,122 +320,147 @@ const SegmentDetails = ({ match }: RouteComponentProps) => { setDialogDetails({ title: 'Reload Segment', content: 'Are you sure want to reload this segment?', - successCb: () => handleReloadOp() + successCb: () => handleReloadOp(), }); setConfirmDialog(true); }; const handleReloadOp = async () => { - const result = await PinotMethodUtils.reloadSegmentOp(tableName, segmentName); - if(result.status){ - dispatch({type: 'success', message: result.status, show: true}); + const result = await PinotMethodUtils.reloadSegmentOp( + tableName, + segmentName + ); + if (result.status) { + dispatch({ type: 'success', message: result.status, show: true }); fetchData(); } else { - dispatch({type: 'error', message: result.error, show: true}); + dispatch({ type: 'error', message: result.error, show: true }); } closeDialog(); - } + }; - return fetching ? ( - - ) : ( - -
- -
- {handleDeleteSegmentClick()}} - tooltipTitle="Delete Segment" - enableTooltip={true} - > - Delete Segment - - {handleReloadSegmentClick()}} - tooltipTitle="Reload the segment to apply changes such as indexing, column default values, etc" - enableTooltip={true} - > - Reload Segment - -
-
-
-
- - - - Segment Name: {unescape(segmentSummary.segmentName)} + if (initialLoad) { + return ; + } else if (segmentNotFound) { + return ; + } else { + return ( + +
+ +
+ { + handleDeleteSegmentClick(); + }} + tooltipTitle="Delete Segment" + enableTooltip={true} + > + Delete Segment + + { + handleReloadSegmentClick(); + }} + tooltipTitle="Reload the segment to apply changes such as indexing, column default values, etc" + enableTooltip={true} + > + Reload Segment + +
+
+
+
+ + + + Segment Name:{' '} + {unescape(segmentSummary.segmentName)} + + + + Total Docs: + + + {segmentSummary.totalDocs ? ( + segmentSummary.totalDocs + ) : ( + + )} + + + + + Create Time: + + + {segmentSummary.createTime ? ( + segmentSummary.createTime + ) : ( + + )} + + - - Total Docs: {segmentSummary.totalDocs} +
+ + + + - - Create Time: {segmentSummary.createTime} + +
+ + + +
-
- - + - -
- - - -
-
+ {confirmDialog && dialogDetails && ( + + )}
- - - - - - {confirmDialog && dialogDetails && } -
- ); + ); + } }; export default SegmentDetails; diff --git a/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx b/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx index 7a79d2ecdac7..0e586662d108 100644 --- a/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx +++ b/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx @@ -17,18 +17,15 @@ * under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Grid, makeStyles } from '@material-ui/core'; -import { TableData } from 'Models'; -import AppLoader from '../components/AppLoader'; -import PinotMethodUtils from '../utils/PinotMethodUtils'; -import CustomizedTables from '../components/Table'; import CustomButton from '../components/CustomButton'; import SimpleAccordion from '../components/SimpleAccordion'; import AddSchemaOp from '../components/Homepage/Operations/AddSchemaOp'; import AddOfflineTableOp from '../components/Homepage/Operations/AddOfflineTableOp'; import AddRealtimeTableOp from '../components/Homepage/Operations/AddRealtimeTableOp'; -import Skeleton from '@material-ui/lab/Skeleton'; +import AsyncPinotTables from '../components/AsyncPinotTables'; +import { AsyncPinotSchemas } from '../components/AsyncPinotSchemas'; const useStyles = makeStyles(() => ({ gridContainer: { @@ -44,27 +41,9 @@ const useStyles = makeStyles(() => ({ }, })); -const TableTooltipData = [ - null, - 'Uncompressed size of all data segments with replication', - 'Estimated size of all data segments with replication, in case any servers are not reachable for actual size', - null, - 'GOOD if all replicas of all segments are up', -]; - const TablesListingPage = () => { const classes = useStyles(); - const [schemaDetails, setSchemaDetails] = useState({ - columns: PinotMethodUtils.allSchemaDetailsColumnHeader, - records: [], - isLoading: true, - }); - const [tableData, setTableData] = useState({ - columns: PinotMethodUtils.allTableDetailsColumnHeader, - records: [], - isLoading: true, - }); const [showSchemaModal, setShowSchemaModal] = useState(false); const [showAddOfflineTableModal, setShowAddOfflineTableModal] = useState( false @@ -72,55 +51,11 @@ const TablesListingPage = () => { const [showAddRealtimeTableModal, setShowAddRealtimeTableModal] = useState( false ); - - const loading = { customRenderer: }; - - const fetchData = async () => { - const schemaResponse = await PinotMethodUtils.getQuerySchemaList(); - const schemaList = []; - const schemaData = []; - schemaResponse.records.map((record) => { - schemaList.push(...record); - }); - schemaList.map((schema) => { - schemaData.push([schema].concat([...Array(PinotMethodUtils.allSchemaDetailsColumnHeader.length - 1)].map((e) => loading))); - }); - const tablesResponse = await PinotMethodUtils.getQueryTablesList({ - bothType: true, - }); - const tablesList = []; - const tableData = []; - tablesResponse.records.map((record) => { - tablesList.push(...record); - }); - tablesList.map((table) => { - tableData.push([table].concat([...Array(PinotMethodUtils.allTableDetailsColumnHeader.length - 1)].map((e) => loading))); - }); - // Set the table data to "Loading..." at first as tableSize can take minutes to fetch - // for larger tables. - setTableData({ - columns: PinotMethodUtils.allTableDetailsColumnHeader, - records: tableData, - isLoading: false, - }); - - // Set just the column headers so these do not have to load with the data - setSchemaDetails({ - columns: PinotMethodUtils.allSchemaDetailsColumnHeader, - records: schemaData, - isLoading: false, - }); - - // these implicitly set isLoading=false by leaving it undefined - const tableDetails = await PinotMethodUtils.getAllTableDetails(tablesList); - setTableData(tableDetails); - const schemaDetailsData = await PinotMethodUtils.getAllSchemaDetails(schemaList); - setSchemaDetails(schemaDetailsData); - }; - - useEffect(() => { - fetchData(); - }, []); + // This is used to refresh the tables and schemas data after a new table or schema is added. + // This is quite hacky, but it's simpler than trying to useRef or useContext to maintain + // a link between this component and the child table and schema components. + const [tablesKey, setTablesKey] = useState(0); + const [schemasKey, setSchemasKey] = useState(0); return ( @@ -157,29 +92,20 @@ const TablesListingPage = () => {
- - + {showSchemaModal && ( { setShowSchemaModal(false); }} - fetchData={fetchData} + fetchData={() => { + setSchemasKey((prevKey) => prevKey + 1); + }} /> )} {showAddOfflineTableModal && ( @@ -187,7 +113,9 @@ const TablesListingPage = () => { hideModal={() => { setShowAddOfflineTableModal(false); }} - fetchData={fetchData} + fetchData={() => { + setTablesKey((prevKey) => prevKey + 1); + }} tableType={'OFFLINE'} /> )} @@ -196,7 +124,9 @@ const TablesListingPage = () => { hideModal={() => { setShowAddRealtimeTableModal(false); }} - fetchData={fetchData} + fetchData={() => { + setTablesKey((prevKey) => prevKey + 1); + }} tableType={'REALTIME'} /> )} diff --git a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx index 13b65ae0d12b..0fbd9c6af451 100644 --- a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx +++ b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx @@ -40,8 +40,10 @@ import Confirm from '../components/Confirm'; import { NotificationContext } from '../components/Notification/NotificationContext'; import Utils from '../utils/Utils'; import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'; -import { get } from "lodash"; +import { get, isEmpty } from "lodash"; import { SegmentStatusRenderer } from '../components/SegmentStatusRenderer'; +import Skeleton from '@material-ui/lab/Skeleton'; +import NotFound from '../components/NotFound'; const useStyles = makeStyles((theme) => ({ root: { @@ -98,8 +100,8 @@ type Props = { type Summary = { tableName: string; - reportedSize: string | number; - estimatedSize: string | number; + reportedSize: number; + estimatedSize: number; }; const TenantPageDetails = ({ match }: RouteComponentProps) => { @@ -108,13 +110,16 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { const history = useHistory(); const location = useLocation(); const [fetching, setFetching] = useState(true); - const [tableSummary, setTableSummary] = useState({ + const [tableNotFound, setTableNotFound] = useState(false); + + const initialTableSummary: Summary = { tableName: match.params.tableName, - reportedSize: '', - estimatedSize: '', - }); + reportedSize: null, + estimatedSize: null, + }; + const [tableSummary, setTableSummary] = useState(initialTableSummary); - const [state, setState] = React.useState({ + const [tableState, setTableState] = React.useState({ enabled: true, }); @@ -124,15 +129,14 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { const [showEditConfig, setShowEditConfig] = useState(false); const [config, setConfig] = useState('{}'); - const [instanceCountData, setInstanceCountData] = useState({ - columns: [], - records: [], - }); - const [segmentList, setSegmentList] = useState({ - columns: [], - records: [], - }); + const instanceColumns = ["Instance Name", "# of segments"]; + const loadingInstanceData = Utils.getLoadingTableData(instanceColumns); + const [instanceCountData, setInstanceCountData] = useState(loadingInstanceData); + + const segmentListColumns = ['Segment Name', 'Status']; + const loadingSegmentList = Utils.getLoadingTableData(segmentListColumns); + const [segmentList, setSegmentList] = useState(loadingSegmentList); const [tableSchema, setTableSchema] = useState({ columns: [], @@ -149,12 +153,37 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { const [schemaJSONFormat, setSchemaJSONFormat] = useState(false); const fetchTableData = async () => { + // We keep all the fetching inside this component since we need to be able + // to handle users making changes to the table and then reloading the json. setFetching(true); - const result = await PinotMethodUtils.getTableSummaryData(tableName); - setTableSummary(result); - fetchSegmentData(); + fetchSyncTableData().then(()=> { + setFetching(false); + if (!tableNotFound) { + fetchAsyncTableData(); + } + }); }; + const fetchSyncTableData = async () => { + return Promise.all([ + fetchTableSchema(), + fetchTableJSON(), + ]); + } + + const fetchAsyncTableData = async () => { + // set async data back to loading + setTableSummary(initialTableSummary); + setInstanceCountData(loadingInstanceData); + setSegmentList(loadingSegmentList); + + // load async data + PinotMethodUtils.getTableSummaryData(tableName).then((result) => { + setTableSummary(result); + }); + fetchSegmentData() + } + const fetchSegmentData = async () => { const result = await PinotMethodUtils.getSegmentList(tableName); const {columns, records, externalViewObj} = result; @@ -173,7 +202,7 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { instanceRecords.push([instanceName, instanceObj[instanceName]]); }) setInstanceCountData({ - columns: ["Instance Name", "# of segments"], + columns: instanceColumns, records: instanceRecords }); @@ -194,7 +223,6 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { ); setSegmentList({columns, records: segmentTableRows}); - fetchTableSchema(); }; const fetchTableSchema = async () => { @@ -210,26 +238,30 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { const tableSchema = Utils.syncTableSchemaData(result, true); setTableSchema(tableSchema); } - fetchTableJSON(); }; const fetchTableJSON = async () => { - const result = await PinotMethodUtils.getTableDetails(tableName); - if(result.error){ - setFetching(false); - dispatch({type: 'error', message: result.error, show: true}); - } else { - const tableObj:any = result.OFFLINE || result.REALTIME; - setTableType(tableObj.tableType); - setTableConfig(JSON.stringify(result, null, 2)); - fetchTableState(tableObj.tableType); - } + return PinotMethodUtils.getTableDetails(tableName).then((result) => { + if(result.error){ + dispatch({type: 'error', message: result.error, show: true}); + } else { + if (isEmpty(result)) { + setTableNotFound(true); + return; + } + const tableObj:any = result.OFFLINE || result.REALTIME; + setTableType(tableObj.tableType); + setTableConfig(JSON.stringify(result, null, 2)); + return fetchTableState(tableObj.tableType); + } + }); }; const fetchTableState = async (type) => { - const stateResponse = await PinotMethodUtils.getTableState(tableName, type); - setState({enabled: stateResponse.state === 'enabled'}); - setFetching(false); + return PinotMethodUtils.getTableState(tableName, type) + .then((stateResponse) => { + return setTableState({enabled: stateResponse.state === 'enabled'}); + }); }; useEffect(() => { @@ -238,15 +270,15 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { const handleSwitchChange = (event) => { setDialogDetails({ - title: state.enabled ? 'Disable Table' : 'Enable Table', - content: `Are you sure want to ${state.enabled ? 'disable' : 'enable'} this table?`, + title: tableState.enabled ? 'Disable Table' : 'Enable Table', + content: `Are you sure want to ${tableState.enabled ? 'disable' : 'enable'} this table?`, successCb: () => toggleTableState() }); setConfirmDialog(true); }; const toggleTableState = async () => { - const result = await PinotMethodUtils.toggleTableState(tableName, state.enabled ? InstanceState.DISABLE : InstanceState.ENABLE, tableType.toLowerCase() as TableType); + const result = await PinotMethodUtils.toggleTableState(tableName, tableState.enabled ? InstanceState.DISABLE : InstanceState.ENABLE, tableType.toLowerCase() as TableType); syncResponse(result); }; @@ -409,238 +441,257 @@ const TenantPageDetails = ({ match }: RouteComponentProps) => { setDialogDetails(null); }; - return fetching ? ( - - ) : ( - -
- -
- { - setActionType('editTable'); - setConfig(tableConfig); - setShowEditConfig(true); - }} - tooltipTitle="Edit Table" - enableTooltip={true} - > - Edit Table - - - Delete Table - - { - setActionType('editSchema'); - setConfig(JSON.stringify(schemaJSON, null, 2)); - setShowEditConfig(true); - }} - tooltipTitle="Edit Schema" - enableTooltip={true} - > - Edit Schema - - - Delete Schema - - {console.log('truncate table');}} - // tooltipTitle="Truncate Table" - // enableTooltip={true} - > - Truncate Table - - - Reload All Segments - - - Reload Status - - {setShowRebalanceServerModal(true);}} - tooltipTitle="Recalculates the segment to server mapping for this table" - enableTooltip={true} - > - Rebalance Servers - - - Rebalance Brokers - - - ; + } else if (tableNotFound) { + return ; + } else { + return ( + +
+ +
+ { + setActionType('editTable'); + setConfig(tableConfig); + setShowEditConfig(true); + }} + tooltipTitle="Edit Table" + enableTooltip={true} + > + Edit Table + + + Delete Table + + { + setActionType('editSchema'); + setConfig(JSON.stringify(schemaJSON, null, 2)); + setShowEditConfig(true); + }} + tooltipTitle="Edit Schema" + enableTooltip={true} + > + Edit Schema + + + Delete Schema + + {}} + tooltipTitle="Truncate Table" + enableTooltip={true} + > + Truncate Table + + + Reload All Segments + + + Reload Status + + {setShowRebalanceServerModal(true);}} + tooltipTitle="Recalculates the segment to server mapping for this table" + enableTooltip={true} + > + Rebalance Servers + + + Rebalance Brokers + + + + } + label="Enable" + /> + +
+
+
+
+ + + + Table Name: {tableSummary.tableName} + + + + + Reported Size: + + + + {/* Now Skeleton can be a block element because it's the only thing inside this grid item */} + {tableSummary.reportedSize ? + Utils.formatBytes(tableSummary.reportedSize) : + + } + + + + + + Estimated Size: + + + + {/* Now Skeleton can be a block element because it's the only thing inside this grid item */} + {tableSummary.estimatedSize ? + Utils.formatBytes(tableSummary.estimatedSize) : + + } + + + +
+ + + +
+ + + +
+ -
-
-
-
-
- - - - Table Name: {tableSummary.tableName} - - - Reported Size: {Utils.formatBytes(tableSummary.reportedSize)} - - - - - - Estimated Size: - {Utils.formatBytes(tableSummary.estimatedSize)} - - - - -
- - - -
- - + {!schemaJSONFormat ? + {setSchemaJSONFormat(!schemaJSONFormat);} + }} /> - -
- + {setSchemaJSONFormat(!schemaJSONFormat);} + }} + > + + + } - addLinks - showSearchBox={true} - inAccordionFormat={true} - /> -
- - {!schemaJSONFormat ? {setSchemaJSONFormat(!schemaJSONFormat);} - }} /> - : -
- {setSchemaJSONFormat(!schemaJSONFormat);} - }} - > - - -
- } - +
-
- {setShowEditConfig(false);}} - saveConfig={saveConfigAction} - config={config} - handleConfigChange={handleConfigChange} - /> - { - showReloadStatusModal && - {setShowReloadStatusModal(false); setReloadStatusData(null)}} - reloadStatusData={reloadStatusData} - tableJobsData={tableJobsData} + {setShowEditConfig(false);}} + saveConfig={saveConfigAction} + config={config} + handleConfigChange={handleConfigChange} /> - } - {showRebalanceServerModal && - {setShowRebalanceServerModal(false)}} - tableType={tableType.toUpperCase()} - tableName={tableName} - /> - } - {confirmDialog && dialogDetails && } - - ); + { + showReloadStatusModal && + {setShowReloadStatusModal(false); setReloadStatusData(null)}} + reloadStatusData={reloadStatusData} + tableJobsData={tableJobsData} + /> + } + {showRebalanceServerModal && + {setShowRebalanceServerModal(false)}} + tableType={tableType.toUpperCase()} + tableName={tableName} + /> + } + {confirmDialog && dialogDetails && } + + ); + } }; export default TenantPageDetails; diff --git a/pinot-controller/src/main/resources/app/pages/Tenants.tsx b/pinot-controller/src/main/resources/app/pages/Tenants.tsx index e9944c478b20..19f61ec6e11e 100644 --- a/pinot-controller/src/main/resources/app/pages/Tenants.tsx +++ b/pinot-controller/src/main/resources/app/pages/Tenants.tsx @@ -17,76 +17,47 @@ * under the License. */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { Grid, makeStyles } from '@material-ui/core'; -import { TableData } from 'Models'; +import { InstanceType } from 'Models'; import { RouteComponentProps } from 'react-router-dom'; -import CustomizedTables from '../components/Table'; -import AppLoader from '../components/AppLoader'; -import PinotMethodUtils from '../utils/PinotMethodUtils'; import SimpleAccordion from '../components/SimpleAccordion'; +import AsyncPinotTables from '../components/AsyncPinotTables'; import CustomButton from '../components/CustomButton'; +import { AsyncInstanceTable } from '../components/AsyncInstanceTable'; const useStyles = makeStyles((theme) => ({ operationDiv: { border: '1px #BDCCD9 solid', borderRadius: 4, - marginBottom: 20 - } + marginBottom: 20, + }, })); type Props = { - tenantName: string + tenantName: string; }; -const TableTooltipData = [ - null, - "Uncompressed size of all data segments with replication", - "Estimated size of all data segments with replication, in case any servers are not reachable for actual size", - null, - "GOOD if all replicas of all segments are up" -]; - const TenantPage = ({ match }: RouteComponentProps) => { - - const {tenantName} = match.params; - const columnHeaders = ['Table Name', 'Reported Size', 'Estimated Size', 'Number of Segments', 'Status']; - const [fetching, setFetching] = useState(true); - const [tableData, setTableData] = useState({ - columns: columnHeaders, - records: [] - }); - const [brokerData, setBrokerData] = useState(null); - const [serverData, setServerData] = useState([]); - - const fetchData = async () => { - const tenantData = await PinotMethodUtils.getTenantTableData(tenantName); - const brokersData = await PinotMethodUtils.getBrokerOfTenant(tenantName); - const serversData = await PinotMethodUtils.getServerOfTenant(tenantName); - setTableData(tenantData); - const separatedBrokers = Array.isArray(brokersData) ? brokersData.map((elm) => [elm]) : []; - setBrokerData(separatedBrokers || []); - const separatedServers = Array.isArray(serversData) ? serversData.map((elm) => [elm]) : []; - setServerData(separatedServers || []); - setFetching(false); - }; - useEffect(() => { - fetchData(); - }, []); - + const { tenantName } = match.params; const classes = useStyles(); return ( - fetching ? : - +
- +
{console.log('rebalance');}} + onClick={() => {}} tooltipTitle="Recalculates the segment to server mapping for all tables in this tenant" enableTooltip={true} isDisabled={true} @@ -94,7 +65,7 @@ const TenantPage = ({ match }: RouteComponentProps) => { Rebalance Server Tenant {console.log('rebuild');}} + onClick={() => {}} tooltipTitle="Rebuilds brokerResource mappings for all tables in this tenant" enableTooltip={true} isDisabled={true} @@ -104,40 +75,22 @@ const TenantPage = ({ match }: RouteComponentProps) => {
- - 0 ? brokerData : [] - }} - addLinks - baseURL="/instance/" - showSearchBox={true} - inAccordionFormat={true} + - 0 ? serverData : [] - }} - addLinks - baseURL="/instance/" - showSearchBox={true} - inAccordionFormat={true} + diff --git a/pinot-controller/src/main/resources/app/pages/TenantsListingPage.tsx b/pinot-controller/src/main/resources/app/pages/TenantsListingPage.tsx index 531d35e1ed01..2fc32803a078 100644 --- a/pinot-controller/src/main/resources/app/pages/TenantsListingPage.tsx +++ b/pinot-controller/src/main/resources/app/pages/TenantsListingPage.tsx @@ -17,46 +17,27 @@ * under the License. */ -import React, {useState, useEffect} from 'react'; +import React from 'react'; import { Grid, makeStyles } from '@material-ui/core'; -import { TableData } from 'Models'; -import AppLoader from '../components/AppLoader'; -import PinotMethodUtils from '../utils/PinotMethodUtils'; -import TenantsListing from '../components/Homepage/TenantsListing'; +import TenantsTable from '../components/Homepage/TenantsListing'; const useStyles = makeStyles(() => ({ gridContainer: { padding: 20, backgroundColor: 'white', maxHeight: 'calc(100vh - 70px)', - overflowY: 'auto' + overflowY: 'auto', }, - })); const TenantsListingPage = () => { const classes = useStyles(); - const [fetching, setFetching] = useState(true); - const [tenantsData, setTenantsData] = useState({ records: [], columns: [] }); - - const fetchData = async () => { - const tenantsDataResponse = await PinotMethodUtils.getTenantsData(); - setTenantsData(tenantsDataResponse); - setFetching(false); - }; - - useEffect(() => { - fetchData(); - }, []); - - return fetching ? ( - - ) : ( + return ( - + ); }; -export default TenantsListingPage; \ No newline at end of file +export default TenantsListingPage; diff --git a/pinot-controller/src/main/resources/app/requests/index.ts b/pinot-controller/src/main/resources/app/requests/index.ts index 7da6cb6683be..e2d4d54910ea 100644 --- a/pinot-controller/src/main/resources/app/requests/index.ts +++ b/pinot-controller/src/main/resources/app/requests/index.ts @@ -18,10 +18,34 @@ */ import { AxiosResponse } from 'axios'; -import { TableData, Instances, Instance, Tenants, ClusterConfig, TableName, TableSize, - IdealState, QueryTables, TableSchema, SQLResult, ClusterName, ZKGetList, ZKConfig, OperationResponse, - BrokerList, ServerList, UserList, TableList, UserObject, TaskProgressResponse, TableSegmentJobs, TaskRuntimeConfig, - SegmentDebugDetails, QuerySchemas, TableType, InstanceState +import { + TableData, + Instances, + Instance, + Tenants, + ClusterConfig, + TableName, + TableSize, + IdealState, + QueryTables, + TableSchema, + SQLResult, + ClusterName, + ZKGetList, + ZKConfig, + OperationResponse, + BrokerList, + ServerList, + UserList, + TableList, + UserObject, + TaskProgressResponse, + TableSegmentJobs, + TaskRuntimeConfig, + SegmentDebugDetails, + QuerySchemas, + TableType, + InstanceState, SegmentMetadata, } from 'Models'; const headers = { @@ -62,7 +86,7 @@ export const putSchema = (name: string, params: string, reload?: boolean): Promi return baseApi.put(`/schemas/${name}`, params, { headers, params: queryParams }); } -export const getSegmentMetadata = (tableName: string, segmentName: string): Promise> => +export const getSegmentMetadata = (tableName: string, segmentName: string): Promise> => baseApi.get(`/segments/${tableName}/${segmentName}/metadata?columns=*`); export const getTableSize = (name: string): Promise> => diff --git a/pinot-controller/src/main/resources/app/router.tsx b/pinot-controller/src/main/resources/app/router.tsx index 3975ebf50796..90bb5da9459c 100644 --- a/pinot-controller/src/main/resources/app/router.tsx +++ b/pinot-controller/src/main/resources/app/router.tsx @@ -37,6 +37,7 @@ import LoginPage from './pages/LoginPage'; import UserPage from "./pages/UserPage"; export default [ + // TODO: make async { path: '/', Component: HomePage }, { path: '/query', Component: QueryPage }, { path: '/tenants', Component: TenantsListingPage }, diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts index ef7488b0cfcc..7d971036ddf7 100644 --- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts +++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts @@ -19,7 +19,7 @@ import jwtDecode from "jwt-decode"; import { get, map, each, isEqual, isArray, keys, union } from 'lodash'; -import { DataTable, SqlException, SQLResult } from 'Models'; +import { DataTable, SegmentMetadata, SqlException, SQLResult, TableSize } from 'Models'; import moment from 'moment'; import { getTenants, @@ -352,7 +352,6 @@ const getTenantTableData = (tenantName) => { const getSchemaObject = async (schemaName) =>{ let schemaObj:Array = []; let {data} = await getSchema(schemaName); - console.log(data); schemaObj.push(data.schemaName); schemaObj.push(data.dimensionFieldSpecs ? data.dimensionFieldSpecs.length : 0); schemaObj.push(data.dateTimeFieldSpecs ? data.dateTimeFieldSpecs.length : 0); @@ -409,6 +408,30 @@ const allTableDetailsColumnHeader = [ 'Status', ]; +const getTableSizes = (tableName: string) => { + return getTableSize(tableName).then(result => { + return { + reported_size: Utils.formatBytes(result.data.reportedSizeInBytes), + estimated_size: Utils.formatBytes(result.data.estimatedSizeInBytes), + }; + }) +} + +const getSegmentCountAndStatus = (tableName: string) => { + return getIdealState(tableName).then(result => { + const idealState = result.data.OFFLINE || result.data.REALTIME || {}; + return getExternalView(tableName).then(result => { + const externalView = result.data.OFFLINE || result.data.REALTIME || {}; + const externalSegmentCount = Object.keys(externalView).length; + const idealSegmentCount = Object.keys(idealState).length; + return { + segment_count: `${externalSegmentCount} / ${idealSegmentCount}`, + segment_status: Utils.getSegmentStatus(idealState, externalView) + }; + }); + }); +} + const getAllTableDetails = (tablesList) => { if (tablesList.length) { const promiseArr = []; @@ -464,10 +487,10 @@ const getAllTableDetails = (tablesList) => { }; }); } - return { + return Promise.resolve({ columns: allTableDetailsColumnHeader, records: [] - }; + }); }; // This method is used to display summary of a particular tenant table @@ -556,7 +579,7 @@ const getSegmentDetails = (tableName, segmentName) => { return Promise.all(promiseArr).then((results) => { const obj = results[0].data.OFFLINE || results[0].data.REALTIME; - const segmentMetaData = results[1].data; + const segmentMetaData: SegmentMetadata = results[1].data; const debugObj = results[2].data; let debugInfoObj = {}; @@ -569,7 +592,6 @@ const getSegmentDetails = (tableName, segmentName) => { }); } } - console.log(debugInfoObj); const result = []; for (const prop in obj[segmentName]) { @@ -719,13 +741,13 @@ const deleteNode = (path) => { }); }; -const getBrokerOfTenant = (tenantName) => { +const getBrokerOfTenant = (tenantName: string) => { return getBrokerListOfTenant(tenantName).then((response)=>{ return !response.data.error ? response.data : []; }); }; -const getServerOfTenant = (tenantName) => { +const getServerOfTenant = (tenantName: string) => { return getServerListOfTenant(tenantName).then((response)=>{ return !response.data.error ? response.data.ServerInstances : []; }); @@ -1227,6 +1249,7 @@ export default { getSegmentStatus, getTableDetails, getSegmentDetails, + getSegmentCountAndStatus, getClusterName, getLiveInstance, getLiveInstanceConfig, @@ -1249,6 +1272,7 @@ export default { fetchSegmentReloadStatus, getTaskTypeDebugData, getTableData, + getTableSizes, getTaskRuntimeConfigData, getTaskInfo, stopAllTasks, diff --git a/pinot-controller/src/main/resources/app/utils/Utils.tsx b/pinot-controller/src/main/resources/app/utils/Utils.tsx index 623e1ffe678e..c0983230e1a2 100644 --- a/pinot-controller/src/main/resources/app/utils/Utils.tsx +++ b/pinot-controller/src/main/resources/app/utils/Utils.tsx @@ -22,7 +22,14 @@ import React from 'react'; import ReactDiffViewer, {DiffMethod} from 'react-diff-viewer'; import { map, isEqual, findIndex, findLast } from 'lodash'; import app_state from '../app_state'; -import {DISPLAY_SEGMENT_STATUS, SEGMENT_STATUS, TableData} from 'Models'; +import { + DISPLAY_SEGMENT_STATUS, InstanceType, + PinotTableDetails, + SEGMENT_STATUS, + SegmentStatus, + TableData, +} from 'Models'; +import Loading from '../components/Loading'; const sortArray = function (sortingArr, keyName, ascendingFlag) { if (ascendingFlag) { @@ -47,6 +54,26 @@ const sortArray = function (sortingArr, keyName, ascendingFlag) { }); }; +const pinotTableDetailsFormat = (tableDetails: PinotTableDetails): Array => { + return [ + tableDetails.name, + tableDetails.estimated_size || Loading, + tableDetails.reported_size || Loading, + tableDetails.number_of_segments || Loading, + tableDetails.segment_status || Loading + ]; +} + +const pinotTableDetailsFromArray = (tableDetails: Array): PinotTableDetails => { + return { + name: tableDetails[0] as string, + estimated_size: tableDetails[1] as string, + reported_size: tableDetails[2] as string, + number_of_segments: tableDetails[3] as string, + segment_status: tableDetails[4] as any + }; +} + const tableFormat = (data: TableData): Array<{ [key: string]: any }> => { const rows = data.records; const header = data.columns; @@ -321,7 +348,7 @@ const encodeString = (str: string) => { return str; } -const formatBytes = (bytes, decimals = 2) => { +const formatBytes = (bytes: number, decimals = 2) => { if (bytes === 0) return '0 Bytes'; const k = 1024; @@ -385,6 +412,43 @@ export const getDisplaySegmentStatus = (idealState, externalView): DISPLAY_SEGME return DISPLAY_SEGMENT_STATUS.UPDATING; } +export const getInstanceTypeFromInstanceName = (instanceName: string): InstanceType => { + if (instanceName.toLowerCase().startsWith(InstanceType.BROKER.toLowerCase())) { + return InstanceType.BROKER; + } else if (instanceName.toLowerCase().startsWith(InstanceType.CONTROLLER.toLowerCase())) { + return InstanceType.CONTROLLER; + } else if (instanceName.toLowerCase().startsWith(InstanceType.MINION.toLowerCase())) { + return InstanceType.MINION; + } else if (instanceName.toLowerCase().startsWith(InstanceType.SERVER.toLowerCase())) { + return InstanceType.SERVER; + } else { + return null; + } +} + +export const getInstanceTypeFromString = (instanceType: string): InstanceType => { + if (instanceType.toLowerCase() === InstanceType.BROKER.toLowerCase()) { + return InstanceType.BROKER; + } else if (instanceType.toLowerCase() === InstanceType.CONTROLLER.toLowerCase()) { + return InstanceType.CONTROLLER; + } else if (instanceType.toLowerCase() === InstanceType.MINION.toLowerCase()) { + return InstanceType.MINION; + } else if (instanceType.toLowerCase() === InstanceType.SERVER.toLowerCase()) { + return InstanceType.SERVER; + } else { + return null; + } +} + +const getLoadingTableData = (columns: string[]): TableData => { + return { + columns: columns, + records: [ + columns.map((_) => Loading), + ], + }; +} + export default { sortArray, tableFormat, @@ -396,5 +460,8 @@ export default { syncTableSchemaData, encodeString, formatBytes, - splitStringByLastUnderscore + splitStringByLastUnderscore, + pinotTableDetailsFormat, + pinotTableDetailsFromArray, + getLoadingTableData };