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 3c714856d2 AMBARI-26388: Ambari Web React: List Versions in stack and 
Versions (#4061)
3c714856d2 is described below

commit 3c714856d2d5ebb5746b468f1ae29806ae1f2b9e
Author: Sandeep  Kumar <skuma...@visa.com>
AuthorDate: Tue Sep 9 21:40:28 2025 +0530

    AMBARI-26388: Ambari Web React: List Versions in stack and Versions (#4061)
---
 .../ClusterAdmin/StackAndVersions/ListVersion.tsx  | 467 +++++++++++++++++++++
 .../screens/ClusterAdmin/StackAndVersions/types.ts | 137 ++++++
 2 files changed, 604 insertions(+)

diff --git 
a/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/ListVersion.tsx 
b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/ListVersion.tsx
new file mode 100644
index 0000000000..c91b075307
--- /dev/null
+++ 
b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/ListVersion.tsx
@@ -0,0 +1,467 @@
+/**
+ * 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, useRef, useState } from "react";
+import {
+  Button,
+  ButtonGroup,
+  Dropdown,
+  OverlayTrigger,
+  Tooltip,
+} from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faEdit, faExternalLink } from "@fortawesome/free-solid-svg-icons";
+import VersionsApi from "../../../api/VersionsApi";
+import toast from "react-hot-toast";
+import Spinner from "../../../components/Spinner";
+import Table from "../../../components/Table";
+import { get } from "lodash";
+import Modal from "../../../components/Modal";
+import usePolling from "../../../hooks/usePolling";
+import { Link } from "react-router-dom";
+import { AppContext } from "../../../store/context";
+import { RequestApi } from "../../../api/requestApi";
+import OperationsProgress from "../../../components/OperationProgress";
+import { StackVersion } from "./types";
+
+export default function Versions() {
+  const [loading, setLoading] = useState(false);
+  const [services, setServices] = useState<string[]>([]);
+  const [stacks, setStacks] = useState<StackVersion[]>([]);
+  const { clusterName } = useContext(AppContext);
+
+  const [versionModal, setVersionModal] = useState(false);
+  const [installPackagesModal, setInstallPackagesModal] = useState(false);
+  const [repoModal, setRepoModal] = useState(false);
+  const [hostModal, setHostModal] = useState(false);
+  const [, setCompletionStatus] = useState(false);
+
+  const [selectedStack, setSelectedStack] = useState<
+    StackVersion | undefined
+  >();
+  const [operations, setOperations] = useState({});
+  const [payload, setPayload] = useState({});
+
+  const hostModalContent = useRef("");
+  const hostModalTitle = useRef("");
+
+  const {} = usePolling(fetchServices, 1000);
+
+  async function fetchServices() {
+    try {
+      if (!stacks) setLoading(true);
+      const response = await VersionsApi.getServices(clusterName);
+
+      setStacks(response.items);
+      const services = Object.keys(
+        response.items[0].ClusterStackVersions.repository_summary.services
+      );
+      setServices(services);
+      setLoading(false);
+    } catch (err) {
+      toast.error("Failed to fetch data");
+      setLoading(false);
+    }
+  }
+
+  if (loading) {
+    return <Spinner />;
+  }
+
+  const columns = [
+    {
+      accessorKey: "name",
+      header: "",
+      cell: (info: any) => info.row.original,
+      width: "10%",
+    },
+    ...stacks.map((stackData: StackVersion, index: number) => {
+      return {
+        accessorKey: `stack-${index}`,
+        header: getStackHeader(stackData),
+        cell: (info: any) => {
+          const service = get(
+            stackData.ClusterStackVersions.repository_summary.services,
+            info.row.original,
+            { version: "UNKNOWN" }
+          );
+          return service.version;
+        },
+        id: `stack-${index}`,
+      };
+    }),
+  ];
+
+  function getStackHeader(stackData: StackVersion) {
+    return (
+      <div>
+        <div>
+          {stackData.repository_versions[0].RepositoryVersions.display_name}
+        </div>
+        <div className="mt-2">
+          <small className="text-muted mt-2">
+            (
+            {
+              stackData.repository_versions[0].RepositoryVersions
+                .repository_version
+            }
+            )
+          </small>
+        </div>
+        <div className="mt-2">
+          <small
+            className="custom-link"
+            onClick={() => {
+              setSelectedStack(stackData);
+              setVersionModal(true);
+            }}
+          >
+            Show Details
+          </small>
+        </div>
+        {getButtonName(stackData) === "installing" ||
+        getButtonName(stackData) === "intermediateInstalling" ? (
+          <Link to={""}>
+            <OperationsProgress
+              operations={operations as any}
+              title="install packages"
+              description="install packages"
+              setCompletionStatus={setCompletionStatus}
+            />
+          </Link>
+        ) : getButtonName(stackData) === "UPGRADE" ? (
+          <Dropdown as={ButtonGroup}>
+            <Button variant="success">Upgrade</Button>
+            <Dropdown.Toggle split variant="success" id="dropdown" />
+            <Dropdown.Menu>
+              <Dropdown.Item
+                onClick={() => installPackagesPayloadSet(stackData)}
+              >
+                Re-install
+              </Dropdown.Item>
+              <Dropdown.Item>Pre-upgrade check</Dropdown.Item>
+            </Dropdown.Menu>
+          </Dropdown>
+        ) : (
+          <Button
+            className="mt-2"
+            variant="success"
+            size="sm"
+            onClick={() => handleUpgradeButton(stackData)}
+          >
+            {getButtonName(stackData)}
+          </Button>
+        )}
+      </div>
+    );
+  }
+
+  function installPackagesPayloadSet(stackData: StackVersion) {
+    setSelectedStack(stackData);
+    const payload = {
+      ClusterStackVersions: {
+        stack: stackData.ClusterStackVersions.stack,
+        version: stackData.ClusterStackVersions.version,
+        repository_version:
+          stackData.repository_versions[0].RepositoryVersions
+            .repository_version,
+      },
+    };
+
+    setPayload(payload);
+    setInstallPackagesModal(true);
+  }
+
+  function handleUpgradeButton(stackData: StackVersion) {
+    setSelectedStack(stackData);
+    switch (getButtonName(stackData)) {
+      case "CURRENT":
+        return;
+      case "UPGRADE":
+        return "show upgrade modal";
+      default:
+        installPackagesPayloadSet(stackData);
+    }
+  }
+
+  function installPackages() {
+    const operations = [
+      {
+        id: "1",
+        label: "installing",
+        skippable: false,
+        context: "install packages",
+        callback: async () => {
+          const reqData = await RequestApi.installPackages(
+            clusterName,
+            payload
+          );
+          return reqData;
+        },
+      },
+    ];
+    setOperations(operations);
+    if (selectedStack) {
+      selectedStack.ClusterStackVersions.state = "INTERMEDIATE";
+    }
+    setInstallPackagesModal(false);
+  }
+
+  function getButtonName(stackData: StackVersion) {
+    const stackState = stackData.ClusterStackVersions.state;
+
+    switch (stackState) {
+      case "CURRENT":
+        return "CURRENT";
+      case "INSTALL_FAILED":
+        return "RE-INSTALL";
+      case "INSTALLED":
+        return "UPGRADE";
+      case "INSTALLING":
+        return "installing";
+      case "INTERMEDIATE":
+        return "intermediateInstalling";
+      default:
+        return "INSTALL_PACKAGES";
+    }
+  }
+
+  function getInstallPackageModalBody() {
+    const displayName =
+      selectedStack?.repository_versions?.[0]?.RepositoryVersions
+        ?.display_name || "N/A";
+    return (
+      <div>
+        You are about to install packages for version
+        <strong>{displayName}</strong> on all hosts.
+      </div>
+    );
+  }
+
+  function getVersionModalBody() {
+    if (!selectedStack || Object.keys(selectedStack).length === 0) {
+      return null;
+    }
+
+    const hostStates = selectedStack.ClusterStackVersions.host_states;
+    const installed = hostStates.INSTALLED.length;
+    const current = hostStates.CURRENT.length;
+    const notInstalled =
+      Object.values(hostStates).flat().length - installed - current;
+
+    return (
+      <div>
+        <div className="d-flex justify-content-center">
+          <div className="fw-bold">
+            {selectedStack?.repository_versions[0]?.RepositoryVersions
+              ?.display_name || "NA"}
+          </div>
+          <small className="mx-2">
+            <OverlayTrigger
+              placement="top"
+              delay={{ show: 250, hide: 400 }}
+              overlay={<Tooltip>Click to Edit Repositories</Tooltip>}
+            >
+              <FontAwesomeIcon
+                onClick={() => setRepoModal(true)}
+                icon={faEdit}
+              />
+            </OverlayTrigger>
+          </small>
+        </div>
+        <div className="mt-2 text-center">
+          <small className="text-muted mt-2">
+            (
+            {selectedStack?.repository_versions?.[0]?.RepositoryVersions
+              ?.repository_version || "NA"}
+            )
+          </small>
+        </div>
+        <div className="text-center">
+          {getButtonName(selectedStack) === "installing" ||
+          getButtonName(selectedStack) === "intermediateInstalling" ? (
+            <Link to={""}>
+              <OperationsProgress
+                operations={operations as any}
+                title="install packages"
+                description="install packages"
+                setCompletionStatus={setCompletionStatus}
+              />
+            </Link>
+          ) : getButtonName(selectedStack) === "UPGRADE" ? (
+            <Dropdown as={ButtonGroup}>
+              <Button variant="success">Upgrade</Button>
+              <Dropdown.Toggle split variant="success" id="dropdown" />
+              <Dropdown.Menu>
+                <Dropdown.Item
+                  onClick={() => installPackagesPayloadSet(selectedStack)}
+                >
+                  Re-install
+                </Dropdown.Item>
+                <Dropdown.Item>Pre-upgrade check</Dropdown.Item>
+              </Dropdown.Menu>
+            </Dropdown>
+          ) : (
+            <Button
+              className="mt-2"
+              variant="success"
+              size="sm"
+              onClick={() => handleUpgradeButton(selectedStack)}
+            >
+              {getButtonName(selectedStack)}
+            </Button>
+          )}
+        </div>
+        <div>
+          <div className="text-center mt-3">Hosts</div>
+          <div className="d-flex justify-content-between mt-2">
+            <div>
+              <Button
+                variant="link"
+                onClick={() =>
+                  handleHostClick(
+                    "notInstalled",
+                    Object.values(hostStates)
+                      .flat()
+                      .filter(
+                        (host) =>
+                          !hostStates.CURRENT.includes(host) &&
+                          !hostStates.INSTALLED.includes(host)
+                      )
+                  )
+                }
+                disabled={notInstalled === 0}
+              >
+                {notInstalled}
+              </Button>
+              <div>not installed</div>
+            </div>
+            <div>
+              <Button
+                variant="link"
+                onClick={() =>
+                  handleHostClick("installed", hostStates.INSTALLED)
+                }
+                disabled={installed === 0}
+              >
+                {installed}
+              </Button>
+              <div>installed</div>
+            </div>
+            <div>
+              <Button
+                variant="link"
+                onClick={() => handleHostClick("current", hostStates.CURRENT)}
+                disabled={current === 0}
+              >
+                {current}
+              </Button>
+              <div>current</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  function handleHostClick(status: string, hosts: string[]) {
+    const versionStatus =
+      status === "current"
+        ? "Current"
+        : status === "installed"
+        ? "Installed"
+        : "Not installed";
+    const versionName =
+      selectedStack?.repository_versions[0]?.RepositoryVersions?.display_name 
||
+      "N/A";
+    const hostList = hosts.join("\n\n");
+
+    hostModalContent.current = `${versionName} is ${
+      versionStatus.toLowerCase() === "current"
+        ? "applied"
+        : versionStatus.toLowerCase()
+    } on ${hosts.length} hosts:\n\n\n${hostList}`;
+    hostModalTitle.current = `Version Status: ${versionStatus}`;
+    setHostModal(true);
+  }
+
+  return (
+    <>
+      <div className="mt-4">
+        <div>
+          <Button variant="success" size="sm">
+            <FontAwesomeIcon icon={faExternalLink} /> Manage versions
+          </Button>
+        </div>
+        <Table data={services} columns={columns} />
+      </div>
+
+      <Modal
+        isOpen={versionModal}
+        onClose={() => setVersionModal(false)}
+        modalTitle="Version Details"
+        modalBody={getVersionModalBody()}
+        options={{
+          okButtonText: "DISMISS",
+          cancelableViaIcon: true,
+        }}
+        successCallback={() => setVersionModal(false)}
+      />
+
+      <Modal
+        isOpen={repoModal}
+        onClose={() => setRepoModal(false)}
+        modalTitle="Repositories"
+        modalBody="Repository details will be shown here"
+        options={{
+          cancelableViaBtn: true,
+          okButtonText: "SAVE",
+        }}
+        successCallback={() => {}}
+      />
+
+      <Modal
+        isOpen={installPackagesModal}
+        onClose={() => setInstallPackagesModal(false)}
+        modalTitle="Confirmation"
+        modalBody={getInstallPackageModalBody()}
+        options={{
+          cancelableViaBtn: true,
+          cancelableViaIcon: true,
+        }}
+        successCallback={() => installPackages()}
+      />
+
+      <Modal
+        isOpen={hostModal}
+        onClose={() => setHostModal(false)}
+        modalTitle={hostModalTitle.current}
+        modalBody={hostModalContent.current}
+        options={{
+          cancelableViaBtn: true,
+          cancelableViaIcon: true,
+          okButtonText: "GO TO HOSTS",
+          cancelButtonText: "CLOSE",
+        }}
+        successCallback={() => {
+          "go to hosts";
+        }}
+      />
+    </>
+  );
+}
\ No newline at end of file
diff --git 
a/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/types.ts 
b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/types.ts
new file mode 100644
index 0000000000..a03939ddde
--- /dev/null
+++ b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/types.ts
@@ -0,0 +1,137 @@
+/**
+ * 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.
+ */
+
+type RepositoryVersion = {
+  href: string;
+  RepositoryVersions: {
+    display_name: string;
+    has_children: boolean;
+    hidden: boolean;
+    id: number;
+    parent_id: number | null;
+    repository_version: string;
+    resolved: boolean;
+    services: Service[];
+    stack_name: string;
+    stack_services: StackService[];
+    stack_version: string;
+    type: string;
+    release: {
+      build: string | null;
+      compatible_with: string | null;
+      notes: string;
+      version: string;
+    };
+  };
+  operating_systems: OperatingSystem[];
+}
+
+type Service = {
+  name: string;
+  versions: Version[];
+  display_name: string;
+}
+
+type Version = {
+  version: string;
+  components: any[];
+}
+
+type StackService = {
+  name: string;
+  display_name: string;
+  comment: string;
+  versions: string[];
+}
+
+type OperatingSystem = {
+  href: string;
+  OperatingSystems: {
+    ambari_managed_repositories: boolean;
+    os_type: string;
+    repository_version_id: number;
+    stack_name: string;
+    stack_version: string;
+  };
+  repositories: Repository[];
+}
+
+type Repository = {
+  href: string;
+  Repositories: {
+    applicable_services: any[];
+    base_url: string;
+    cluster_version_id: number;
+    components: any | null;
+    default_base_url: string;
+    distribution: any | null;
+    mirrors_list: string;
+    os_type: string;
+    repo_id: string;
+    repo_name: string;
+    repository_version_id: number;
+    stack_name: string;
+    stack_version: string;
+    tags: any[];
+    unique: boolean;
+  };
+}
+
+type ClusterStackVersion = {
+  cluster_name: string;
+  id: number;
+  repository_summary: {
+    services: {
+      [key: string]: {
+        version: string;
+        release_version: string;
+        upgrade: boolean;
+      };
+    };
+  };
+  repository_version: number;
+  stack: string;
+  state: string;
+  supports_revert: boolean;
+  version: string;
+  host_states: {
+    CURRENT: string[];
+    INSTALLED: string[];
+    INSTALLING: string[];
+    INSTALL_FAILED: string[];
+    NOT_REQUIRED: string[];
+    OUT_OF_SYNC: string[];
+  };
+}
+
+type StackVersion = {
+  href: string;
+  ClusterStackVersions: ClusterStackVersion;
+  repository_versions: RepositoryVersion[];
+}
+
+export type {
+    RepositoryVersion,
+    Service,
+    Version,
+    StackService,
+    OperatingSystem,
+    Repository,
+    ClusterStackVersion,
+    StackVersion,
+};
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@ambari.apache.org
For additional commands, e-mail: commits-h...@ambari.apache.org

Reply via email to