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 0bc3307d7b AMBARI-26588 Cluster Installation steps 0,1,4 for
incremental data of cluster installation (#4114)
0bc3307d7b is described below
commit 0bc3307d7b76fa10b1e4f45bd452ed4ebed087f3
Author: vanshuhassija <[email protected]>
AuthorDate: Fri Feb 20 10:27:13 2026 +0530
AMBARI-26588 Cluster Installation steps 0,1,4 for incremental data of
cluster installation (#4114)
---
.../latest/src/components/MissingServiceModal.tsx | 62 +
.../src/components/StepWizard/WizardFooter.tsx | 48 +-
.../latest/src/screens/ClusterWizard/Step0.tsx | 163 +++
.../latest/src/screens/ClusterWizard/Step1.tsx | 1255 ++++++++++++++++++++
.../latest/src/screens/ClusterWizard/Step4.tsx | 623 ++++++++++
.../latest/src/screens/ClusterWizard/constants.ts | 56 +-
6 files changed, 2188 insertions(+), 19 deletions(-)
diff --git a/ambari-web/latest/src/components/MissingServiceModal.tsx
b/ambari-web/latest/src/components/MissingServiceModal.tsx
new file mode 100644
index 0000000000..2b013956f7
--- /dev/null
+++ b/ambari-web/latest/src/components/MissingServiceModal.tsx
@@ -0,0 +1,62 @@
+/**
+ * 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 { Button, Modal } from "react-bootstrap";
+import DefaultButton from "./DefaultButton";
+import { ModalType } from "../screens/ClusterWizard/constants";
+
+type PropTypes = {
+ isOpen: boolean;
+ onClose: () => void;
+ onCancel: () => void;
+ title : React.ReactNode;
+ body: React.ReactNode;
+ modalType?: string;
+};
+
+const MissingServiceModal = ({
+ isOpen,
+ onClose,
+ onCancel,
+ title,
+ body,
+ modalType,
+}: PropTypes) => {
+ return (
+ <Modal size="lg"show={isOpen} onHide={onCancel}>
+ <Modal.Header closeButton>
+ <Modal.Title>{title}</Modal.Title>
+ </Modal.Header>
+ <Modal.Body>{body}</Modal.Body>
+ <Modal.Footer>
+ <DefaultButton size="sm" onClick={onCancel}>
+ Cancel
+ </DefaultButton>
+ <Button
+ variant= {modalType === ModalType.MISSING_SERVICE ? "warning" :
"success"}
+ size="sm"
+ onClick={onClose}
+ className="rounded-1"
+ >
+ {modalType === ModalType.MISSING_SERVICE ? "PROCEED ANYWAY" : "OK"}
+ </Button>
+ </Modal.Footer>
+ </Modal>
+ );
+};
+
+export default MissingServiceModal;
diff --git a/ambari-web/latest/src/components/StepWizard/WizardFooter.tsx
b/ambari-web/latest/src/components/StepWizard/WizardFooter.tsx
index 86a2a11ffb..bf3bbdfb3a 100644
--- a/ambari-web/latest/src/components/StepWizard/WizardFooter.tsx
+++ b/ambari-web/latest/src/components/StepWizard/WizardFooter.tsx
@@ -21,7 +21,6 @@ import { ArrowLeft, ArrowRight } from "react-bootstrap-icons";
import { Step } from "../../types/StepWizard";
import Modal from "../Modal";
import { useState } from "react";
-import "./styles.scss";
interface PropTypes {
onBack: Function;
@@ -30,6 +29,8 @@ interface PropTypes {
isNextEnabled: boolean;
isBackEnabled?: boolean;
onCancel?: () => void;
+ lifted?: boolean;
+ sideItems?: any;
}
function WizardFooter({
@@ -39,10 +40,20 @@ function WizardFooter({
isNextEnabled,
onCancel = () => {},
isBackEnabled = true,
+ lifted = false,
+ sideItems,
}: PropTypes) {
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
return (
- <div className="step-wizard-footer d-flex justify-content-between bg-white
p-2">
+ <div
+ className="step-wizard-footer d-flex justify-content-between bg-white
p-2"
+ style={{
+ position: "absolute",
+ bottom: lifted ? "0px" : "-40px",
+ left: "0px",
+ width: "100%",
+ }}
+ >
<Modal
isOpen={showConfirmationModal}
onClose={() => {
@@ -50,7 +61,7 @@ function WizardFooter({
}}
options={{}}
modalTitle="Confirmation"
- modalBody="Are you sure?"
+ modalBody="Are you sure you want to cancel the operation?"
successCallback={() => {
onCancel();
}}
@@ -73,26 +84,27 @@ function WizardFooter({
variant="outline-secondary"
className="d-flex align-items-center ms-3 h-100"
onClick={() => {
- if (!onCancel) setShowConfirmationModal(true);
- else {
- onCancel();
- }
+ setShowConfirmationModal(true);
}}
>
<span className="ms-1">CANCEL</span>
</Button>
</Stack>
- <Button
- variant="success"
- className="me-3"
- onClick={() => {
- onNext();
- }}
- disabled={!isNextEnabled}
- >
- <span className="me-1">{step.nextLabel || "NEXT"}</span>
- <ArrowRight />
- </Button>
+ <Stack direction="horizontal" className="align-items-center">
+ {sideItems ? sideItems : null}
+
+ <Button
+ variant="success"
+ className="me-3"
+ onClick={() => {
+ onNext();
+ }}
+ disabled={!isNextEnabled}
+ >
+ <span className="me-1">{step.nextLabel || "NEXT"}</span>
+ <ArrowRight />
+ </Button>
+ </Stack>
</div>
);
}
diff --git a/ambari-web/latest/src/screens/ClusterWizard/Step0.tsx
b/ambari-web/latest/src/screens/ClusterWizard/Step0.tsx
new file mode 100644
index 0000000000..39785896f4
--- /dev/null
+++ b/ambari-web/latest/src/screens/ClusterWizard/Step0.tsx
@@ -0,0 +1,163 @@
+/**
+ * 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, useState } from "react";
+import {
+ Card,
+ CardBody,
+ Col,
+ Form,
+ OverlayTrigger,
+ Popover,
+} from "react-bootstrap";
+import { ActionTypes } from "./clusterStore/types";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCircleXmark } from "@fortawesome/free-solid-svg-icons";
+import { useTranslation } from "react-i18next";
+import WizardFooter from "../../components/StepWizard/WizardFooter";
+import { ContextWrapper } from ".";
+import { get } from "lodash";
+
+function Step0({ wizardName = "clusterCreation" }) {
+ const { Context } = useContext(ContextWrapper);
+ const {
+ state,
+ dispatch,
+ flushStateToDb,
+ stepWizardUtilities: { currentStep, handleNextImperitive },
+ }: any = useContext(Context);
+ const MAX_CLUSTER_NAME_LENGTH = 80;
+ const [errorMessage, setErrorMessage] = useState("");
+ const [clusterName, setClusterName] = useState("");
+ const [isClusterNameValid, setIsClusterNameValid] = useState(false);
+ const [nextEnabled, setNextEnabled] = useState(false);
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ setClusterName(
+ get(state, `${wizardName}Steps.${currentStep.name}.data.clusterName`, "")
+ );
+ }, [state]);
+
+ useEffect(() => {
+ if (validateClusterName(clusterName)) {
+ setNextEnabled(true);
+ }
+ }, [clusterName]);
+
+ const enableNext = () => {
+ setNextEnabled(true);
+ };
+
+ const disableNext = () => {
+ setNextEnabled(false);
+ };
+
+ const validateClusterName = (name: string) => {
+ if (!name) {
+ disableNext();
+ return false;
+ } else if (name.length > MAX_CLUSTER_NAME_LENGTH) {
+ disableNext();
+ setErrorMessage("Cluster name is too long");
+ return false;
+ } else if (/\s/.test(name)) {
+ disableNext();
+ setErrorMessage("Cluster Name cannot contain whitespace.");
+ return false;
+ } else if (/[^\w\s]/gi.test(name)) {
+ disableNext();
+ setErrorMessage("Cluster Name cannot contain special characters.");
+ return false;
+ } else {
+ setErrorMessage("");
+ enableNext();
+ return true;
+ }
+ };
+
+ const handleInputChange = (inputString: string) => {
+ setClusterName(inputString);
+ setIsClusterNameValid(validateClusterName(inputString));
+ };
+
+ return (
+ <>
+ <div className="d-flex flex-column">
+ <div className="step-title">{t("installer.step0.getStarted")}</div>
+ <div
className="step-description">{t("installer.step0.description")}</div>
+ <Card className="mb-4 mt-2">
+ <CardBody>
+ <span> {t("installer.step0.nameClusterInstruction")}</span>
+ <OverlayTrigger
+ trigger="hover"
+ key="learn more"
+ placement="right"
+ overlay={
+ <Popover id="popover-positioned-right">
+ <Popover.Header as="h3">
+ {t("installer.step0.clusterNamePlaceholder")}
+ </Popover.Header>
+ <Popover.Body>
+ {t("installer.step0.uniqueCluster")}
+ </Popover.Body>
+ </Popover>
+ }
+ >
+ <span className="text-info"> {t("common.learnMore")}</span>
+ </OverlayTrigger>
+ <Col className="d-flex my-2">
+ <Form.Control
+ type="text"
+ placeholder={t("installer.step0.clusterNamePlaceholder")}
+ onChange={(e) => handleInputChange(e.target.value)}
+ style={{ width: "25%" }}
+ value={clusterName}
+ />
+ {!isClusterNameValid && errorMessage && (
+ <div className="mt-2 mx-2 d-flex">
+ <FontAwesomeIcon icon={faCircleXmark} />
+ <p className="text-danger mx-2">{errorMessage}</p>
+ </div>
+ )}
+ </Col>
+ </CardBody>
+ </Card>
+ </div>
+ <WizardFooter
+ isNextEnabled={nextEnabled}
+ step={currentStep}
+ onNext={async () => {
+ if (validateClusterName(clusterName)) {
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: { step: currentStep.name, data: { clusterName } },
+ });
+ flushStateToDb("next");
+ handleNextImperitive();
+ }
+ }}
+ onCancel={() => {
+ flushStateToDb("cancel");
+ }}
+ onBack={() => {}}
+ />
+ </>
+ );
+}
+export default Step0;
diff --git a/ambari-web/latest/src/screens/ClusterWizard/Step1.tsx
b/ambari-web/latest/src/screens/ClusterWizard/Step1.tsx
new file mode 100644
index 0000000000..eb0cf3a16d
--- /dev/null
+++ b/ambari-web/latest/src/screens/ClusterWizard/Step1.tsx
@@ -0,0 +1,1255 @@
+/**
+ * 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.
+ */
+
+/* eslint-disable no-useless-escape */
+/* eslint-disable no-unsafe-optional-chaining */
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+//@ts-nocheck
+import { useContext, useEffect, useState } from "react";
+import {
+ Item,
+ VersionDefinition,
+ VersionDefinitionResponse,
+} from "./types/VersionsDefinition";
+import { useParams } from "react-router-dom";
+import {
+ Col,
+ Dropdown,
+ DropdownButton,
+ Nav,
+ Row,
+ Tab,
+ Form,
+ OverlayTrigger,
+ Alert,
+ Button,
+ Tooltip,
+ Card,
+ CardBody,
+} from "react-bootstrap";
+import { TransformedOperatingSystem, TransformedRepo } from "./types/Os";
+import { find, set, cloneDeep, get, isEmpty } from "lodash";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ faAdd,
+ faMinus,
+ faPencil,
+ faQuestionCircle,
+ faUndo,
+} from "@fortawesome/free-solid-svg-icons";
+import VersionsApi from "../../api/VersionsApi";
+import toast from "react-hot-toast";
+import AddVersionModal from "../../components/AddVersionModal";
+import LostNetworkModal from "../../components/LostNetworkModal";
+import Table from "../../components/Table";
+import DefaultButton from "../../components/DefaultButton";
+import RedhatSatelliteUsageInfo from
"../../components/RedhatSatelliteInfoModal";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import WizardFooter from "../../components/StepWizard/WizardFooter";
+import { ActionTypes } from "./clusterStore/types";
+import { getStepData } from "../../Utils/Utility";
+import wizardSteps from "./wizardSteps";
+import { ContextWrapper } from ".";
+
+enum RepositoryType {
+ PUBLIC = "public",
+ LOCAL = "local",
+}
+
+enum OSOperations {
+ GET = "get",
+ EDITALL = "editall",
+}
+
+export default function Step1({ wizardName = "clusterCreation" }) {
+ const [versionDefinitions, setVersionDefinitions] = useState<Item[]>([]);
+ const [selectedVersion, setSelectedVersion] = useState<any>({});
+ const [networkLost, setNetworkLost] = useState<boolean>(false);
+ const [showRegistrationModal, setShowRegistrationModal] =
+ useState<boolean>(false);
+ const [selectedStack, setSelectedStack] = useState<VersionDefinition>(
+ {} as VersionDefinition
+ );
+ // const history = useHistory();
+ const [selectedChoice, setSelectedChoice] = useState<string>(
+ RepositoryType.LOCAL
+ );
+ const { stack, version } = useParams<any>();
+ const [versionNumber, setVersionNumber] = useState(
+ version?.substring(4, version.length) || ""
+ );
+ const [versionValidationError, setVersionValidationError] = useState(false);
+ const [showNetworkModal, setShowNetworkModal] = useState<boolean>(false);
+ const [skipValidation, setSkipValidation] = useState<boolean>(false);
+ const [redhatSatellite, setRedhatSatellite] = useState<boolean>(false);
+ const [repoInfo, setRepoInfo] = useState<{ status: string }>(
+ {} as { status: string }
+ );
+ const [savedRepositoryVersionDetails, setSavedRepositoryVersionDetails] =
+ useState<any>({});
+ const [showRepoValidationBanner, setShowRepoValidationBanner] =
+ useState<boolean>(false);
+ const [showRedhatInfoModal, setShowRedhatInfoModal] =
+ useState<boolean>(false);
+ const [showAddVersionModal, setShowAddVersionModal] = useState(false);
+ const [addedVersions, setAddedVersions] = useState<{
+ [key: string]: { label: string; value: string }[];
+ }>({});
+ const [operatingSystems, setOperatingSystems] = useState<{
+ [key: string]: TransformedOperatingSystem[];
+ }>({});
+ const [nextEnabled, setNextEnabled] = useState(false);
+ const { Context } = useContext(ContextWrapper);
+ const {
+ state,
+ dispatch,
+ flushStateToDb,
+ stepWizardUtilities: {
+ currentStep,
+ handleNextImperitive,
+ jumpToStep,
+ handleBackImperitive,
+ },
+ }: any = useContext(Context);
+
+ const enableNext = () => {
+ setNextEnabled(true);
+ };
+
+ const disableNext = () => {
+ setNextEnabled(false);
+ };
+
+ const selectNewVersion = (
+ version: VersionDefinition,
+ newVersion?: { label: string; value: string },
+ newVersionDefinition?: VersionDefinition
+ ) => {
+ if (version.id) {
+ const addedVersionsCopy: {
+ [key: string]: { label: string; value: any }[];
+ } = cloneDeep(addedVersions);
+ const defaultVersion = {
+ label: `${version.id} (Default Version definition)`,
+ value: version,
+ };
+ if (addedVersionsCopy[version.id]) {
+ if (newVersion)
+ addedVersionsCopy[version.id] = [
+ ...addedVersionsCopy[version.id],
+ newVersion,
+ ];
+ } else {
+ if (newVersion) {
+ addedVersionsCopy[version.id] = [defaultVersion, newVersion];
+ } else {
+ addedVersionsCopy[version.id] = [defaultVersion];
+ }
+ }
+ setAddedVersions(addedVersionsCopy);
+ setSelectedVersion(newVersionDefinition ? newVersionDefinition :
version);
+ }
+ };
+
+ /* START GENAI */
+ useEffect(() => {
+ async function getVersionDefinitions() {
+ const definitions: VersionDefinitionResponse =
+ await VersionsApi.getVersionDefinitions();
+ const sortedItems = definitions.items.sort((a: any, b: any) => {
+ const versionA = parseFloat(a.VersionDefinition.id.split("-")[1]);
+ const versionB = parseFloat(b.VersionDefinition.id.split("-")[1]);
+
+ return versionB - versionA;
+ });
+ setVersionDefinitions(sortedItems);
+ setNetworkIssues(definitions.items);
+
+ const stateData = get(
+ state,
+ `${wizardName}Steps.${currentStep.name}.data`,
+ {}
+ );
+
+ if (isEmpty(stateData)) {
+ // Only set defaults if no previous state exists
+ setSelectedStack(sortedItems[0]?.VersionDefinition);
+ selectNewVersion(sortedItems[0]?.VersionDefinition);
+ }
+ }
+ getVersionDefinitions();
+ }, [state, wizardName, currentStep]);
+ /* END GENAI */
+
+ useEffect(() => {
+ if (!versionNumber) {
+ setVersionValidationError(false);
+ } else {
+ // Pattern for two numbers separated by a dot
+ const twoNumbersPattern = /^\d+\.\d+$/;
+ // Pattern for three numbers separated by a dot and a dash
+ const threeNumbersPattern = /^\d+\.\d+-\d+$/;
+
+ // Check if the string matches any of the patterns
+ if (
+ twoNumbersPattern.test(versionNumber) ||
+ threeNumbersPattern.test(versionNumber)
+ ) {
+ setVersionValidationError(false);
+ } else {
+ setVersionValidationError(true);
+ }
+ }
+ }, [versionNumber]);
+
+ useEffect(() => {
+ if (redhatSatellite) {
+ setShowRedhatInfoModal(true);
+ const updatedOs = editAllRepos("isEditable", false);
+ setOperatingSystems(updatedOs);
+ } else {
+ const revertOs = editAllRepos("isEditable", true);
+ setOperatingSystems(revertOs);
+ }
+ }, [redhatSatellite]);
+
+ /* START GENAI */
+ useEffect(() => {
+ const stateData = get(
+ state,
+ `${wizardName}Steps.${currentStep.name}.data`,
+ {}
+ );
+ if (!isEmpty(stateData)) {
+ // Fix for TLHASD-1260: Ensure proper state restoration for base URL
persistence
+ setSelectedVersion(stateData?.selectedVersion);
+ setSelectedStack(stateData?.selectedStack); // Fixed typo: was
'selelectedStack'
+ setSelectedChoice(stateData?.selectedChoice);
+ setSkipValidation(stateData?.skipValidation);
+ setRedhatSatellite(stateData?.redhatSatellite);
+ setOperatingSystems(stateData?.operatingSystems);
+ // Fix for TLHASD-1260: Restore addedVersions from XML uploads
+ if (stateData?.addedVersions) {
+ setAddedVersions(stateData.addedVersions);
+ }
+ }
+ }, [state]);
+ /* END GENAI */
+
+ const getOsfromRepoDetails = (oSystem: string, alreadyAddedOs: any) => {
+ let operatingSystem;
+ for (const addedOs of alreadyAddedOs?.[0]?.repository_versions?.[0]
+ ?.operating_systems) {
+ if (addedOs.OperatingSystems.os_type === oSystem) {
+ operatingSystem = addedOs;
+ }
+ }
+ return operatingSystem;
+ };
+
+ const isAllOsValidated = () => {
+ let allOsRemoved = true;
+ const remotePattern =
+
/^(?:(?:https?|ftp):\/{2})(?:\S+(?::\S*)?@)?(?:(?:(?:[\w\-.]))*)(?::[0-9]+)?(?:\/\S*)?$/;
+ const localPattern =
+ /^file:\/{2,3}([a-zA-Z][:|]\/){0,1}[\w~!*'();@&=\/\\\-+$,?%#.\[\]]+$/;
+ if (operatingSystems?.[selectedVersion.id]) {
+ for (const oSystem of operatingSystems?.[selectedVersion.id]) {
+ if (oSystem.isAdded) {
+ allOsRemoved = false;
+ }
+ for (const repo of oSystem.repos) {
+ if (
+ oSystem.isAdded &&
+ !(
+ remotePattern.test(repo.baseUrl) ||
+ localPattern.test(repo.baseUrl)
+ )
+ ) {
+ return false;
+ }
+ }
+ }
+ return allOsRemoved ? false : true;
+ }
+ return false;
+ };
+
+ useEffect(() => {
+ if (!isAllOsValidated() || versionValidationError) {
+ disableNext();
+ } else {
+ enableNext();
+ }
+ }, [operatingSystems, versionNumber, versionValidationError,
skipValidation]);
+
+ /* START GENAI */
+ useEffect(() => {
+ async function getVersionOs() {
+ const stateData = get(
+ state,
+ `${wizardName}Steps.${currentStep.name}.data`,
+ {}
+ );
+
+ // Don't overwrite existing operating systems data if we have restored
state
+ if (!isEmpty(stateData) &&
stateData.operatingSystems?.[selectedVersion.id]) {
+ return;
+ }
+
+ const versionOperatingSystems =
+ await VersionsApi.getVersionOperatingSystems(
+ selectedStack.stack_version
+ );
+ let alreadyAddedOs: any = [];
+ const allOs: TransformedOperatingSystem[] =
+ versionOperatingSystems.operating_systems.map((os: any) => {
+ const defaultRepos = os.repositories.map((repo: any) => {
+ return {
+ id: repo.Repositories.repo_id,
+ defaultId: repo.Repositories.repo_id,
+ baseUrl: "",
+ name: repo?.Repositories?.repo_name,
+ defaultUrl: "",
+ };
+ });
+ const matchingOs = undefined;
+ const isOsAdded = true;
+ return {
+ os: os.OperatingSystems.os_type,
+ isAdded: isOsAdded,
+ repos: defaultRepos,
+ };
+ });
+ const operatingSystemsCopy = cloneDeep(operatingSystems);
+ operatingSystemsCopy[selectedVersion.id] = [...allOs];
+ setOperatingSystems(operatingSystemsCopy);
+ }
+ if (
+ selectedStack &&
+ selectedVersion &&
+ selectedVersion.id &&
+ !operatingSystems?.[selectedVersion?.id]
+ ) {
+ getVersionOs();
+ }
+ }, [selectedVersion, state, wizardName, currentStep]);
+ /* END GENAI */
+
+ function getColumns() {
+ return [
+ {
+ header: "",
+ accessorKey: "display_name",
+ id: "name",
+ },
+ {
+ header: "",
+ id: "versions",
+ cell: (info: any) => {
+ const allVersions = info.row.original.versions;
+ return allVersions.join(",");
+ },
+ },
+ ];
+ }
+
+ const getSystemsWithKeyValue = (
+ key: string,
+ value: any,
+ osOperation: string
+ ) => {
+ const osCopy = cloneDeep(operatingSystems);
+ switch (osOperation) {
+ case OSOperations.GET:
+ return osCopy?.[selectedVersion?.id]?.filter(
+ (oSystem: TransformedOperatingSystem) =>
+ oSystem[key as keyof TransformedOperatingSystem] === value
+ );
+ case OSOperations.EDITALL:
+ return osCopy?.[selectedVersion]?.map(
+ (oSystem: TransformedOperatingSystem) => {
+ //@ts-ignore
+ oSystem[key as keyof TransformedOperatingSystem] = value;
+ return oSystem;
+ }
+ );
+ }
+ };
+
+ // const redirectToList = () => {
+ // history.push("/stackVersions");
+ // };
+
+ const getAddableOperatingSystems = () => {
+ const addableSystems = getSystemsWithKeyValue(
+ "isAdded",
+ false,
+ OSOperations.GET
+ );
+ return addableSystems || [];
+ };
+
+ const handleModalVisibility = (show: boolean) => {
+ setShowNetworkModal(show);
+ };
+
+ const setNetworkIssues = (versions: any[]) => {
+ const isNetworkLost = !!versions.find(
+ (_version) => _version.VersionDefinition.stack_default === false
+ );
+
+ if (isNetworkLost) {
+ setSelectedChoice(RepositoryType.LOCAL);
+ // clearRepoVersions();
+ }
+ setNetworkLost(isNetworkLost);
+ };
+
+ const addBackOperatingSystem = (os: TransformedOperatingSystem) => {
+ const osCopy = cloneDeep(operatingSystems);
+ if (osCopy?.[selectedVersion.id]) {
+ const matchingOs = osCopy?.[selectedVersion.id].find(
+ (oSystem) => oSystem.os === os.os
+ );
+ if (matchingOs) {
+ matchingOs.isAdded = true;
+ setOperatingSystems(osCopy);
+ }
+ }
+ };
+
+ const osListHeaders = [
+ {
+ label: "OS",
+ columnCount: 3,
+ },
+ {
+ label: "name",
+ columnCount: 3,
+ },
+ { label: "Base URL", columnCount: 5 },
+ {
+ columnCount: 1,
+ label: (
+ <Dropdown>
+ <Dropdown.Toggle
+ className="btn-default"
+ as={Button}
+ variant="secondary"
+ size="sm"
+ disabled={getAddableOperatingSystems()?.length === 0}
+ >
+ <FontAwesomeIcon className="me-2" icon={faAdd} />
+ Add
+ </Dropdown.Toggle>
+
+ <Dropdown.Menu>
+ {getAddableOperatingSystems()?.map((oSystem) => {
+ return (
+ <Dropdown.Item
+ onClick={() => {
+ addBackOperatingSystem(oSystem);
+ }}
+ key={oSystem.os}
+ className="text-dark"
+ >
+ {oSystem.os}
+ </Dropdown.Item>
+ );
+ })}
+ </Dropdown.Menu>
+ </Dropdown>
+ ),
+ },
+ ];
+
+ function editAllRepos(key: string, value: any) {
+ const operatingSystemsCopy = cloneDeep(operatingSystems);
+ const allOs = operatingSystemsCopy?.[selectedVersion.id];
+ allOs?.map((os: TransformedOperatingSystem) => {
+ os.repos.map((repo) => {
+ //@ts-ignore
+ repo[key as keyof TransformedRepo] = value;
+ return repo;
+ });
+ return os;
+ });
+ return operatingSystemsCopy;
+ }
+
+ function editOsOrRepo(
+ operatingSystem: string,
+ repoId: string,
+ key: string,
+ value: any
+ ) {
+ const operatingSystemsCopy = cloneDeep(operatingSystems);
+ const matchingOperatingSystem = find(
+ operatingSystemsCopy?.[selectedVersion.id],
+ {
+ os: operatingSystem,
+ }
+ );
+
+ if (matchingOperatingSystem) {
+ if (repoId) {
+ const matchingRepo = find(matchingOperatingSystem.repos, {
+ id: repoId,
+ });
+
+ if (matchingRepo) {
+ //@ts-ignore
+ matchingRepo[key as keyof TransformedRepo] =
+ value as TransformedRepo[keyof TransformedRepo];
+ matchingRepo.hasError = false;
+ setOperatingSystems(operatingSystemsCopy);
+ }
+ } else {
+ set(matchingOperatingSystem, key, value);
+ setOperatingSystems(operatingSystemsCopy);
+ }
+ }
+ }
+
+ // async function saveVersion() {
+ // let createdVersionDefinition: any = {};
+
+ // try {
+ // createdVersionDefinition = await VersionsApi.readVersionInfo(
+ // {
+ // VersionDefinition: {
+ // available: selectedStack.id,
+ // display_name: `${selectedStack.id}.${versionNumber}`,
+ // },
+ // },
+ // {},
+ // false
+ // );
+ // } catch (err) {
+ // toast.error("Could not read version info");
+ // }
+ // const addedOs = operatingSystems?.[selectedVersion.id]?.filter(
+ // (os: TransformedOperatingSystem) => os.isAdded
+ // );
+ // let payload = {};
+
+ // payload = {
+ // ...{
+ // operating_systems: addedOs?.map((os:
TransformedOperatingSystem) => {
+ // return {
+ // OperatingSystems: {
+ // os_type: os.os,
+ // ambari_managed_repositories: skipValidation,
+ // stack_name: selectedStack.stack_name,
+ // stack_version: selectedStack.stack_version,
+ // ...({ version_defintion_id: selectedStack.id }),
+ // },
+ // repositories: os.repos.map((repo: TransformedRepo) =>
{
+ // return {
+ // Repositories: {
+ // applicable_services: [],
+ // base_url: repo.baseUrl,
+ // components: null,
+ // default_base_url: "",
+ // distribution: null,
+ // intial_base_url: repo.defaultUrl,
+ // initial__repo_id: selectedStack.id,
+ // mirrors_list: null,
+ // os_type: os.os,
+ // stack_name: selectedStack.stack_name,
+ // stack_version:
selectedStack.stack_version,
+ // tags: [],
+ // unique: false,
+ // version_defintion_id: selectedStack.id,
+ // repo_id: repo.id,
+ // repo_name: repo.name,
+ // },
+ // hasError: false,
+ // invalidBaseUrl: false,
+ // };
+ // }),
+ // selected: true,
+ // };
+ // }),
+ // }};
+ // try {
+ // await VersionsApi.saveRepoVersions(
+ // selectedStack.stack_name,
+ // selectedStack.stack_version,
+ //
createdVersionDefinition?.resources?.[0]?.VersionDefinition?.id,
+ // payload
+ // );
+ // toast.success("Version saved successfully");
+ // // redirectToList();
+ // } catch (err: any) {
+ // const errorMessage = err.response.data.message;
+ // if (
+ // errorMessage.includes(
+ // "is already defined for another repository version"
+ // )
+ // ) {
+ // setShowRegistrationModal(true);
+ // await VersionsApi.deleteRepositoryVersion(
+ // selectedStack.stack_name,
+ // selectedStack.stack_version,
+ //
createdVersionDefinition?.resources?.[0]?.VersionDefinition?.id
+ // );
+ // }
+ // // toast.error("Could not save version");
+ // }
+ // }
+
+ async function validateRepos() {
+ if (!isAllOsValidated()) {
+ return;
+ }
+ if (skipValidation) {
+ // saveVersion();
+ return true;
+ } else {
+ const operatingSystemsCopy = cloneDeep(operatingSystems);
+ let allOsValidated = true;
+ const versionValidationPromises = [];
+ const allAddedOs = operatingSystemsCopy?.[selectedVersion.id].filter(
+ (oSystem) => oSystem.isAdded
+ );
+ for (const oSystem of allAddedOs) {
+ const repos = oSystem.repos;
+ for (const repo of repos) {
+ versionValidationPromises.push(
+ VersionsApi.validateRepos(
+ selectedStack.stack_version,
+ selectedStack.stack_version,
+ oSystem.os,
+ repo.id,
+ {
+ base_url: repo.baseUrl,
+ repo_name: repo.name,
+ }
+ )
+ );
+ }
+ }
+ try {
+ const validationResponses = await Promise.allSettled(
+ versionValidationPromises
+ );
+ //In the response array map the response operation to corresponding
repo via matching index
+ //If the response at nth index is empty then the nth repo is valid add
a key called hasError false
+ //If the response at nth index is not empty then the nth repo is
invalid add a key called hasError true
+ const osWithValidationStatus = allAddedOs.map((os, osIndex) => {
+ os.repos.map((repo, repoIndex) => {
+ const validationResult = validationResponses[osIndex *
os.repos.length + repoIndex];
+ if (validationResult?.status === "rejected") {
+ allOsValidated = false;
+ repo.hasError = true;
+ /* START GENAI */
+ // Show error message to user when validation fails (Fix for
TLHASD-1257)
+ const errorReason = validationResult.reason?.message ||
validationResult.reason || "Invalid Base URL";
+ toast.error(`Repository validation failed for ${repo.name}
(${os.os}): ${errorReason}`);
+ /* END GENAI */
+ } else {
+ repo.hasError = false;
+ }
+ return repo;
+ });
+ return os;
+ });
+ const osCopy = cloneDeep(operatingSystems);
+ osCopy[selectedVersion.id].map((oSystem) => {
+ const matchingOs = osWithValidationStatus.find(
+ (os) => os.os === oSystem.os
+ );
+ if (matchingOs) {
+ oSystem.repos = matchingOs.repos;
+ }
+ return oSystem;
+ });
+ if (!allOsValidated) {
+ setShowRepoValidationBanner(true);
+ } else {
+ setShowRepoValidationBanner(false);
+ // saveVersion();
+ }
+ setOperatingSystems({
+ ...operatingSystems,
+ [selectedVersion.id]: osCopy[selectedVersion.id],
+ });
+ return allOsValidated;
+ } catch (error) {
+ console.log("Error", error);
+ }
+ }
+ }
+
+ const readVersionCallback = async (versionResources: any) => {
+ try {
+ const addedVersionOperatingSystems =
+ versionResources?.resources?.[0]?.operating_systems;
+ const addedVersion = versionResources?.resources?.[0]?.VersionDefinition;
+
+ const versionOperatingSystems =
+ await VersionsApi.getVersionOperatingSystems(
+ selectedStack.stack_version
+ );
+ const newVersion = {
+ label: `${addedVersion.stack_name}-${addedVersion.repository_version}`,
+ value: {
+ ...addedVersion,
+ id: addedVersion.repository_version,
+ defaultId: addedVersion.repository_version,
+ },
+ };
+ const stackVersion = addedVersion.stack_version;
+ const belongingStack = versionDefinitions.find((definition) => {
+ return definition.VersionDefinition.stack_version === stackVersion;
+ });
+ const addedOperatingSystems = addedVersionOperatingSystems.map(
+ (os: any) => {
+ return os.OperatingSystems.os_type;
+ }
+ );
+ const allOs: TransformedOperatingSystem[] =
+ versionOperatingSystems.operating_systems.map((os: any) => {
+ const matchingOs =
+ versionResources?.resources?.[0]?.operating_systems.find(
+ (oS: any) => {
+ return (
+ oS.OperatingSystems.os_type === os.OperatingSystems.os_type
+ );
+ }
+ );
+ return {
+ os: os.OperatingSystems.os_type,
+ isAdded:
addedOperatingSystems.includes(os.OperatingSystems.os_type)
+ ? true
+ : false,
+ repos: (addedOperatingSystems.includes(os.OperatingSystems.os_type)
+ ? matchingOs
+ : os
+ ).repositories.map((repo: any) => {
+ return {
+ id: repo?.Repositories?.repo_id,
+ defaultId: repo?.Repositories?.repo_id,
+ baseUrl: repo?.Repositories?.base_url,
+ name: repo?.Repositories?.repo_name,
+ defaultUrl: repo?.Repositories?.default_base_url || "",
+ };
+ }),
+ };
+ });
+
+ const operatingSystemsCopy = cloneDeep(operatingSystems);
+ operatingSystemsCopy[addedVersion.repository_version] = [...allOs];
+ setOperatingSystems(operatingSystemsCopy);
+
+ setSelectedStack(belongingStack?.VersionDefinition as VersionDefinition);
+ selectNewVersion(
+ belongingStack?.VersionDefinition as VersionDefinition,
+ newVersion,
+ {
+ ...addedVersion,
+ id: addedVersion.repository_version,
+ }
+ );
+ const addedVersionName = addedVersion.repository_version.split(".");
+ setVersionNumber(
+ addedVersionName.splice(2, addedVersionName.length).join(".")
+ );
+ setShowAddVersionModal(false);
+ } catch (err) {
+ toast.error("Could not read version");
+ console.log("Error", err);
+ }
+ };
+
+ // const readOnlyRepoProperties = [
+ // {
+ // label: "Stack",
+ // value:
`${savedRepositoryVersionDetails?.stack_name}-${savedRepositoryVersionDetails?.stack_version}`,
+ // },
+ // {
+ // label: "Name",
+ // value: savedRepositoryVersionDetails?.display_name,
+ // },
+ // {
+ // label: "Version",
+ // value: savedRepositoryVersionDetails?.repository_version,
+ // },
+ // ];
+
+ return (
+ <>
+ <AddVersionModal
+ isOpen={showAddVersionModal}
+ onReadVersion={readVersionCallback}
+ onClose={() => {
+ setShowAddVersionModal(false);
+ }}
+ />
+ <LostNetworkModal
+ isOpen={showNetworkModal}
+ onClose={() => {
+ handleModalVisibility(false);
+ }}
+ ></LostNetworkModal>
+ <RedhatSatelliteUsageInfo
+ isOpen={showRedhatInfoModal}
+ onCancel={() => {
+ setShowRedhatInfoModal(false);
+ setRedhatSatellite(false);
+ }}
+ onClose={() => {
+ setShowRedhatInfoModal(false);
+ }}
+ />
+ <ConfirmationModal
+ modalTitle="Unable to Register"
+ modalBody="You are attempting to register a version with a Base URL
that is already in use with an existing registered version. You *must* review
your Base URLs and confirm they are unique for the version you are trying to
register."
+ isOpen={showRegistrationModal}
+ successCallback={() => {
+ wizardSteps;
+ setShowRegistrationModal(false);
+ }}
+ onClose={() => {
+ setShowRegistrationModal(false);
+ }}
+ />
+ <div className="step-title">Select Version</div>
+ <div className="step-description mt-2">
+ Select the software version and method of delivery for your cluster.
+ </div>
+ <Card className="mt-2">
+ <CardBody>
+ <Tab.Container className="p-0">
+ <Col sm={12}>
+ <Nav variant="pills">
+ {versionDefinitions.map((definition) => {
+ return (
+ <Nav.Item
+ onClick={() => {
+ selectNewVersion(definition.VersionDefinition);
+ setSelectedStack(definition.VersionDefinition);
+ }}
+ className={`my-2 ${
+ selectedStack.id === definition.VersionDefinition.id
+ ? "border-bottom border-2 border-success"
+ : "muted-text"
+ }`}
+ >
+ <Nav.Link
+ className={`nowrap text-decoration-none ${
+ selectedStack.id === definition.VersionDefinition.id
+ ? "text-dark"
+ : "muted-text"
+ } `}
+ >
+ {definition.VersionDefinition.id}
+ </Nav.Link>
+ </Nav.Item>
+ );
+ })}
+ </Nav>
+ </Col>
+ </Tab.Container>
+ <Col className="mx-4 mt-2" style={{ maxHeight: "100%" }}>
+ <div className="d-flex justify-content-between">
+ <Dropdown>
+ <DropdownButton
+ size="sm"
+ variant="outline-secondary"
+ id="dropdown-basic"
+ data-testid="version-dropdown"
+ title={
+ selectedVersion?.id || selectedVersion?.repository_version
+ }
+ >
+ {addedVersions[selectedStack?.id]
+ ? addedVersions?.[selectedStack?.id]?.map((vers) => {
+ return (
+ <Dropdown.Item
+ key={vers.label}
+ className="text-dark"
+ onClick={() => {
+ setSelectedVersion(vers.value);
+ }}
+ >
+ {vers.label}
+ </Dropdown.Item>
+ );
+ })
+ : null}
+ <Dropdown.Item
+ className="text-dark"
+ data--testid="add-version-option"
+ onClick={() => {
+ setShowAddVersionModal(true);
+ }}
+ >
+ Add Version
+ </Dropdown.Item>
+ </DropdownButton>
+ </Dropdown>
+ </div>
+ <div className="border mt-2">
+ <Table
+ scrollable
+ maxHeight="30vh"
+ columns={getColumns()}
+ data={
+ (versionDefinitions?.find(
+ (definition) =>
+ definition.VersionDefinition.stack_version ===
+ selectedVersion.stack_version
+ )?.VersionDefinition.stack_services as unknown[]) || []
+ }
+ />
+ </div>
+ </Col>
+ <div className="mx-4 mt-4">
+ <h1>Repositories</h1>
+ <p>
+ Using a Public Repository requires Internet connectivity. Using a
+ Local Repository requires you have configured the software in a
+ repository available in your network.
+ </p>
+ <Row className="align-items-center mt-4">
+ <Col sm={4} className="d-flex mt-">
+ <Form.Check
+ type="radio"
+ disabled={networkLost}
+ label="Use Public repository"
+ checked={selectedChoice === RepositoryType.PUBLIC}
+ onClick={() => {
+ setSelectedChoice(RepositoryType.PUBLIC);
+ }}
+ />
+ <div
+ className="ms-1 custom-link cursor-pointer"
+ onClick={() => {
+ handleModalVisibility(true);
+ }}
+ >
+ Why is this not Selected?
+ </div>
+ </Col>
+ <Col className="d-flex align-items-center">
+ <Form.Check
+ checked={selectedChoice === RepositoryType.LOCAL}
+ type="radio"
+ label="Use Local repository"
+ onClick={() => {
+ setSelectedChoice(RepositoryType.LOCAL);
+ }}
+ />
+ </Col>
+ </Row>
+ <Alert className="mt-2" variant="info">
+ Provide Base URLs for the Operating Systems you are configuring.
+ </Alert>
+ {selectedChoice === RepositoryType.LOCAL ? (
+ <Alert variant="warning">
+ Attention: Repository Base URLs of at least one OS are REQUIRED
+ before you can proceed. Please make sure they are in correct
+ format with its protocol.
+ </Alert>
+ ) : null}
+ </div>
+ <Row className=" mx-2">
+ <div className="p-3">
+ <Row className="align-items-center py-3 border-bottom">
+ {osListHeaders.map((header) => {
+ return <Col md={header.columnCount}>{header.label}</Col>;
+ })}
+ </Row>
+ {operatingSystems?.[selectedVersion.id]
+ ?.filter(
+ (oSystem: TransformedOperatingSystem) => oSystem.isAdded
+ )
+ ?.map((oSystem: any) => {
+ return (
+ <Row
+ className="border-bottom py-4"
+ data-testid="operating-systems"
+ >
+ <Col md={3}>{oSystem.os}</Col>
+ <Col md={8}>
+ {oSystem?.repos?.map((repo: any, index: any) => {
+ return (
+ <Row
+ className={`d-flex align-items-center nowrap ${
+ index > 0 ? "mt-4" : ""
+ }`}
+ >
+ {" "}
+ <Col
+ md={4}
+ className={`d-flex align-items-center ${
+ repo.hasError ? "text-danger" : ""
+ }`}
+ >
+ {repo.isEditing ? (
+ <Form
+ onSubmit={(e) => {
+ e.preventDefault();
+ editOsOrRepo(
+ oSystem.os,
+ repo.id,
+ "isEditing",
+ false
+ );
+ }}
+ >
+ <Form.Control
+ value={repo.id}
+ onChange={(e) => {
+ editOsOrRepo(
+ oSystem.os,
+ repo.id,
+ "id",
+ e.target.value
+ );
+ }}
+ type="text"
+ ></Form.Control>
+ </Form>
+ ) : (
+ repo.id
+ )}
+ {redhatSatellite && !repo.isEditing ? (
+ <FontAwesomeIcon
+ className="ms-2"
+ icon={faPencil}
+ onClick={() => {
+ editOsOrRepo(
+ oSystem.os,
+ repo.id,
+ "isEditing",
+ true
+ );
+ }}
+ />
+ ) : null}
+ {repo.isEditing &&
+ repo.id !== repo.defaultId ? (
+ <FontAwesomeIcon
+ icon={faUndo}
+ className="text-warning ms-2"
+ onClick={() => {
+ editOsOrRepo(
+ oSystem.os,
+ repo.id,
+ "id",
+ repo.defaultId
+ );
+ }}
+ ></FontAwesomeIcon>
+ ) : null}
+ </Col>
+ <Col md={8} className="d-flex
align-items-center">
+ <Form.Control
+ onChange={(e) => {
+ editOsOrRepo(
+ oSystem.os,
+ repo.id,
+ "baseUrl",
+ e.target.value
+ );
+ }}
+ value={repo.baseUrl}
+ type="text"
+ className={`${
+ repo.hasError ? "border border-danger" : ""
+ }`}
+ disabled={repo.isEditable === false}
+ placeholder="Enter Base URL or remove this
OS"
+ ></Form.Control>
+ {repo.baseUrl !== repo.defaultUrl ? (
+ <FontAwesomeIcon
+ icon={faUndo}
+ className="text-warning ms-2
cursor-pointer"
+ onClick={() => {
+ editOsOrRepo(
+ oSystem.os,
+ repo.id,
+ "baseUrl",
+ repo.defaultUrl
+ );
+ }}
+ />
+ ) : (
+ <div className="ms-4"></div>
+ )}
+ </Col>
+ </Row>
+ );
+ })}
+ </Col>
+ <Col md={1} className="mt-2">
+ <div
+ className="text-danger cursor-pointer"
+ onClick={() => {
+ editOsOrRepo(oSystem.os, "", "isAdded", false);
+ }}
+ >
+ <FontAwesomeIcon
+ icon={faMinus}
+ className="me-2 cursor-pointer"
+ />
+ Remove
+ </div>
+ </Col>
+ </Row>
+ );
+ })}
+ </div>
+ <div className="repo-configs">
+ <Form>
+ <Form.Check
+ checked={skipValidation}
+ type="checkbox"
+ id="skipValidation"
+ onChange={() => {
+ setSkipValidation(!skipValidation);
+ }}
+ label={
+ <div style={{ marginTop: 2 }}>
+ Skip Repository Base URL validation (Advanced)
+ <OverlayTrigger
+ placement="right"
+ delay={{ show: 250, hide: 400 }}
+ overlay={
+ <Tooltip>
+ Warning! This is for advanced users only. Use this
+ optionif you want to skip validation for Repository
+ Base URLs.
+ </Tooltip>
+ }
+ >
+ <FontAwesomeIcon
+ className="ms-2"
+ icon={faQuestionCircle}
+ />
+ </OverlayTrigger>
+ </div>
+ }
+ ></Form.Check>
+ </Form>
+ <Form.Check
+ type="checkbox"
+ disabled={selectedChoice === RepositoryType.PUBLIC}
+ checked={redhatSatellite}
+ id="disableLocal"
+ onChange={() => {
+ setRedhatSatellite(!redhatSatellite);
+ }}
+ label={
+ <div style={{ marginTop: 2 }}>
+ Use RedHat Satellite/Spacewalk
+ <OverlayTrigger
+ placement="right"
+ delay={{ show: 250, hide: 400 }}
+ overlay={
+ <Tooltip>
+ Disable distributed repositories and use RedHat
+ Satellite/Spacewalk channels instead
+ </Tooltip>
+ }
+ >
+ <FontAwesomeIcon
+ className="ms-2"
+ icon={faQuestionCircle}
+ />
+ </OverlayTrigger>
+ </div>
+ }
+ ></Form.Check>
+ </div>
+ </Row>
+ </CardBody>
+ </Card>
+ <WizardFooter
+ lifted
+ isNextEnabled={nextEnabled}
+ step={currentStep}
+ onNext={async () => {
+ const allValidated = await validateRepos();
+ if (!allValidated) {
+ disableNext();
+ } else {
+ const operatingSystemsCopy = cloneDeep(operatingSystems);
+ const versionUpdatePromises = [];
+ const allAddedOs = operatingSystemsCopy?.[
+ selectedVersion.id
+ ].filter((oSystem) => oSystem.isAdded);
+ for (const oSystem of allAddedOs) {
+ const repos = oSystem.repos;
+ for (const repo of repos) {
+ versionUpdatePromises.push(
+ VersionsApi.updateOSInfo(
+ selectedStack.stack_name,
+ selectedStack.stack_version,
+ oSystem.os,
+ repo.id,
+ {
+ Repositories: {
+ base_url: repo.baseUrl,
+ repo_name: repo.name,
+ verify_base_url: true,
+ },
+ }
+ )
+ );
+ }
+ }
+ Promise.allSettled(versionUpdatePromises).then(() => {
+ /* START GENAI */
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: {
+ selectedVersion,
+ selectedStack,
+ selectedChoice,
+ skipValidation,
+ redhatSatellite,
+ operatingSystems,
+ addedVersions,
+ },
+ },
+ });
+ /* END GENAI */
+ flushStateToDb("next");
+ handleNextImperitive();
+ });
+ }
+ }}
+ onCancel={() => {
+ flushStateToDb("cancel");
+ }}
+ onBack={() => {
+ flushStateToDb("back");
+ handleBackImperitive();
+ }}
+ />
+ </>
+ );
+}
diff --git a/ambari-web/latest/src/screens/ClusterWizard/Step4.tsx
b/ambari-web/latest/src/screens/ClusterWizard/Step4.tsx
new file mode 100644
index 0000000000..1fc50a66c1
--- /dev/null
+++ b/ambari-web/latest/src/screens/ClusterWizard/Step4.tsx
@@ -0,0 +1,623 @@
+/**
+ * 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, useState } from "react";
+import Table from "../../components/Table";
+import { cloneDeep, forEach, get, isEmpty, map } from "lodash";
+import { Form } from "react-bootstrap";
+import MissingServiceModal from "../../components/MissingServiceModal";
+import {
+ dfsServices,
+ displayOrder,
+ coSelectedServices,
+ excludeServicesOnDisplay,
+ ModalType,
+ warnningMessages,
+} from "./constants";
+import WizardFooter from "../../components/StepWizard/WizardFooter";
+import { ActionTypes } from "./clusterStore/types";
+import { getStepData } from "../../Utils/Utility";
+import { ContextWrapper } from ".";
+import { AppContext } from "../../store/context";
+import Spinner from "../../components/Spinner";
+import { ChooseServicesApi } from "../../api/chooseServicesApi";
+
+type Service = {
+ displayName: string;
+ serviceName: string;
+ serviceType: string;
+ version: string;
+ comments: string;
+ selected: boolean;
+ required: string[];
+ isIgnored?: boolean;
+ isHiddenOnDisplay: boolean;
+};
+
+type ErrorType = {
+ serviceName: string;
+ modalType: string;
+};
+
+export default function Step4({ wizardName = "clusterCreation" }) {
+ const [, setServicesFromApi] = useState<any>([]);
+ const [services, setServices] = useState<{ [key: string]: Service }>({});
+ const [errorStack, setErrorStack] = useState<ErrorType[]>([]);
+ const [showModal, setShowModal] = useState<boolean>(false);
+ const [nextDisabled, setNextDisabled] = useState<boolean>(false);
+ const { Context } = useContext(ContextWrapper);
+ const {
+ state,
+ dispatch,
+ flushStateToDb,
+ serviceContextLoading = false,
+ handleBackImperitive,
+ installedServices: installedServicesProps = [],
+ stepWizardUtilities: { currentStep, handleNextImperitive },
+ }: any = useContext(Context);
+ const { services: servicesContext } = useContext(AppContext);
+ let installedServices = installedServicesProps;
+ if (wizardName !== "clusterCreation") {
+ installedServices = map(servicesContext, "ServiceInfo.service_name");
+ }
+ const stepData = getStepData(state, currentStep.name, "");
+
+ const versionStepData = get(state, `${wizardName}Steps.VERSION.data`, {});
+ const version = get(versionStepData, "selectedVersion.stack_version", "");
+ const stack = get(versionStepData, "selectedStack.stack_name", "");
+
+ const isDFS = (serviceName: string) => {
+ return dfsServices.includes(serviceName);
+ };
+
+ const selectAllServices = () => {
+ const updatedServices = cloneDeep(services);
+ const allSelected = isAllServicesSelected();
+ Object.values(updatedServices).forEach((service: Service) => {
+ if (!isDFS(service.serviceName)) {
+ service.selected = isServiceSelected(service.serviceName)
+ ? true
+ : !allSelected;
+ }
+ if (service.serviceName === "KERBEROS") {
+ service.selected = false;
+ }
+ });
+ setServices(updatedServices);
+ };
+
+ const isAllServicesSelected = () => {
+ for (const service of Object.values(services)) {
+ if (
+ !isDFS(service.serviceName) &&
+ service.serviceName !== "KERBEROS" &&
+ !service.selected
+ ) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ const handleCheckboxChange = (serviceName: string) => {
+ const updatedServices = cloneDeep(services);
+ updatedServices[serviceName].selected =
+ !updatedServices[serviceName].selected;
+
+ if (coSelectedServices[serviceName]) {
+ for (const coSelectedService of coSelectedServices[serviceName]) {
+ updatedServices[coSelectedService].selected =
+ updatedServices[serviceName].selected;
+ }
+ }
+ setServices(updatedServices);
+ };
+
+ const validateSelectedServices = () => {
+ const selectedServices = Object.values(services).filter(
+ (service) => service.selected === true
+ );
+
+ const newErrorStack: ErrorType[] = [];
+
+ fileSystemServiceValidation(selectedServices, newErrorStack);
+
+ for (const selectedService of selectedServices) {
+ selectedService?.required?.forEach((requiredService) => {
+ dependantServiceValidation(
+ selectedServices,
+ requiredService,
+ newErrorStack
+ );
+ });
+ }
+
+ serviceValidation("RANGER", newErrorStack);
+ serviceValidation("AMBARI_METRICS", newErrorStack);
+
+ setErrorStack(newErrorStack);
+ if (newErrorStack.length > 0) {
+ setShowModal(true);
+ } else {
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: {
+ services,
+ },
+ },
+ });
+ flushStateToDb("next");
+ handleNextImperitive();
+ }
+ };
+
+ const dependantServiceValidation = (
+ selectedServices: Service[],
+ requiredService: string,
+ errorStackCopy: ErrorType[]
+ ) => {
+ if (
+ !selectedServices.find(
+ (service) => service.serviceName === requiredService
+ ) &&
+ !errorStackCopy.find((error) => error.serviceName === requiredService)
+ ) {
+ errorStackCopy.push({
+ serviceName: requiredService,
+ modalType: ModalType.MISSING_DEPENDANT_SERVICE,
+ });
+ }
+ };
+
+ const serviceValidation = (service: string, errorStackCopy: ErrorType[]) => {
+ if (
+ services[service].selected === false &&
+ !errorStackCopy.find(
+ (missingService) => missingService.serviceName === service
+ ) &&
+ !services[service].isIgnored
+ ) {
+ errorStackCopy.push({
+ serviceName: service,
+ modalType: ModalType.MISSING_SERVICE,
+ });
+ }
+ };
+
+ const fileSystemServiceValidation = (
+ selectedServices: Service[],
+ errorStackCopy: ErrorType[]
+ ) => {
+ const selectedFileSystems = selectedServices.filter((service) =>
+ dfsServices.includes(service.serviceName)
+ );
+ if (selectedFileSystems.length === 0) {
+ errorStackCopy.push({
+ serviceName: "HDFS",
+ modalType: ModalType.MISSING_FILE_SYSTEM,
+ });
+ }
+ };
+
+ const handleCloseAddServiceModal = () => {
+ handleCheckboxChange(errorStack[0].serviceName);
+ const updatedErrorStack = errorStack.slice(1);
+ setErrorStack(updatedErrorStack);
+ setShowModal(updatedErrorStack.length > 0);
+ };
+
+ const handleCloseLimitiedFunctionalityModal = () => {
+ services[errorStack[0].serviceName].isIgnored = true;
+ const updatedErrorStack = errorStack.slice(1);
+ setErrorStack(updatedErrorStack);
+ setShowModal(updatedErrorStack.length > 0);
+ };
+
+ const combineCoSelectedServices = (services: { [key: string]: Service }) => {
+ Object.keys(coSelectedServices).forEach((service) => {
+ forEach(coSelectedServices[service], (coSelectedService) => {
+ services[service].displayName =
+ services[service].displayName +
+ " + " +
+ services[coSelectedService].displayName;
+ services[coSelectedService].isHiddenOnDisplay = true;
+ });
+ });
+ };
+
+ const isServiceSelected = (serviceName: string) => {
+ let isSelected = true;
+ if (wizardName === "addService") {
+ if (
+ installedServices?.length &&
+ installedServices?.includes(serviceName)
+ ) {
+ isSelected = true;
+ } else {
+ isSelected = false;
+ }
+ return isSelected;
+ }
+ };
+
+ const canToggleServiceSelection = (serviceName: string) => {
+ let canToggle = true;
+ if (wizardName === "addService") {
+ if (
+ installedServices?.length &&
+ installedServices?.includes(serviceName)
+ ) {
+ canToggle = false;
+ } else {
+ canToggle = true;
+ }
+ return canToggle;
+ }
+ return canToggle;
+ };
+
+ useEffect(() => {
+ const fetchServicesData = async () => {
+ try {
+ const chooseServices = await ChooseServicesApi.getServices(
+ stack,
+ version
+ );
+ const transformedData: { [key: string]: any } = {};
+ chooseServices.items.forEach((service: any) => {
+ transformedData[service.StackServices.service_name] = {
+ displayName: service.StackServices.display_name,
+ serviceName: service.StackServices.service_name,
+ serviceType: service.StackServices.service_type,
+ version: service.StackServices.service_version,
+ comments: service.StackServices.comments,
+ selected: isServiceSelected(service.StackServices.service_name),
+ required: service.StackServices.required_services,
+ isIgnored: false,
+ installed: installedServices.includes(
+ service.StackServices.service_name
+ ),
+ canToggle: canToggleServiceSelection(
+ service.StackServices.service_name
+ ),
+ isHiddenOnDisplay: excludeServicesOnDisplay.includes(
+ service.StackServices.service_name
+ ),
+ };
+ });
+ combineCoSelectedServices(transformedData);
+
+ const sortedServices = Object.keys(transformedData)
+ .sort((a, b) => displayOrder.indexOf(a) - displayOrder.indexOf(b))
+ .reduce((acc, key) => {
+ acc[key] = transformedData[key];
+ return acc;
+ }, {} as { [key: string]: Service });
+
+ setServicesFromApi(chooseServices);
+ setServices(sortedServices);
+
+ // Handle pre-selection from localStorage (for Add Service from Stack
and Versions page)
+ if (wizardName === "addService") {
+ const preselectedService =
localStorage.getItem('preselectedService');
+
+ if (preselectedService && sortedServices[preselectedService]) {
+ const updatedServices = cloneDeep(sortedServices);
+ updatedServices[preselectedService].selected = true;
+
+ // Also select any co-selected services
+ if (coSelectedServices[preselectedService]) {
+ for (const coSelectedService of
coSelectedServices[preselectedService]) {
+ if (updatedServices[coSelectedService]) {
+ updatedServices[coSelectedService].selected = true;
+ }
+ }
+ }
+
+ setServices(updatedServices);
+
+ // Clear the localStorage item after using it
+ localStorage.removeItem('preselectedService');
+ }
+ }
+ } catch (error) {
+ console.error("Error fetching services data:", error);
+ }
+ };
+ if (!stepData.services&&stack&&version) fetchServicesData();
+ }, [serviceContextLoading,stack,version]);
+
+
+ useEffect(() => {
+ const isNextDisabled = () => {
+ if (wizardName === "addService") {
+ if (
+ Object.values(services).filter((service) => service.selected ===
true)
+ .length === installedServices.length
+ ) {
+ return true;
+ }
+ }
+ return (
+ Object.values(services).filter((service) => service.selected === true)
+ .length === 0
+ );
+ };
+
+ setNextDisabled(isNextDisabled());
+ }, [services]);
+
+ useEffect(() => {
+ console.log("Step Data is", stepData);
+ if (!isEmpty(stepData)) {
+ setServices(stepData.services);
+ }
+ }, []);
+
+ const fileSystemColumns = [
+ {
+ header: " ",
+ cell: ({ row }: any) => {
+ const checkboxId =
`filesystem-step4-checkbox-${row.original.serviceName}`;
+ return (
+ <Form.Check
+ type="checkbox"
+ id={checkboxId}
+ checked={row.original.selected}
+ onChange={() => handleCheckboxChange(row.original.serviceName)}
+ />
+ );
+ },
+ width: "5%",
+ },
+ {
+ header: "Service",
+ accessorKey: "displayName",
+ width: "20%",
+ cell: ({ row }: any) => {
+ return (
+ <span
+ className="cursor-pointer"
+ onClick={() => handleCheckboxChange(row.original.serviceName)}
+ >
+ {row.original.displayName}
+ </span>
+ );
+ },
+ },
+ {
+ header: "Version",
+ accessorKey: "version",
+ width: "10%",
+ },
+ {
+ header: "Description",
+ accessorKey: "comments",
+ width: "65%",
+ },
+ ];
+
+ const servicesColumns = [
+ {
+ header: () => (
+ <Form.Check
+ type="checkbox"
+ id="select-all-services-step4"
+ checked={isAllServicesSelected()}
+ onChange={selectAllServices}
+ />
+ ),
+ id: "selectAllCheckcbox",
+ cell: ({ row }: any) => {
+ const checkboxId =
`service-step4-checkbox-${row.original.serviceName}`;
+ return (
+ <Form.Check
+ type="checkbox"
+ id={checkboxId}
+ checked={row.original.selected}
+ onChange={() => {
+ if (row.original.canToggle) {
+ handleCheckboxChange(row.original.serviceName);
+ }
+ }}
+ />
+ );
+ },
+ width: "5%",
+ },
+ {
+ header: "Service",
+ accessorKey: "displayName",
+ width: "20%",
+ cell: ({ row }: any) => {
+ return (
+ <span
+ className="cursor-pointer"
+ onClick={() => {
+ if (row.original.canToggle) {
+ handleCheckboxChange(row.original.serviceName);
+ }
+ }}
+ >
+ {row.original.displayName}
+ </span>
+ );
+ },
+ },
+ {
+ header: "Version",
+ accessorKey: "version",
+ width: "10%",
+ },
+ {
+ header: "Description",
+ accessorKey: "comments",
+ width: "65%",
+ },
+ ];
+
+ const renderModalTitle = (modalType: string, serviceName: string) => {
+ switch (modalType) {
+ case ModalType.MISSING_DEPENDANT_SERVICE:
+ return serviceName + " Needed";
+ case ModalType.MISSING_SERVICE:
+ return "Limited Functionality Warning";
+ case ModalType.MISSING_FILE_SYSTEM:
+ return "A Hadoop Compatible File System Needed";
+ default:
+ return null;
+ }
+ };
+
+ const renderModalContent = (
+ modalType: string,
+ displayName: string,
+ serviceName: string
+ ) => {
+ switch (modalType) {
+ case ModalType.MISSING_DEPENDANT_SERVICE:
+ return (
+ <p>
+ You did not select {displayName}, but it is needed by other
services
+ you selected. We will automatically add {displayName}. Is this OK?
+ </p>
+ );
+ case ModalType.MISSING_SERVICE:
+ return (
+ <div>
+ <p>{displayName}</p>
+ <p>
+ {warnningMessages[serviceName as keyof typeof warnningMessages]}
+ </p>
+ </div>
+ );
+
+ case ModalType.MISSING_FILE_SYSTEM:
+ return (
+ <p>
+ You did not select a Hadoop Compatible File System, but it is
needed
+ by other services you selected. We will automatically add HDFS. Is
+ this OK?
+ </p>
+ );
+ default:
+ return null;
+ }
+ };
+
+
+ if (isEmpty(services)) {
+ return <Spinner />;
+ }
+
+ return (
+ <>
+ <div>
+ <div>
+ <div className="step-title">Choose File System</div>
+ <p className="step-description mt-1">
+ Choose which file system you want to install on your cluster.
+ </p>
+ <Table
+ data={Object.values(services).filter(
+ (service) =>
+ dfsServices.includes(service.serviceName) === true &&
+ service.isHiddenOnDisplay === false
+ )}
+ columns={fileSystemColumns}
+ />
+ <h1 className="step-title">Choose Services</h1>
+ <p className="step-description">
+ Choose which services you want to install on your cluster.
+ </p>
+ <Table
+ data={Object.values(services).filter(
+ (service) =>
+ dfsServices.includes(service.serviceName) === false &&
+ service.isHiddenOnDisplay === false
+ )}
+ columns={servicesColumns}
+ />
+ </div>
+ <div></div>
+ {errorStack.length > 0 && (
+ <>
+ <MissingServiceModal
+ isOpen={showModal}
+ onClose={() => {
+ if (errorStack[0].modalType === ModalType.MISSING_SERVICE) {
+ handleCloseLimitiedFunctionalityModal();
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: {
+ services,
+ },
+ },
+ });
+ flushStateToDb("next");
+ handleNextImperitive();
+ } else {
+ handleCloseAddServiceModal();
+ }
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: {
+ services,
+ },
+ },
+ });
+ }}
+ onCancel={() => setShowModal(false)}
+ title={renderModalTitle(
+ errorStack[0].modalType,
+ errorStack[0].serviceName
+ )}
+ body={renderModalContent(
+ errorStack[0].modalType,
+ services[errorStack[0]?.serviceName]?.displayName,
+ errorStack[0]?.serviceName
+ )}
+ modalType={errorStack[0].modalType}
+ />
+ </>
+ )}
+ </div>
+ <WizardFooter
+ step={currentStep}
+ lifted
+ onNext={() => {
+ validateSelectedServices();
+ }}
+ onCancel={() => {
+ flushStateToDb("cancel");
+ }}
+ onBack={() => {
+ flushStateToDb("back");
+ handleBackImperitive();
+ }}
+ isNextEnabled={!nextDisabled}
+ />
+ </>
+ );
+}
diff --git a/ambari-web/latest/src/screens/ClusterWizard/constants.ts
b/ambari-web/latest/src/screens/ClusterWizard/constants.ts
index 4b35585d80..5f0b15db1a 100644
--- a/ambari-web/latest/src/screens/ClusterWizard/constants.ts
+++ b/ambari-web/latest/src/screens/ClusterWizard/constants.ts
@@ -16,8 +16,62 @@
* limitations under the License.
*/
+export const dfsServices = ["HDFS", "GLUSTERFS"];
+export const displayOrder: string[] = [
+ "HDFS",
+ "GLUSTERFS",
+ "YARN",
+ "MAPREDUCE2",
+ "TEZ",
+ "GANGLIA",
+ "HIVE",
+ "HAWQ",
+ "PXF",
+ "HBASE",
+ "PIG",
+ "SQOOP",
+ "OOZIE",
+ "ZOOKEEPER",
+ "FALCON",
+ "STORM",
+ "FLUME",
+ "ACCUMULO",
+ "AMBARI_INFRA_SOLR",
+ "AMBARI_METRICS",
+ "ATLAS",
+ "KAFKA",
+ "KNOX",
+ "LOGSEARCH",
+ "RANGER",
+ "RANGER_KMS",
+ "SMARTSENSE",
+ "SPARK",
+ "SPARK2",
+ "ZEPPELIN",
+ "SPARK3",
+ "SSM",
+ "TRINO",
+];
+
+export const coSelectedServices: { [key: string]: string[] } = {
+ YARN: ["MAPREDUCE2"],
+};
+
export const excludeServicesOnDisplay: string[] = [
"KERBEROS",
"GANGLIA",
"MAPREDUCE2",
-];
\ No newline at end of file
+];
+
+export const warnningMessages: { [key: string]: string } = {
+ RANGER:
+ "Apache Ranger provides fine grained authorization and audit of access
attempts for many Hadoop ecosystem services. If you do not install the Apache
Ranger Service and enable Kerberos, the security of your cluster will be
diminished. Are you sure you want to proceed without it?",
+ AMBARI_METRICS:
+ "Ambari Metrics collects metrics from the cluster and makes them available
to Ambari. If you do not install Ambari Metrics service, metrics will not be
accessible from Ambari. Are you sure you want to proceed without Ambari
Metrics?",
+};
+
+export enum ModalType {
+ MISSING_DEPENDANT_SERVICE = "Missing Dependant Service",
+ MISSING_SERVICE = "Missing Service",
+ MISSING_FILE_SYSTEM = "Missing File System",
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]