This is an automated email from the ASF dual-hosted git repository.
jialiang 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 bcc7faca1f AMBARI-26390: Ambari Web React: Implement list Upgrade
History (#4085)
bcc7faca1f is described below
commit bcc7faca1fc7371be0f22f755bc0bb193892f976
Author: Sandeep Kumar <[email protected]>
AuthorDate: Sun Nov 9 07:08:13 2025 +0530
AMBARI-26390: Ambari Web React: Implement list Upgrade History (#4085)
---
ambari-web/latest/src/api/VersionsApi.ts | 8 +
.../StackAndVersions/UpgradeHistory.tsx | 360 +++++++++++++++++++++
2 files changed, 368 insertions(+)
diff --git a/ambari-web/latest/src/api/VersionsApi.ts
b/ambari-web/latest/src/api/VersionsApi.ts
index 9a3e2ed10e..bff4207d70 100644
--- a/ambari-web/latest/src/api/VersionsApi.ts
+++ b/ambari-web/latest/src/api/VersionsApi.ts
@@ -190,6 +190,14 @@ const VersionsApi = {
});
return response.data;
},
+ getUpgradeHistory: async function (clusterName: string) {
+ const url = `/clusters/${clusterName}/upgrades?fields=Upgrade`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET"
+ });
+ return response.data;
+ },
};
export default VersionsApi;
diff --git
a/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/UpgradeHistory.tsx
b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/UpgradeHistory.tsx
new file mode 100644
index 0000000000..f53b273bcd
--- /dev/null
+++
b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/UpgradeHistory.tsx
@@ -0,0 +1,360 @@
+/**
+ * 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 { useContext, useEffect, useRef, useState } from "react";
+import VersionsApi from "../../../api/VersionsApi";
+import { AppContext } from "../../../store/context";
+import { get } from "lodash";
+import Table from "../../../components/Table";
+import { Badge, Button } from "react-bootstrap";
+import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import Spinner from "../../../components/Spinner";
+import { StackVersion } from "./types";
+import { getUpgradeDisplayName } from "../../../Utils/Utility";
+import Upgrade from "./Upgrade";
+
+type Upgrade = {
+ associated_version: string;
+ cluster_name: string;
+ create_time: number;
+ direction: "UPGRADE" | "DOWNGRADE";
+ downgrade_allowed: boolean;
+ end_time: number;
+ exclusive: boolean;
+ pack: string;
+ progress_percent: number;
+ request_context: string;
+ request_id: number;
+ request_status: string;
+ skip_failures: boolean;
+ skip_service_check_failures: boolean;
+ start_time: number;
+ suspended: boolean;
+ type: string;
+ upgrade_id: number;
+ upgrade_type: string;
+ versions: Record<
+ string,
+ {
+ from_repository_id: number;
+ from_repository_version: string;
+ to_repository_id: number;
+ to_repository_version: string;
+ }
+ >;
+};
+
+type UpgradeItem = {
+ href: string;
+ Upgrade: Upgrade;
+};
+
+const options = [
+ { key: "All", values: ["ALL"] },
+ { key: "Upgrade", values: [{ direction: ["UPGRADE"] }] },
+ { key: "Downgrade", values: [{ direction: ["DOWNGRADE"] }] },
+ {
+ key: "Successful Upgrade",
+ values: [{ direction: ["UPGRADE"] }, { request_status: ["COMPLETED"] }],
+ },
+ {
+ key: "Aborted Upgrade",
+ values: [{ direction: ["UPGRADE"] }, { request_status: ["ABORTED"] }],
+ },
+ {
+ key: "Failed Upgrade",
+ values: [{ direction: ["UPGRADE"] }, { request_status: ["FAILED"] }],
+ },
+ {
+ key: "Successful Downgrade",
+ values: [{ direction: ["DOWNGRADE"] }, { request_status: ["COMPLETED"] }],
+ },
+ {
+ key: "Aborted Downgrade",
+ values: [{ direction: ["DOWNGRADE"] }, { request_status: ["ABORTED"] }],
+ },
+ {
+ key: "Failed Downgrade",
+ values: [{ direction: ["DOWNGRADE"] }, { request_status: ["FAILED"] }],
+ },
+];
+
+export default function UpgradeHistory() {
+ const [selectedOption, setSelectedOption] = useState(options[0]);
+ const [stacks, SetStacks] = useState<StackVersion[]>([]);
+ const [originalUpgrades, setOriginalUpgrades] = useState<UpgradeItem[]>([]);
+ const [upgrades, setUpgrades] = useState<UpgradeItem[]>([]);
+ const [showModal, setShowModal] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const { clusterName } = useContext(AppContext);
+ const upgradeIdRef = useRef<number>(0);
+
+ useEffect(() => {
+ async function fetchUpgrades() {
+ if (originalUpgrades.length === 0) setLoading(true);
+
+ const stacksResponse = await VersionsApi.getAllStacks(clusterName);
+ const stacksArray = get(stacksResponse, "items", []);
+ SetStacks(stacksArray);
+ const response = await VersionsApi.getUpgradeHistory(clusterName);
+ setOriginalUpgrades(get(response, "items").reverse());
+ setLoading(false);
+ }
+ fetchUpgrades();
+ }, []);
+
+ useEffect(() => {
+ if (selectedOption.key !== "All") {
+ setUpgrades(
+ originalUpgrades.filter((upgrade) => {
+ return selectedOption.values.every((condition) => {
+ const [key, values] = Object.entries(condition)[0];
+ return values.includes(upgrade.Upgrade[key as keyof Upgrade]);
+ });
+ })
+ );
+ } else {
+ setUpgrades(originalUpgrades);
+ }
+ }, [originalUpgrades, selectedOption]);
+
+ const handleSelectChange = (event: any) => {
+ const option = options.find((o) => o.key === event.target.value);
+ if (option) {
+ setSelectedOption(option);
+ }
+ };
+
+ function getUpgradeId(href: string) {
+ const id = href.split("/").pop();
+ return parseInt(id || "0", 0);
+ }
+ // START GENAI
+ function getUpgradeType(upgrade: Upgrade) {
+ const upgradeType = upgrade.upgrade_type;
+ const stackVersion = stacks.find(
+ (stack) =>
+ stack.repository_versions[0].RepositoryVersions.repository_version ===
+ upgrade.associated_version
+ );
+ const isPatch =
+ stackVersion?.repository_versions[0].RepositoryVersions.type === "PATCH";
+ if (isPatch) {
+ return "PATCH";
+ }
+
+ return getUpgradeDisplayName(upgradeType);
+ }
+ // END GENAI
+
+ const columns = [
+ {
+ header: () => <span>Direction</span>,
+ accessorKey: "direction",
+ width: "10%",
+ cell: (info: any) => {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const toggleExpand = () => {
+ setIsExpanded(!isExpanded);
+ };
+ const versions = get(info, "row.original.Upgrade.versions");
+
+ const handleOpenUpgrade = () => {
+ const upgradeId = getUpgradeId(get(info, "row.original.href"));
+ upgradeIdRef.current = upgradeId;
+ setShowModal(true);
+ };
+
+ return (
+ <>
+ <div>
+ {isExpanded ? (
+ <FontAwesomeIcon
+ icon={faCaretDown}
+ onClick={() => toggleExpand()}
+ />
+ ) : (
+ <FontAwesomeIcon
+ icon={faCaretRight}
+ onClick={() => toggleExpand()}
+ />
+ )}
+ <Button
+ variant="link"
+ size="sm"
+ onClick={() => handleOpenUpgrade()}
+ >
+ {get(info, "row.original.Upgrade.direction")}
+ </Button>
+ </div>
+
+ {isExpanded && (
+ <div className="mt-2 full-width">
+ <table className="table">
+ <tbody>
+ {Object.entries(versions).map(
+ ([serviceName, versionInfo]) => (
+ <tr key={serviceName}>
+ <td className="w-30">{serviceName}</td>
+ <td className="w-30">
+ <Badge>
+ {get(versionInfo, "from_repository_version")}
+ </Badge>
+ </td>
+ <td className="w-5 text-center">→</td>
+ <td className="w-35">
+ <Badge>
+ {get(versionInfo, "to_repository_version")}
+ </Badge>
+ </td>
+ </tr>
+ )
+ )}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </>
+ );
+ },
+ },
+ {
+ header: () => <span>Type</span>,
+ accessorKey: "type",
+ width: "10%",
+ cell: (info: any) => {
+ return getUpgradeDisplayName(
+ get(info, "row.original.Upgrade.upgrade_type")
+ );
+ },
+ },
+ {
+ header: () => <span>Repository</span>,
+ accessorKey: "repository",
+ width: "10%",
+ cell: (info: any) => {
+ return get(info, "row.original.Upgrade.associated_version");
+ },
+ },
+ {
+ header: () => <span>Repository Type</span>,
+ accessorKey: "repository_type",
+ width: "10%",
+ cell: (info: any) => {
+ return getUpgradeType(get(info, "row.original.Upgrade"));
+ },
+ },
+ {
+ header: () => <span>Start Time</span>,
+ accessorKey: "start_time",
+ width: "15%",
+ cell: (info: any) => {
+ const startTime = new Date(
+ get(info, "row.original.Upgrade.start_time")
+ );
+ return (
+ startTime.toDateString() +
+ " " +
+ startTime.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ );
+ },
+ },
+ {
+ header: () => <span>Duration</span>,
+ accessorKey: "duration",
+ width: "10%",
+ cell: (info: any) => {
+ const { start_time, end_time } = info.row.original.Upgrade;
+
+ const durationMs = end_time - start_time;
+ const hours = Math.floor(durationMs / (1000 * 60 * 60));
+ const minutes = Math.floor(
+ (durationMs % (1000 * 60 * 60)) / (1000 * 60)
+ );
+ const seconds = Math.floor((durationMs % (1000 * 60)) / 1000);
+ return `${hours}h ${minutes}m ${seconds}s`;
+ },
+ },
+ {
+ header: () => <span>End Time</span>,
+ accessorKey: "end_time",
+ width: "15%",
+ cell: (info: any) => {
+ const endTime = new Date(get(info, "row.original.Upgrade.end_time"));
+ return (
+ endTime.toDateString() +
+ " " +
+ endTime.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit"
})
+ );
+ },
+ },
+ {
+ header: () => <span>Status</span>,
+ accessorKey: "status",
+ width: "10%",
+ cell: (info: any) => {
+ return get(info, "row.original.Upgrade.request_status");
+ },
+ },
+ ];
+
+ if (loading) {
+ return <Spinner />;
+ }
+
+ if (originalUpgrades.length === 0) {
+ return (
+ <div className="text-left mt-5">
+ <h2>No Upgrade History Found</h2>
+ </div>
+ );
+ }
+
+ return (
+ <>
+ {showModal && (
+ <Upgrade
+ upgradeId={upgradeIdRef.current}
+ onlyView={true}
+ onClose={() => setShowModal(false)}
+ />
+ )}
+ <div className="mt-5">
+ <div className="d-flex justify-content-between">
+ <h2>Upgrade History</h2>
+ <select
+ value={selectedOption.key}
+ onChange={handleSelectChange}
+ className="w-20 mx-2 mb-1"
+ >
+ {options.map((option) => (
+ <option key={option.key} value={option.key}>
+ {option.key}
+ </option>
+ ))}
+ </select>
+ </div>
+ <Table columns={columns} data={upgrades}></Table>
+ </div>
+ </>
+ );
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]