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