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
<[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]