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]