This is an automated email from the ASF dual-hosted git repository.

arshad pushed a commit to branch frontend-refactor
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/frontend-refactor by this push:
     new fcf4c1a0cc AMBARI-26564 :: Ambari Web React: Fix issues with 
OperationProgress component (#4096)
fcf4c1a0cc is described below

commit fcf4c1a0cc0a60ee74dcaf3eb3f1934a84b2a48c
Author: Himanshu Maurya <[email protected]>
AuthorDate: Fri Dec 19 16:15:29 2025 +0530

    AMBARI-26564 :: Ambari Web React: Fix issues with OperationProgress 
component (#4096)
---
 ambari-web/latest/src/Utils/statusIcons.tsx        |  96 ++++
 .../latest/src/components/OperationProgress.tsx    | 506 ++++++++++++---------
 ambari-web/latest/src/constants.ts                 |   7 +-
 3 files changed, 381 insertions(+), 228 deletions(-)

diff --git a/ambari-web/latest/src/Utils/statusIcons.tsx 
b/ambari-web/latest/src/Utils/statusIcons.tsx
new file mode 100644
index 0000000000..6a71818be5
--- /dev/null
+++ b/ambari-web/latest/src/Utils/statusIcons.tsx
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from "react";
+import {
+  faCheck,
+  faClock,
+  faCog,
+  faCogs,
+  faExclamation,
+  faMinus,
+  faTimes,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import classNames from "classnames";
+
+type RequestStatus =
+  | "INIT"
+  | "PENDING"
+  | "QUEUED"
+  | "IN_PROGRESS"
+  | "COMPLETED"
+  | "FAILED"
+  | "HOLDING_FAILED"
+  | "SKIPPED_FAILED"
+  | "HOLDING"
+  | "SUSPENDED"
+  | "ABORTED"
+  | "TIMEDOUT"
+  | "HOLDING_TIMEDOUT"
+  | "SUBITEM_FAILED"
+  | "WARNING";
+
+type StatusIconConfig = {
+  icon: any;
+  color: string;
+  shouldShowOpacity?: boolean;
+};
+
+const STATUS_ICON_MAP: Record<RequestStatus, StatusIconConfig> = {
+  INIT: { icon: faCogs, color: "blue" },
+  PENDING: { icon: faCog, color: "gray", shouldShowOpacity: true },
+  QUEUED: { icon: faCog, color: "gray" },
+  IN_PROGRESS: { icon: faCogs, color: "blue" },
+  COMPLETED: { icon: faCheck, color: "green" },
+  FAILED: { icon: faExclamation, color: "red" },
+  HOLDING_FAILED: { icon: faExclamation, color: "red" },
+  SKIPPED_FAILED: { icon: faTimes, color: "red" },
+  HOLDING: { icon: faClock, color: "orange" },
+  SUSPENDED: { icon: faClock, color: "orange" },
+  ABORTED: { icon: faMinus, color: "orange" },
+  TIMEDOUT: { icon: faClock, color: "orange" },
+  HOLDING_TIMEDOUT: { icon: faClock, color: "orange" },
+  SUBITEM_FAILED: { icon: faTimes, color: "red" },
+  WARNING: { icon: faExclamation, color: "yellow" },
+};
+
+const DEFAULT_STATUS_CONFIG: StatusIconConfig = {
+  icon: faCog,
+  color: "blue",
+  shouldShowOpacity: true,
+};
+
+export const getStatusIcon = (
+  requestStatus: string | undefined
+): React.ReactElement => {
+  const config =
+    requestStatus && requestStatus in STATUS_ICON_MAP
+      ? STATUS_ICON_MAP[requestStatus as RequestStatus]
+      : DEFAULT_STATUS_CONFIG;
+
+  const { icon, color, shouldShowOpacity = false } = config;
+
+  return (
+    <FontAwesomeIcon
+      icon={icon}
+      color={color}
+      className={classNames("me-2", { "opacity-50": shouldShowOpacity })}
+    />
+  );
+};
\ No newline at end of file
diff --git a/ambari-web/latest/src/components/OperationProgress.tsx 
b/ambari-web/latest/src/components/OperationProgress.tsx
index 3084abd49a..344583c774 100644
--- a/ambari-web/latest/src/components/OperationProgress.tsx
+++ b/ambari-web/latest/src/components/OperationProgress.tsx
@@ -15,22 +15,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { cloneDeep, filter, findIndex, get, has, set } from "lodash";
+
+import { cloneDeep, get, has, set } from "lodash";
 import { useContext, useEffect, useRef, useState } from "react";
-import usePolling from "../hooks/usePolling";
 import { RequestApi } from "../api/requestApi";
 import { AppContext } from "../store/context";
 import { isFailed, isFinished } from "../Utils/Utility";
-import { ProgressStatus} from "../constants";
+import { ProgressStatus } from "../constants";
 import { Alert, Button, ProgressBar, Stack } from "react-bootstrap";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
-  faCircleCheck,
-  faTimes,
-  faUndo,
-} from "@fortawesome/free-solid-svg-icons";
+import { faUndo } from "@fortawesome/free-solid-svg-icons";
+//TODO: uncomment below code when background operations modal is implemented
 // import modalManager from "../store/ModalManager";
 // import BackgroundOperations from "../screens/BackgroundOperations";
+import { getStatusIcon } from "../Utils/statusIcons";
 
 type PropTypes = {
   title: string;
@@ -42,6 +40,10 @@ type PropTypes = {
       label: "string";
       callback: any;
       skippable: boolean;
+      requestId?: string | number;
+      status?: string;
+      progress?: number;
+      error?: string;
     }
   ];
   dispatch?: (operationsState: any) => void;
@@ -54,174 +56,326 @@ type OperationRequestResponse = {
     status: string;
   };
   href: string;
-  status?:number;
+  status?: number;
 };
 
 function OperationsProgress({
-  // title,
-  // description,
   setCompletionStatus,
   operations,
   dispatch,
-  errorCallback
+  errorCallback,
 }: PropTypes) {
   const [operationsState, setOperationsState] = useState(operations);
-  const operationsRef = useRef(operations);
-  const activeOperationId = useRef<any>(null);
+  const [activeOperationId, setActiveOperationId] = useState(-1);
   const { clusterName } = useContext(AppContext);
-  const { stopPolling, pausePolling, resumePolling } = usePolling(
-    trackCurrentRequestStatus
-  );
-  const activeRequestId = useRef<string|number>(0);
   const startedTasks: any = useRef([]);
-  async function trackCurrentRequestStatus() {
-    const operationsStateCopy = cloneDeep(operationsRef.current);
+
+  const trackCurrentRequestStatus = async (
+    requestId: number,
+    operationId: number
+  ) => {
+    const operationsStateCopy = cloneDeep(operationsState);
     const trackingStatusForOperation: any = operationsStateCopy.find(
-      (operation) => operation.id == activeOperationId.current
+      (operation) => operation.id == operationId
     );
-    if (activeRequestId.current) {
-      const activeRequestStatus = await RequestApi.getRequestStatus(
+    if (requestId) {
+      set(trackingStatusForOperation, "requestId", requestId);
+      const requestStatus = await RequestApi.getRequestStatus(
         clusterName,
-        activeRequestId.current as any
+        String(requestId)
       );
-      const { Requests } = activeRequestStatus;
-      if (activeRequestStatus?.Requests?.request_status) {
-        set(
-          trackingStatusForOperation,
-          "requestId",
-          activeRequestStatus?.Requests?.id
-        );
-        set(
-          trackingStatusForOperation,
-          "status",
-          activeRequestStatus?.Requests?.request_status||"FAILED"
-        );
-        set(
-          trackingStatusForOperation,
-          "progress",
-          activeRequestStatus?.Requests?.progress_percent
-        );
-        set(
-          trackingStatusForOperation,
-          "requestInfo",
-          activeRequestStatus?.Requests
-        );
-        const requestStages = filter(
-          activeRequestStatus.stages,
-          function (stage) {
-            return has(stage, "Stage.context");
-          }
-        );
-        set(trackCurrentRequestStatus, "stages", requestStages);
+      if (requestStatus?.Requests?.request_status) {
+        const { Requests } = requestStatus;
+        set(trackingStatusForOperation, "status", Requests?.request_status);
+        set(trackingStatusForOperation, "progress", 
Requests?.progress_percent);
+        set(trackingStatusForOperation, "requestInfo", Requests);
+        if (has(trackingStatusForOperation, "error")) {
+          delete trackingStatusForOperation.error;
+        }
+
+
         setOperationsState(operationsStateCopy);
-        operationsRef.current = operationsStateCopy;
-        console.log("Current request status is", Requests.request_status);
-        if (isFinished(Requests.request_status)) {
-          console.log("Operation Progress operation finished");
-          if (Requests.request_status === ProgressStatus.FAILED) {
-            pausePolling();
-          } else {
-            if (activeOperationId.current == (operations as any)?.at(-1)?.id) {
-              stopPolling();
-              setCompletionStatus(true);
-            } else {
-              const currentActiveIndex = findIndex(operations, [
-                "id",
-                Number(activeOperationId?.current),
-              ]);
-              executeTask(operationsRef.current[Number(currentActiveIndex) + 
1]?.id);
-            }
-          }
+
+        if (!isFinished(Requests?.request_status)) {
+          setTimeout(() => {
+            trackCurrentRequestStatus(requestId, operationId);
+          }, 2000);
         }
       }
     }
-  }
-  async function executeTask(id: string | number) {
-    id = Number(id);
-    if (!startedTasks.current.includes(id)) {
-      activeOperationId.current = id;
-      startedTasks.current.push(id);
-      const operationsStateCopy = cloneDeep(operationsRef.current);
+  };
+
+  async function executeTask() {
+    if (!startedTasks.current.includes(activeOperationId)) {
+      startedTasks.current.push(activeOperationId);
+      const operationsStateCopy = cloneDeep(operationsState);
       const matchingOperation: any = operationsStateCopy.find(
-        (operation) => operation.id == id
+        (operation) => operation.id == activeOperationId
       );
+
+      if (
+        matchingOperation &&
+        matchingOperation?.status === ProgressStatus.IN_PROGRESS
+      ) {
+        trackCurrentRequestStatus(
+          matchingOperation.requestId,
+          activeOperationId
+        );
+        return;
+      }
+
       if (matchingOperation) {
         try {
-        const operationCallbackResponse:OperationRequestResponse = await 
matchingOperation?.callback();
-        if (operationCallbackResponse?.Requests) {
-          matchingOperation.requestId = 
operationCallbackResponse?.Requests?.id;
-          activeRequestId.current = operationCallbackResponse?.Requests?.id;
-        }
-        //TODO: @vhassija Please verify for all statusCode
-        else if(operationCallbackResponse?.status === 200|| 
!operationCallbackResponse){
-          if (activeOperationId.current == (operationsRef.current as 
any)?.at(-1)?.id) {
-            stopPolling();
-            setCompletionStatus(true);
+          const operationCallbackResponse: OperationRequestResponse =
+            await matchingOperation?.callback();
+
+          if (operationCallbackResponse?.Requests) {
+            set(
+              matchingOperation,
+              "requestId",
+              operationCallbackResponse?.Requests?.id
+            );
+            trackCurrentRequestStatus(
+              matchingOperation.requestId,
+              activeOperationId
+            );
           } else {
-            const currentActiveIndex = 
operationsRef.current.findIndex((operation) => operation.id == 
activeOperationId.current);
-            executeTask(operationsRef.current[Number(currentActiveIndex) + 
1]?.id);
+            const statusCode = get(operationCallbackResponse, "[0].status", 
operationCallbackResponse?.status);
+
+            // Handle status codes by ranges in case of success or unknown 
response
+            if (
+              (statusCode && statusCode >= 200 && statusCode < 300) ||
+              !operationCallbackResponse
+            ) {
+              // 2xx Success status codes or empty response with no status 
code - treat as success
+              set(matchingOperation, "status", ProgressStatus.COMPLETED);
+              if (has(matchingOperation, "error")) {
+                delete matchingOperation.error;
+              }
+            } else {
+              // Unknown status code or response exists but no status code
+              set(matchingOperation, "status", ProgressStatus.FAILED);
+              if (statusCode) {
+                set(
+                  matchingOperation,
+                  "error",
+                  JSON.stringify(operationCallbackResponse) ||
+                    `Unknown Status Code (${statusCode}): ${JSON.stringify(
+                      operationCallbackResponse
+                    )}`
+                );
+              } else {
+                set(
+                  matchingOperation,
+                  "error",
+                  JSON.stringify(operationCallbackResponse) ||
+                    `Unknown response format: ${JSON.stringify(
+                      operationCallbackResponse
+                    )}`
+                );
+              }
+            }
+            setOperationsState(operationsStateCopy);
           }
+        } catch (error: any) {
+          const statusCode = get(error, "status", "");
+          const errorMessage = get(error, "response.data.message", "");
+          // Handle status codes by ranges in case of error
+          if (statusCode && statusCode >= 300 && statusCode < 400) {
+            // 3xx Redirection status codes - treat as error for operations
+            set(matchingOperation, "status", ProgressStatus.FAILED);
+            set(
+              matchingOperation,
+              "error",
+              JSON.stringify(errorMessage) ||
+                `Redirection Error (${statusCode}): Operation requires manual 
intervention`
+            );
+          } else if (statusCode && statusCode >= 400 && statusCode < 500) {
+            // 4xx Client error status codes
+            set(matchingOperation, "status", ProgressStatus.FAILED);
+            set(
+              matchingOperation,
+              "error",
+              JSON.stringify(errorMessage) ||
+                `Client Error (${statusCode}): Please check the request 
parameters`
+            );
+          } else if (statusCode && statusCode >= 500 && statusCode < 600) {
+            // 5xx Server error status codes
+            set(matchingOperation, "status", ProgressStatus.FAILED);
+            set(
+              matchingOperation,
+              "error",
+              JSON.stringify(errorMessage) ||
+                `Server Error (${statusCode}): Please try again later or 
contact support`
+            );
+          } else {
+            // Unknown status code or response exists but no status code
+            set(matchingOperation, "status", ProgressStatus.FAILED);
+            if (statusCode) {
+              set(
+                matchingOperation,
+                "error",
+                JSON.stringify(errorMessage) ||
+                  `Unknown Status Code (${statusCode}): ${JSON.stringify(
+                    errorMessage
+                  )}`
+              );
+            } else {
+              set(
+                matchingOperation,
+                "error",
+                JSON.stringify(errorMessage) ||
+                  `Unknown response format: ${JSON.stringify(errorMessage)}`
+              );
+            }
+          }
+          setOperationsState(operationsStateCopy);
         }
-        else{
-          console.error("Operation failed with response", 
operationCallbackResponse);
-          matchingOperation.status = "FAILED";
-        }
-        }
-      catch(err){
-        console.error("Got request", err)
-        matchingOperation.status = "FAILED";
-
       }
-      }
-      setOperationsState(operationsStateCopy);
-      operationsRef.current = operationsStateCopy;
     }
   }
+
+  const handleMoveToNextOperation = () => {
+    const currentActiveOperation = operationsState.find(
+      (operation) => operation.id == activeOperationId
+    );
+    if (
+      currentActiveOperation &&
+      isFinished(currentActiveOperation?.status || "")
+    ) {
+      if (currentActiveOperation?.status !== ProgressStatus.FAILED) {
+        if (activeOperationId == operationsState?.[operationsState?.length - 
1]?.id) {
+          setCompletionStatus(true);
+        } else {
+          const currentActiveIndex = operationsState.findIndex(
+            (operation) => operation.id == activeOperationId
+          );
+          setActiveOperationId(
+            Number(operationsState[Number(currentActiveIndex) + 1]?.id)
+          );
+        }
+      }
+    }
+  };
+
   const retryOperation = () => {
-    startedTasks.current = startedTasks.current.filter((task:any) => {
-      task != activeOperationId.current;
+    startedTasks.current = startedTasks.current.filter((task: any) => {
+      task != activeOperationId;
     });
-    executeTask(activeOperationId.current as any);
-    resumePolling();
+    executeTask();
   };
-  const renderStagesForOperation = (operation: any) => {
-    return (
+
+  useEffect(() => {
+    if (activeOperationId >= 0) {
+      executeTask();
+    }
+  }, [activeOperationId]);
+
+  useEffect(() => {
+    if (dispatch) {
+      dispatch(operationsState);
+    }
+    handleMoveToNextOperation();
+  }, [JSON.stringify(operationsState)]);
+
+  useEffect(() => {
+    let activeIdx = -1;
+    let toBeStartedIdx = -1;
+    for (let i = operationsState.length - 1; i >= 0; i--) {
+      if (
+        get(operationsState[i], "requestId", "") ||
+        get(operationsState[i], "status", "")
+      ) {
+        activeIdx = i;
+        break;
+      }
+    }
+
+    if (activeIdx === -1) {
+      toBeStartedIdx = 0;
+    } else {
+      for (let i = 0; i <= activeIdx; i++) {
+        if (
+          get(operationsState[i], "requestId", "") ||
+          get(operationsState[i], "status", "")
+        ) {
+          startedTasks.current.push(operationsState[i].id);
+          if (isFinished(operationsState[i]?.status || "")) {
+            if (operationsState[i]?.status === ProgressStatus.FAILED) {
+              toBeStartedIdx = i;
+            } else {
+              toBeStartedIdx = i + 1;
+            }
+          } else {
+            toBeStartedIdx = i;
+          }
+        }
+      }
+    }
+    setActiveOperationId(Number(operationsState?.[toBeStartedIdx]?.id));
+    if(operationsState?.[toBeStartedIdx]?.requestId){
+      trackCurrentRequestStatus(operationsState?.[toBeStartedIdx]?.requestId 
as number, operationsState?.[toBeStartedIdx]?.id as number);
+    }
+
+  }, []);
+
+  return (
+    <div className="p-3">
       <Stack direction="vertical">
-        {operation.stages.map((stage: any) => {
+        {operationsState.map((operation: any) => {
           return (
             <Stack
               direction="horizontal"
               className="justify-content-between mt-3"
-              key={stage.context}
+              key={operation.label}
             >
               <div className="d-flex align-items-center">
-                {isFinished(stage.status) && (
-                  <FontAwesomeIcon icon={faCircleCheck} color="success" />
-                )}
-                {isFailed(stage.status) && (
-                  <FontAwesomeIcon icon={faTimes} color="danger" />
-                )}
-                <div>{stage.context} </div>
-                {isFailed(stage.status) ? (
+                {getStatusIcon(operation?.status)}
+                <div
+                  onClick={() => {
+                    if (operation.requestId || operation?.requestInfo?.id) {
+                      // modalManager.show(
+                      //   <BackgroundOperations
+                      //     isExplicitClick
+                      //     isOpen
+                      //     onClose={() => {
+                      //       modalManager.hide();
+                      //     }}
+                      //     rootLevel={ViewLevel.HOSTS}
+                      //     requestId={
+                      //       operation.requestId || 
operation?.requestInfo?.id
+                      //     }
+                      //   />
+                      // );
+                    }
+                  }}
+                  className={`${
+                    has(operation, "requestId") ||
+                    has(operation, "requestInfo.id")
+                      ? "custom-link"
+                      : ""
+                  }`}
+                >
+                  {operation.label}{" "}
+                </div>
+                {isFailed(operation.status) ? (
                   <Button
                     size="sm"
                     onClick={retryOperation}
                     variant="success"
-                    className="ms-2"
+                    className="mx-2"
                   >
                     <FontAwesomeIcon className="me-2" icon={faUndo} />
                     Retry Operation
                   </Button>
                 ) : null}
               </div>
-              {get(stage, "progress_percent", 0) &&
-              !isFinished(stage.status) ? (
+              {has(operation, "progress") && !isFinished(operation.status) ? (
                 <ProgressBar
-                  striped
                   className={`w-25`}
                   variant="info"
-                  now={stage.progress_percent}
-                  label={`${Math.floor(stage.progress)}%`}
+                  now={operation.progress}
+                  label={`${Math.floor(operation.progress)}%`}
                 />
               ) : null}
 
@@ -243,108 +397,6 @@ function OperationsProgress({
           );
         })}
       </Stack>
-    );
-  };
-
-  useEffect(() => {
-    if(dispatch){
-      dispatch(operationsState);
-    }
-  }, [JSON.stringify(operationsState)]);
-
-  useEffect(() => {
-    let idx = -1;
-    for (let i = operationsRef.current.length - 1; i >= 0; i--) {
-      if (
-        get(operationsRef.current[i], "requestId", "") ||
-        get(operationsRef.current[i], "status", "")
-      ) {
-        idx = i;
-        activeOperationId.current = operationsRef.current?.[i]?.id;
-        break;
-      }
-    }
-    if (idx === -1) {
-      executeTask(operationsRef.current?.[0]?.id);
-    } else {
-      for (let i = 0; i <= idx; i++) {
-        if (
-          get(operationsRef.current[i], "requestId", "") ||
-          get(operationsRef.current[i], "status", "")
-        ) {
-          startedTasks.current.push(operationsRef.current[i].id);
-        }
-      }
-    }
-  }, []);
-
-
-  return (
-    <div className="p-3">
-      <Stack direction="vertical">
-        {operationsState.map((operation: any) => {
-          const operationStages = operation.stages || [];
-          if (operationStages.length) {
-            return renderStagesForOperation(operation);
-          } else {
-            return (
-              <Stack
-                direction="horizontal"
-                className="justify-content-between mt-3"
-                key={operation.label}
-              >
-                <div className="d-flex align-items-center">
-                  <div
-                    onClick={() => {
-                    //   modalManager.show(
-                    //     <BackgroundOperations
-                    //       isOpen
-                    //       onClose={() => {
-                    //         modalManager.hide();
-                    //       }}
-                    //       rootLevel={ViewLevel.HOSTS}
-                    //       requestId={
-                    //         operation.requestId || 
operation?.requestInfo?.id
-                    //       }
-                    //     />
-                    //   );
-                    }}
-                    className={`${
-                      isFinished(operation.status) ||
-                      has(operation, "progress") ||
-                      has(operation, "requestId")
-                        ? "custom-link"
-                        : ""
-                    }`}
-                  >
-                    {operation.label}{" "}
-                  </div>
-                  {isFailed(operation.status) ? (
-                    <Button
-                      size="sm"
-                      onClick={retryOperation}
-                      variant="success"
-                      className="ms-2"
-                    >
-                      <FontAwesomeIcon className="me-2" icon={faUndo} />
-                      Retry Operation
-                    </Button>
-                  ) : null}
-                </div>
-                {has(operation, "progress") && !isFinished(operation.status) ? 
(
-                  <ProgressBar
-                    striped
-                    className={`w-25`}
-                    variant="info"
-                    now={operation.progress}
-                    label={`${Math.floor(operation.progress)}%`}
-                  />
-                ) : null}
-              </Stack>
-            );
-          }
-        })}
-      </Stack>
     </div>
   );
 }
diff --git a/ambari-web/latest/src/constants.ts 
b/ambari-web/latest/src/constants.ts
index 4e39282312..2311ec7e20 100644
--- a/ambari-web/latest/src/constants.ts
+++ b/ambari-web/latest/src/constants.ts
@@ -28,7 +28,12 @@ export enum ProgressStatus {
   COMPLETED = "COMPLETED",
   FAILED = "FAILED",
 }
-
+export enum ViewLevel {
+  REQUESTS = 1,
+  HOSTS = 2,
+  TASKS_LIST = 3,
+  TASK_LOGS = 4,
+}
 export const serviceNameModelMapping: { [key: string]: string } = {
   HDFS: "hdfs",
   YARN: "yarn",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to