This is an automated email from the ASF dual-hosted git repository. xbli pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push: new 23ae24c0c6 Feature: New UI to monitor Rebalance Server Status (#15400) 23ae24c0c6 is described below commit 23ae24c0c6db266e44dbaa79ddf76ada105c86c6 Author: Soumya Himanish Mohapatra <30361728+himanish-s...@users.noreply.github.com> AuthorDate: Fri Apr 4 08:31:41 2025 +0530 Feature: New UI to monitor Rebalance Server Status (#15400) * add pre checks and job summary section * display rebalance status * add loader and error handling --- .../main/resources/app/components/CustomDialog.tsx | 4 +- .../Operations/RebalanceServerStatusOp.tsx | 181 +++++++++++++++++++++ .../Homepage/Operations/RebalanceServerTableOp.tsx | 55 ++++++- .../src/main/resources/app/components/Table.tsx | 4 +- .../src/main/resources/app/pages/TenantDetails.tsx | 20 +++ .../src/main/resources/app/utils/Utils.tsx | 8 +- 6 files changed, 260 insertions(+), 12 deletions(-) diff --git a/pinot-controller/src/main/resources/app/components/CustomDialog.tsx b/pinot-controller/src/main/resources/app/components/CustomDialog.tsx index 0e5fcc6fb4..ca3b659c40 100644 --- a/pinot-controller/src/main/resources/app/components/CustomDialog.tsx +++ b/pinot-controller/src/main/resources/app/components/CustomDialog.tsx @@ -64,6 +64,7 @@ type Props = { btnOkText?: string, showCancelBtn?: boolean, showOkBtn?: boolean, + okBtnDisabled?: boolean, size?: false | "xs" | "sm" | "md" | "lg" | "xl", disableBackdropClick?: boolean, disableEscapeKeyDown?: boolean, @@ -82,6 +83,7 @@ export default function CustomDialog({ btnOkText, showCancelBtn = true, showOkBtn = true, + okBtnDisabled, size, disableBackdropClick = false, disableEscapeKeyDown = false, @@ -116,7 +118,7 @@ export default function CustomDialog({ </CancelButton>} {moreActions} {showOkBtn && - <Button onClick={handleSave} variant="contained" style={{ textTransform: 'none' }} color="primary"> + <Button disabled={okBtnDisabled} onClick={handleSave} variant="contained" style={{ textTransform: 'none' }} color="primary"> {btnOkText || 'Save'} </Button>} </DialogActions> diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/RebalanceServerStatusOp.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/RebalanceServerStatusOp.tsx new file mode 100644 index 0000000000..d5a8cc910e --- /dev/null +++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/RebalanceServerStatusOp.tsx @@ -0,0 +1,181 @@ +/** + * 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 { + Box, Button, CircularProgress, DialogContent, Grid +} from "@material-ui/core"; +import Dialog from "../../CustomDialog"; +import React, {useEffect, useState} from "react"; +import {RebalanceServerSection} from "./RebalanceServer/RebalanceServerSection"; +import CustomCodemirror from "../../CustomCodemirror"; +import './RebalanceServer/RebalanceServerResponses/CustomCodeMirror.css'; +import {RebalanceServerResponseCard} from "./RebalanceServer/RebalanceServerResponses/RebalanceServerResponseCard"; +import CustomizedTables from "../../Table"; +import Utils from "../../../utils/Utils"; +import PinotMethodUtils from "../../../utils/PinotMethodUtils"; + +export type RebalanceTableSegmentJobs = { + [key: string]: { + jobId: string, + messageCount: number, + submissionTimeMs: number, + jobType: string, + tableName: string, + REBALANCE_PROGRESS_STATS: string, + REBALANCE_CONTEXT: string; + } +} + +type RebalanceServerStatusOpProps = { + tableName: string; + hideModal: () => void; +}; + +export const RebalanceServerStatusOp = ( + { tableName, hideModal } : RebalanceServerStatusOpProps +) => { + const [rebalanceServerJobs, setRebalanceServerJobs] = React.useState<RebalanceTableSegmentJobs>({}) + const [jobSelected, setJobSelected] = useState<string | null>(null); + const [rebalanceContext, setRebalanceContext] = useState<{}>({}); + const [rebalanceProgressStats, setRebalanceProgressStats] = useState<{}>({}); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setLoading(true); + PinotMethodUtils + .fetchTableJobs(tableName, "TABLE_REBALANCE") + .then(jobs => { + if (jobs.error) { + return; + } + setRebalanceServerJobs(jobs as RebalanceTableSegmentJobs) + }) + .finally(() => setLoading(false)); + }, []); + + const BackAction = () => { + return ( + <Button + variant='outlined' + color='primary' + onClick={() => setJobSelected(null)} + > + Back + </Button> + ); + } + + useEffect(() => { + try { + if (jobSelected !== null) { + setRebalanceContext(JSON.parse(rebalanceServerJobs[jobSelected].REBALANCE_CONTEXT)) + setRebalanceProgressStats(JSON.parse(rebalanceServerJobs[jobSelected].REBALANCE_PROGRESS_STATS)) + } + } catch (e) { + setRebalanceContext( + { + message: 'Failed to load rebalance context' + }); + setRebalanceProgressStats( + { + message: 'Failed to load rebalance progress stats' + }); + } + }, [jobSelected]); + + if (loading) { + return ( + <Dialog + open={true} + handleClose={hideModal} + title="Rebalance Table Status" + showOkBtn={false} + size='lg' + moreActions={jobSelected ? <BackAction /> : null} + > + <DialogContent> + <Box alignItems='center' display='flex' justifyContent='center'> + <CircularProgress /> + </Box> + </DialogContent> + </Dialog> + ) + } + + return ( + <Dialog + open={true} + handleClose={hideModal} + title="Rebalance Table Status" + showOkBtn={false} + size='lg' + moreActions={jobSelected ? <BackAction /> : null} + > + <DialogContent> + { + !jobSelected ? + <CustomizedTables + title='Job Status' + isCellClickable + makeOnlyFirstCellClickable + cellClickCallback={(cell: string) => { + setJobSelected(cell); + }} + data={{ + records: Object.keys(rebalanceServerJobs).map(jobId => { + const progressStats = JSON.parse(rebalanceServerJobs[jobId].REBALANCE_PROGRESS_STATS); + return [ + rebalanceServerJobs[jobId].jobId, + rebalanceServerJobs[jobId].tableName, + progressStats.status, + Utils.formatTime(+rebalanceServerJobs[jobId].submissionTimeMs) + ]; + }), + columns: ['Job id', 'Table name', 'Status', 'Started at'] + }} + showSearchBox + /> : + <Grid container spacing={2}> + <Grid item xs={12}> + <RebalanceServerResponseCard> + <RebalanceServerSection sectionTitle={"Progress Stats"} canHideSection showSectionByDefault={true}> + <CustomCodemirror + customClass='rebalance_server_response_section' + data={rebalanceProgressStats} + isEditable={false} + /> + </RebalanceServerSection> + </RebalanceServerResponseCard> + </Grid> + <Grid item xs={12}> + <RebalanceServerResponseCard> + <RebalanceServerSection sectionTitle={"Context"} canHideSection showSectionByDefault={true}> + <CustomCodemirror + customClass='rebalance_server_response_section' + data={rebalanceContext} + isEditable={false} + /> + </RebalanceServerSection> + </RebalanceServerResponseCard> + </Grid> + </Grid> + } + </DialogContent> + </Dialog> + ); +} \ No newline at end of file diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/RebalanceServerTableOp.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/RebalanceServerTableOp.tsx index 3078776ed8..5fb17fd4eb 100644 --- a/pinot-controller/src/main/resources/app/components/Homepage/Operations/RebalanceServerTableOp.tsx +++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/RebalanceServerTableOp.tsx @@ -19,10 +19,7 @@ import React from 'react'; import { - Grid, - Box, - Typography, - Divider, Button + Grid, Box, Typography, Divider, Button, CircularProgress } from '@material-ui/core'; import Dialog from '../../CustomDialog'; import PinotMethodUtils from '../../../utils/PinotMethodUtils'; @@ -42,9 +39,9 @@ type Props = { hideModal: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void }; -const DryRunAction = ({ handleOnRun }: { handleOnRun: () => void }) => { +const DryRunAction = ({ handleOnRun, disabled }: { handleOnRun: () => void, disabled?: boolean }) => { return ( - <Button onClick={handleOnRun} variant="outlined" style={{ textTransform: 'none' }} color="primary"> + <Button disabled={disabled} onClick={handleOnRun} variant="outlined" style={{ textTransform: 'none' }} color="primary"> Dry Run </Button> ); @@ -55,6 +52,7 @@ export default function RebalanceServerTableOp({ tableName, tableType }: Props) { + const [pending, setPending] = React.useState(false); const [rebalanceResponse, setRebalanceResponse] = React.useState(null) const [rebalanceConfig, setRebalanceConfig] = React.useState( rebalanceServerOptions.reduce((config, option) => ({ ...config, [option.name]: option.defaultValue }), {}) @@ -69,18 +67,39 @@ export default function RebalanceServerTableOp({ const handleSave = async () => { const data = getData(); + setPending(true); const response = await PinotMethodUtils.rebalanceServersForTableOp(tableName, data); - setRebalanceResponse(response); + + if (response.error) { + setRebalanceResponse({ + description: response.error, + jobId: "NA", + status: response.code + }) + } else { + setRebalanceResponse(response); + } + setPending(false); }; const handleDryRun = async () => { const data = getData(); + setPending(true); const response = await PinotMethodUtils.rebalanceServersForTableOp(tableName, { ...data, dryRun: true, preChecks: true }); - setRebalanceResponse(response); + if (response.error) { + setRebalanceResponse({ + description: response.error, + jobId: "NA", + status: response.code + }) + } else { + setRebalanceResponse(response); + } + setPending(false); }; const handleConfigChange = (config: { [key: string]: string | number | boolean }) => { @@ -90,19 +109,37 @@ export default function RebalanceServerTableOp({ }); } + if (pending) { + return ( + <Dialog + showTitleDivider + showFooterDivider + size='md' + open={true} + handleClose={hideModal} + title={<RebalanceServerDialogHeader />} + showOkBtn={false} + > + <Box alignItems='center' display='flex' justifyContent='center'> + <CircularProgress /> + </Box> + </Dialog> + ) + } return ( <Dialog showTitleDivider showFooterDivider size='md' + okBtnDisabled={pending} open={true} handleClose={hideModal} title={<RebalanceServerDialogHeader />} handleSave={handleSave} btnOkText='Rebalance' showOkBtn={!rebalanceResponse} - moreActions={!rebalanceResponse ? <DryRunAction handleOnRun={handleDryRun} /> : null} + moreActions={!rebalanceResponse ? <DryRunAction disabled={pending} handleOnRun={handleDryRun} /> : null} > {!rebalanceResponse ? <Box flexDirection="column"> diff --git a/pinot-controller/src/main/resources/app/components/Table.tsx b/pinot-controller/src/main/resources/app/components/Table.tsx index 69d67cdcb4..7b866f38b7 100644 --- a/pinot-controller/src/main/resources/app/components/Table.tsx +++ b/pinot-controller/src/main/resources/app/components/Table.tsx @@ -59,6 +59,7 @@ type Props = { addLinks?: boolean, cellClickCallback?: Function, isCellClickable?: boolean, + makeOnlyFirstCellClickable?: boolean, highlightBackground?: boolean, isSticky?: boolean, baseURL?: string, @@ -271,6 +272,7 @@ export default function CustomizedTables({ addLinks, cellClickCallback, isCellClickable, + makeOnlyFirstCellClickable, highlightBackground, isSticky, baseURL, @@ -562,7 +564,7 @@ export default function CustomizedTables({ ) : ( <StyledTableCell key={idx} - className={isCellClickable ? classes.isCellClickable : (isSticky ? classes.isSticky : '')} + className={isCellClickable && (!makeOnlyFirstCellClickable || !idx) ? classes.isCellClickable : (isSticky ? classes.isSticky : '')} onClick={() => {cellClickCallback && cellClickCallback(cell);}} > {makeCell(cell ?? '--', index)} diff --git a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx index a34018b6fd..acb72833a2 100644 --- a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx +++ b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx @@ -44,6 +44,9 @@ import { get, isEmpty } from "lodash"; import { SegmentStatusRenderer } from '../components/SegmentStatusRenderer'; import Skeleton from '@material-ui/lab/Skeleton'; import NotFound from '../components/NotFound'; +import { + RebalanceServerStatusOp, RebalanceTableSegmentJobs +} from "../components/Homepage/Operations/RebalanceServerStatusOp"; const useStyles = makeStyles((theme) => ({ root: { @@ -151,6 +154,7 @@ const TenantPageDetails = ({ match }: RouteComponentProps<Props>) => { const [tableJobsData, setTableJobsData] = useState<TableSegmentJobs | null>(null); const [showRebalanceServerModal, setShowRebalanceServerModal] = useState(false); const [schemaJSONFormat, setSchemaJSONFormat] = useState(false); + const [showRebalanceServerStatus, setShowRebalanceServerStatus] = useState(false); // This is quite hacky, but it's the only way to get this to work with the dialog. // The useState variables are simply for the dialog box to know what to render in @@ -445,6 +449,10 @@ const TenantPageDetails = ({ match }: RouteComponentProps<Props>) => { } }; + const handleRebalanceTableStatus = () => { + setShowRebalanceServerStatus(true); + }; + const handleRebalanceBrokers = () => { setDialogDetails({ title: (<>Rebalance brokers <Tooltip interactive title={(<a className={"tooltip-link"} target="_blank" href="https://docs.pinot.apache.org/operators/operating-pinot/rebalance/rebalance-brokers">Click here for more details</a>)} arrow placement="top"><InfoOutlinedIcon/></Tooltip></>), @@ -536,6 +544,13 @@ const TenantPageDetails = ({ match }: RouteComponentProps<Props>) => { > Rebalance Servers </CustomButton> + <CustomButton + onClick={handleRebalanceTableStatus} + tooltipTitle="The status of table rebalance job" + enableTooltip={true} + > + Rebalance Servers Status + </CustomButton> <CustomButton onClick={handleRebalanceBrokers} tooltipTitle="Rebuilds brokerResource mapping for this table" @@ -686,6 +701,11 @@ const TenantPageDetails = ({ match }: RouteComponentProps<Props>) => { tableJobsData={tableJobsData} /> )} + {showRebalanceServerStatus && ( + <RebalanceServerStatusOp + hideModal={() => setShowRebalanceServerStatus(false)} + tableName={tableName} /> + )} {showRebalanceServerModal && ( <RebalanceServerTableOp hideModal={() => { diff --git a/pinot-controller/src/main/resources/app/utils/Utils.tsx b/pinot-controller/src/main/resources/app/utils/Utils.tsx index bc4d836877..30904765fd 100644 --- a/pinot-controller/src/main/resources/app/utils/Utils.tsx +++ b/pinot-controller/src/main/resources/app/utils/Utils.tsx @@ -31,6 +31,7 @@ import { TableData, } from 'Models'; import Loading from '../components/Loading'; +import moment from "moment"; const sortArray = function (sortingArr, keyName, ascendingFlag) { if (ascendingFlag) { @@ -463,6 +464,10 @@ const getLoadingTableData = (columns: string[]): TableData => { }; } +const formatTime = (time: number, format?: string): string => { + return moment(time).format(format ?? "MMMM Do YYYY, HH:mm:ss") +} + export default { sortArray, tableFormat, @@ -477,5 +482,6 @@ export default { splitStringByLastUnderscore, pinotTableDetailsFormat, pinotTableDetailsFromArray, - getLoadingTableData + getLoadingTableData, + formatTime }; --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@pinot.apache.org For additional commands, e-mail: commits-h...@pinot.apache.org