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

Reply via email to