diff --git a/libs/locales/lib/en/translation.json b/libs/locales/lib/en/translation.json index 75be7f1911..34abecffc2 100644 --- a/libs/locales/lib/en/translation.json +++ b/libs/locales/lib/en/translation.json @@ -169,7 +169,6 @@ "ai:Cluster installation failed": "Cluster installation failed", "ai:Cluster installation process": "Cluster installation process", "ai:Cluster installation was cancelled": "Cluster installation was cancelled.", - "ai:Cluster is ready for installation": "Cluster is ready for installation.", "ai:Cluster must have at least 3 hosts.": "Cluster must have at least 3 hosts.", "ai:Cluster name": "Cluster name", "ai:Cluster network CIDR": "Cluster network CIDR", @@ -350,7 +349,6 @@ "ai:Generating iPXE script": "Generating iPXE script", "ai:Go to cluster {{clusterName}}": "Go to cluster {{clusterName}}", "ai:Go to cluster configuration to start the installation": "Go to cluster configuration to start the installation.", - "ai:Go to cluster list": "Go to cluster list", "ai:Guest": "Guest", "ai:Hardware": "Hardware", "ai:Hardware information": "Hardware information", @@ -408,7 +406,6 @@ "ai:If the cluster hosts are in a network with a re-encrypting (MITM) proxy or the cluster needs to trust certificates for other purposes (e.g. container image registries).": "If the cluster hosts are in a network with a re-encrypting (MITM) proxy or the cluster needs to trust certificates for other purposes (e.g. container image registries).", "ai:If the configuration is taking longer than 5 minutes, you might need to troubleshoot.": "If the configuration is taking longer than 5 minutes, you might need to troubleshoot.", "ai:If there are many clusters, use higher values for the storage fields.": "If there are many clusters, use higher values for the storage fields.", - "ai:If you exit this flow you can see its status in the list view or details page.": "If you exit this flow, you can see its status in the list view or details page.", "ai:If you prefer using the CLI, follow the instructions in": "If you prefer using the CLI, follow the instructions in", "ai:If you used DHCP networking, verify that your DHCP server is enabled": "If you used DHCP networking, verify that your DHCP server is enabled", "ai:If you used static IP, bridges, and bonds networking, verify that your configurations are correct": "If you used static IP, bridges, and bonds networking, verify that your configurations are correct", @@ -651,7 +648,6 @@ "ai:Please select one host for the cluster.": "Select one host for the cluster.", "ai:Please wait till all checks are finished.": "Wait until all of the checks are finished.", "ai:Port of the NodePort service. If set to 0, the port is dynamically assigned when the service is created.": "Port of the NodePort service. If set to 0, the port is dynamically assigned when the service is created.", - "ai:Preparing cluster installation": "Preparing cluster installation", "ai:Preparing for installation": "Preparing for installation", "ai:Preparing step failed": "Preparing step failed", "ai:Preparing step successful": "Preparing step successful", @@ -710,7 +706,6 @@ "ai:Save": "Save", "ai:Saving changes...": "Saving changes...", "ai:Secret and keys": "Secret and keys", - "ai:See cluster details": "See cluster details", "ai:Select a state": "Select a state", "ai:Select all": "Select all", "ai:Select how you'd like to add hosts (Discovery ISO, iPXE, or BMC form) and follow the instructions that appear.": "Select the method of adding hosts (Discovery ISO, iPXE, or BMC form) and follow the instructions.", @@ -813,8 +808,6 @@ "ai:There is still {{count}} pending check_plural": "There are still {{count}} pending checks", "ai:There may be issues with the boot order": "There might be issues with the boot order", "ai:There was an error retrieving data. Check your connection and": "There was an error retrieving data. Check your connection and", - "ai:This cluster has been created and is ready to begin installation.": "This cluster has been created and is ready to install.", - "ai:This cluster is in the process of getting ready to start installation.": "This cluster is getting ready to install.", "ai:This host completed its installation successfully.": "This host completed its installation successfully.", "ai:This host does not meet the minimum hardware or networking requirements and can not be included in the cluster.": "This host does not meet the minimum hardware or networking requirements and can not be included in the cluster.", "ai:This host does not meet the minimum hardware or networking requirements and will not be included in the cluster.": "This host does not meet the minimum hardware or networking requirements and will not be included in the cluster.", @@ -849,7 +842,6 @@ "ai:To configure for disconnected environments, <2>view documentation <1>": "To configure for disconnected environments, <2>view documentation <1>", "ai:To enable the host's baseboard management controller (BMC) on the hub cluster, you must first <2>create a provisioning configuration.": "To enable the host's baseboard management controller (BMC) on the hub cluster, you must first <2>create a provisioning configuration.", "ai:To finish adding nodes to the cluster, approve the join request inside OpenShift Console's Nodes section.": "To finish adding nodes to the cluster, approve the join request inside OpenShift Console's Nodes section.", - "ai:To see the status of the installation see the details page.": "To see the status of the installation see the details page.", "ai:To use static network configuration, follow the steps listed in the documentation.": "To use static network configuration, follow the steps listed in the documentation.", "ai:To use this encryption method, enable TPMv2 encryption in the BIOS of each selected host.": "To use this encryption method, enable TPMv2 encryption in the BIOS of each selected host.", "ai:To verify that the agent ran successfully, check the logs:": "To verify that the agent ran successfully, check the logs:", @@ -890,6 +882,7 @@ "ai:Valid hostname": "Valid hostname", "ai:Valid network prefix": "Valid network prefix", "ai:Valid network type": "Valid network type", + "ai:Validating...": "Validating...", "ai:Validations are running. If they take more than 2 minutes, please attend to the alert below.": "Validations are running. If they take more than 2 minutes, resolve the alert that is displayed.", "ai:Verify that you can access your host machine using SSH, or a console such as BMC or virtual machine console. In the CLI, enter the following command:": "Verify that you can access your host machine using SSH, or a console such as BMC or virtual machine console. In the CLI, enter the following command:", "ai:View {{count}} affected host": "View {{count}} affected host", diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ACMClusterDeploymentDetailsStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ACMClusterDeploymentDetailsStep.tsx index 4eeea86448..06c4b6fe08 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ACMClusterDeploymentDetailsStep.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ACMClusterDeploymentDetailsStep.tsx @@ -1,7 +1,7 @@ +import React from 'react'; import { Formik, FormikProps, useFormikContext } from 'formik'; +import { Stack } from '@patternfly/react-core'; import noop from 'lodash-es/noop.js'; -import * as React from 'react'; -import { Ref } from 'react'; import { ClusterDetailsValues, getRichTextValidation } from '../../../common'; import { ClusterImageSetK8sResource } from '../../types/k8s/cluster-image-set'; import ClusterDeploymentDetailsForm from './ClusterDeploymentDetailsForm'; @@ -33,18 +33,20 @@ const DetailsFormBody: React.FC = ({ }, []); return ( - + + + ); }; type ACMClusterDeploymentDetailsStepProps = DetailsFormBodyProps & { usedClusterNames: string[]; - formRef: Ref>; + formRef: React.Ref>; }; const ACMClusterDeploymentDetailsStep: React.FC = ({ diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentCreateProgress.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentCreateProgress.tsx deleted file mode 100644 index 6ba1a18c21..0000000000 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentCreateProgress.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from 'react'; -import { CheckCircleIcon } from '@patternfly/react-icons/dist/js/icons/check-circle-icon'; -import { global_palette_green_500 as okColor } from '@patternfly/react-tokens/dist/js/global_palette_green_500'; -import { - Bullseye, - Button, - EmptyState, - EmptyStateBody, - EmptyStateIcon, - EmptyStateActions, - Spinner, - Title, -} from '@patternfly/react-core'; -import { isInstallationReady } from './helpers'; -import { AgentClusterInstallK8sResource } from '../../types'; -import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; - -const GreenCheckCircleIcon: React.FC = (props) => ( - -); - -type ClusterDeploymentCreateProgressProps = { - agentClusterInstall: AgentClusterInstallK8sResource; - toDetails: VoidFunction; - toListView: VoidFunction; -}; - -const ClusterDeploymentCreateProgress: React.FC = ({ - agentClusterInstall, - toDetails, - toListView, -}) => { - const { t } = useTranslation(); - return ( - - - {isInstallationReady(agentClusterInstall) ? ( - <> - - - {t('ai:Cluster is ready for installation')} - - -
{t('ai:This cluster has been created and is ready to begin installation.')}
-
{t('ai:To see the status of the installation see the details page.')}
-
- - ) : ( - <> - - - {t('ai:Preparing cluster installation')} - - -
- {t('ai:This cluster is in the process of getting ready to start installation.')} -
-
- {t( - 'ai:If you exit this flow you can see its status in the list view or details page.', - )} -
-
- - )} - - - - -
-
- ); -}; - -export default ClusterDeploymentCreateProgress; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsForm.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsForm.tsx index 7c0c23a9ab..eb469bb57f 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsForm.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsForm.tsx @@ -1,5 +1,13 @@ import * as React from 'react'; -import { Alert, Stack, StackItem } from '@patternfly/react-core'; +import { + Alert, + AlertVariant, + Stack, + StackItem, + useWizardContext, + useWizardFooter, + WizardFooter, +} from '@patternfly/react-core'; import { AgentClusterInstallK8sResource, ClusterDeploymentK8sResource, OsImage } from '../../types'; import { ClusterImageSetK8sResource } from '../../types/k8s/cluster-image-set'; import { getOCPVersions, getSelectedVersion } from '../helpers'; @@ -10,6 +18,8 @@ import { } from './ClusterDetailsFormFields'; import { useFormikContext } from 'formik'; import { ClusterDetailsValues, CpuArchitecture, SupportedCpuArchitecture } from '../../../common'; +import { ClusterDeploymentWizardContext } from './ClusterDeploymentWizardContext'; +import { ValidationSection } from './components/ValidationSection'; type ClusterDeploymentDetailsFormProps = { clusterImages: ClusterImageSetK8sResource[]; @@ -20,6 +30,56 @@ type ClusterDeploymentDetailsFormProps = { osImages?: OsImage[]; }; +export const ClusterDeploymentDetailsFormWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { activeStep, goToPrevStep, goToNextStep, close } = useWizardContext(); + const { syncError } = React.useContext(ClusterDeploymentWizardContext); + const { submitForm, isSubmitting, isValid, isValidating, dirty } = + useFormikContext(); + const { t } = useTranslation(); + + const handleOnNext = () => { + if (dirty) { + void submitForm(); + } else { + void goToNextStep(); + } + }; + + const footer = ( + + ); + + useWizardFooter(footer); + + return ( + + {children} + {syncError && ( + + + + {syncError} + + + + )} + + ); +}; + const ClusterDeploymentDetailsForm: React.FC = ({ agentClusterInstall, clusterDeployment, @@ -59,7 +119,7 @@ const ClusterDeploymentDetailsForm: React.FC }, [osImages, ocpVersions, values.openshiftVersion]); return ( - + <> {isEditFlow && ( cpuArchitectures={cpuArchitectures} /> - + ); }; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsStep.tsx index 81a922ffc2..176d7b1fcb 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsStep.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentDetailsStep.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Formik } from 'formik'; import { Lazy } from 'yup'; -import { Grid, GridItem } from '@patternfly/react-core'; +import { Grid, GridItem, useWizardContext } from '@patternfly/react-core'; import { useAlerts, @@ -15,9 +15,6 @@ import { } from '../../../common'; import { ClusterDeploymentDetailsStepProps, ClusterDeploymentDetailsValues } from './types'; -import ClusterDeploymentWizardFooter from './ClusterDeploymentWizardFooter'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; -import ClusterDeploymentWizardStep from './ClusterDeploymentWizardStep'; import { getAICluster, getNetworkType, getOCPVersions } from '../helpers'; import { AgentClusterInstallK8sResource, @@ -25,8 +22,10 @@ import { ClusterDeploymentK8sResource, InfraEnvK8sResource, } from '../../types'; -import ClusterDeploymentDetailsForm from './ClusterDeploymentDetailsForm'; -import { isCIMFlow, getGridSpans } from './helpers'; +import ClusterDeploymentDetailsForm, { + ClusterDeploymentDetailsFormWrapper, +} from './ClusterDeploymentDetailsForm'; +import { getGridSpans } from './helpers'; import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; type UseDetailsFormikArgs = { @@ -108,15 +107,16 @@ const ClusterDeploymentDetailsStep: React.FC agents, usedClusterNames, onSaveDetails, - onClose, isPreviewOpen, infraEnv, isNutanix, }) => { - const { addAlert } = useAlerts(); - const { setCurrentStepId } = React.useContext(ClusterDeploymentWizardContext); const { t } = useTranslation(); + const { addAlert } = useAlerts(); + const { goToNextStep } = useWizardContext(); + const ocpVersions = getOCPVersions(clusterImages, isNutanix); + const gridSpans = getGridSpans(isPreviewOpen); const [initialValues, validationSchema] = useDetailsFormik({ clusterDeployment, @@ -126,15 +126,11 @@ const ClusterDeploymentDetailsStep: React.FC usedClusterNames, infraEnv, }); - const next = () => - isCIMFlow(clusterDeployment) - ? setCurrentStepId('hosts-selection') - : setCurrentStepId('hosts-discovery'); const handleSubmit = async (values: ClusterDeploymentDetailsValues) => { try { await onSaveDetails(values); - next(); + await goToNextStep(); } catch (error) { addAlert({ title: t('ai:Failed to save ClusterDeployment'), @@ -149,45 +145,21 @@ const ClusterDeploymentDetailsStep: React.FC validate={getRichTextValidation(validationSchema)} onSubmit={handleSubmit} > - {({ submitForm, isSubmitting, isValid, isValidating, dirty }) => { - const handleOnNext = () => { - if (dirty) { - void submitForm(); - } else { - next(); - } - }; - - const footer = ( - - ); - - const gridSpans = getGridSpans(isPreviewOpen); - - return ( - - - - Cluster Details - - - - - - - ); - }} + + + Cluster Details + + + + + + + ); }; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostSelectionStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostSelectionStep.tsx index 01aeef04bc..1f7158e504 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostSelectionStep.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostSelectionStep.tsx @@ -1,16 +1,21 @@ import React from 'react'; import * as Yup from 'yup'; import { Formik, FormikConfig, useFormikContext } from 'formik'; -import { Alert, AlertVariant, Grid, GridItem } from '@patternfly/react-core'; -import { ClusterWizardStepHeader, useAlerts } from '../../../common'; +import { + Alert, + AlertVariant, + Grid, + GridItem, + useWizardContext, + useWizardFooter, + WizardFooter, +} from '@patternfly/react-core'; +import { Alerts, ClusterWizardStepHeader, useAlerts } from '../../../common'; import { AgentClusterInstallK8sResource, AgentK8sResource, ClusterDeploymentK8sResource, } from '../../types'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; -import ClusterDeploymentWizardFooter from './ClusterDeploymentWizardFooter'; -import ClusterDeploymentWizardStep from './ClusterDeploymentWizardStep'; import ClusterDeploymentHostsSelection from './ClusterDeploymentHostsSelection'; import { ClusterDeploymentHostSelectionStepProps, @@ -25,6 +30,8 @@ import { import { canNextFromHostSelectionStep } from './wizardTransition'; import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; import { TFunction } from 'i18next'; +import { ValidationSection } from './components/ValidationSection'; +import { ClusterDeploymentWizardContext } from './ClusterDeploymentWizardContext'; const getInitialValues = ({ agents, @@ -138,14 +145,15 @@ type HostSelectionFormProps = Omit = ({ agents, agentClusterInstall, - onClose, clusterDeployment, aiConfigMap, onEditRole: onEditRoleInit, onSetInstallationDiskId, isNutanix, }) => { - const { setCurrentStepId } = React.useContext(ClusterDeploymentWizardContext); + const { activeStep, goToNextStep, goToPrevStep, close } = useWizardContext(); + const { syncError } = React.useContext(ClusterDeploymentWizardContext); + const { alerts } = useAlerts(); const [showClusterErrors, setShowClusterErrors] = React.useState(false); const { values, @@ -186,7 +194,7 @@ const HostSelectionForm: React.FC = ({ setShowClusterErrors(false); }, []); - const onNext = async () => { + const onNext = React.useCallback(async () => { if (!showFormErrors) { setShowFormErrors(true); const errors = await validateForm(); @@ -202,7 +210,7 @@ const HostSelectionForm: React.FC = ({ } void submitForm(); setNextRequested(true); - }; + }, [setTouched, showFormErrors, submitForm, validateForm]); React.useEffect(() => { if (nextRequested && !isSubmitting) { @@ -223,71 +231,106 @@ const HostSelectionForm: React.FC = ({ ) { setShowClusterErrors(true); if (canNextFromHostSelectionStep(agentClusterInstall, selectedAgents)) { - setCurrentStepId('networking'); + void goToNextStep(); } } } - }, [nextRequested, selectedAgents, agentClusterInstall, setCurrentStepId, isSubmitting, t]); - - let submittingText: string | undefined = undefined; + }, [nextRequested, selectedAgents, agentClusterInstall, isSubmitting, t, goToNextStep]); - if (isSubmitting) { - submittingText = t('ai:Saving changes...'); - } else if (nextRequested && !showClusterErrors) { - submittingText = t('ai:Binding hosts...'); - } + const submittingText = React.useMemo(() => { + if (isSubmitting) { + return t('ai:Saving changes...'); + } else if (nextRequested && !showClusterErrors) { + return t('ai:Binding hosts...'); + } + return undefined; + }, [isSubmitting, nextRequested, showClusterErrors, t]); - const onSyncError = React.useCallback(() => setNextRequested(false), []); + React.useEffect(() => { + if (syncError) { + setNextRequested(false); + } + }, [syncError]); - const footer = ( - setCurrentStepId('cluster-details')} - onCancel={onClose} - showClusterErrors={showClusterErrors} - onSyncError={onSyncError} - > - {showFormErrors && errors.selectedHostIds && touched.selectedHostIds && ( - - {errors.selectedHostIds} + const errorsSection = ( + + {syncError && ( + + {syncError} )} - + ); + const footer = React.useMemo( + () => ( + + ), + [ + activeStep, + onNext, + nextRequested, + isSubmitting, + showFormErrors, + isValid, + isValidating, + submittingText, + t, + goToPrevStep, + close, + ], + ); + + useWizardFooter(footer); + return ( - - + + + {t('ai:Cluster hosts')} + + + + + {(showClusterErrors || showFormErrors) && !!alerts.length && ( - {t('ai:Cluster hosts')} + + )} + + {syncError && {errorsSection}} + {showFormErrors && errors.selectedHostIds && touched.selectedHostIds && ( - + + {errors.selectedHostIds} + - - + )} + ); }; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostsDiscoveryStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostsDiscoveryStep.tsx index 88393dc0e2..e38f051e82 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostsDiscoveryStep.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentHostsDiscoveryStep.tsx @@ -1,28 +1,37 @@ import React from 'react'; import uniq from 'lodash-es/uniq.js'; -import { Grid, GridItem, Alert, AlertVariant, List, ListItem } from '@patternfly/react-core'; +import { + Grid, + GridItem, + Alert, + AlertVariant, + List, + ListItem, + useWizardFooter, + WizardFooter, + useWizardContext, +} from '@patternfly/react-core'; -import { ClusterWizardStepHeader } from '../../../common'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; -import ClusterDeploymentWizardFooter from './ClusterDeploymentWizardFooter'; -import ClusterDeploymentWizardStep from './ClusterDeploymentWizardStep'; +import { Alerts, ClusterWizardStepHeader } from '../../../common'; import { ClusterDeploymentHostsDiscoveryStepProps } from './types'; import ClusterDeploymentHostsDiscovery from './ClusterDeploymentHostsDiscovery'; import { getAgentsHostsNames, isAgentOfInfraEnv } from './helpers'; import { getIsSNOCluster, getWizardStepAgentStatus } from '../helpers'; import { canNextFromHostDiscoveryStep } from './wizardTransition'; import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; +import { ValidationSection } from './components/ValidationSection'; +import { ClusterDeploymentWizardContext } from './ClusterDeploymentWizardContext'; -const ClusterDeploymentHostsDiscoveryStep: React.FC = ({ +const ClusterDeploymentHostsDiscoveryStep = ({ agentClusterInstall, agents: allAgents, infraEnv, onSaveHostsDiscovery, - onClose, ...rest -}) => { +}: ClusterDeploymentHostsDiscoveryStepProps) => { const { t } = useTranslation(); - const { setCurrentStepId } = React.useContext(ClusterDeploymentWizardContext); + const { syncError } = React.useContext(ClusterDeploymentWizardContext); + const { activeStep, goToNextStep, goToPrevStep, close } = useWizardContext(); const [showClusterErrors, setShowClusterErrors] = React.useState(false); const [nextRequested, setNextRequested] = React.useState(false); const [showFormErrors, setShowFormErrors] = React.useState(false); @@ -61,7 +70,7 @@ const ClusterDeploymentHostsDiscoveryStep: React.FC { + const onNext = React.useCallback(async () => { if (!showFormErrors) { setShowFormErrors(true); if (errors.length) { @@ -70,7 +79,7 @@ const ClusterDeploymentHostsDiscoveryStep: React.FC { setNextRequested(false); @@ -86,31 +95,68 @@ const ClusterDeploymentHostsDiscoveryStep: React.FC (nextRequested && !showClusterErrors ? t('ai:Saving changes...') : undefined), + [nextRequested, showClusterErrors, t], + ); + + React.useEffect(() => { + if (syncError) { + setNextRequested(false); + } + }, [syncError]); - const onSyncError = React.useCallback(() => setNextRequested(false), []); + const footer = React.useMemo( + () => ( + + ), + [ + activeStep, + close, + errors.length, + goToPrevStep, + nextRequested, + onNext, + showFormErrors, + submittingText, + t, + ], + ); + useWizardFooter(footer); - const footer = ( - setCurrentStepId('cluster-details')} - showClusterErrors={showClusterErrors} - onSyncError={onSyncError} - onCancel={onClose} - > + return ( + + + {t('ai:Cluster hosts')} + + + + + {showClusterErrors && !!errors.length && ( + + + + )} {showFormErrors && !!errors.length && ( )} - - ); - return ( - - + {!!syncError && ( - {t('ai:Cluster hosts')} + + + {syncError} + + - - - - - + )} + ); }; - export default ClusterDeploymentHostsDiscoveryStep; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentNetworkingStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentNetworkingStep.tsx index 4e676d3985..b5f3b0c5aa 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentNetworkingStep.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentNetworkingStep.tsx @@ -1,6 +1,14 @@ import React from 'react'; import { Formik, useFormikContext } from 'formik'; -import { Alert, AlertVariant, Grid, GridItem } from '@patternfly/react-core'; +import { + Alert, + AlertVariant, + Grid, + GridItem, + useWizardContext, + useWizardFooter, + WizardFooter, +} from '@patternfly/react-core'; import { useAlerts, @@ -8,6 +16,7 @@ import { FormikAutoSave, getFormikErrorFields, clusterFieldLabels, + Alerts, } from '../../../common'; import { @@ -15,20 +24,18 @@ import { AgentTableActions, ClusterDeploymentNetworkingValues, } from './types'; -import ClusterDeploymentWizardFooter from './ClusterDeploymentWizardFooter'; -import ClusterDeploymentWizardStep from './ClusterDeploymentWizardStep'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; import ClusterDeploymentNetworkingForm from './ClusterDeploymentNetworkingForm'; -import { isAgentOfCluster, isCIMFlow } from './helpers'; +import { isAgentOfCluster } from './helpers'; import { useInfraEnvProxies, useNetworkingFormik } from './use-networking-formik'; import { canNextFromNetworkingStep } from './wizardTransition'; import { AgentK8sResource } from '../../types'; import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; +import { ValidationSection } from './components/ValidationSection'; +import { ClusterDeploymentWizardContext } from './ClusterDeploymentWizardContext'; type NetworkingFormProps = { clusterDeployment: ClusterDeploymentDetailsNetworkingProps['clusterDeployment']; agentClusterInstall: ClusterDeploymentDetailsNetworkingProps['agentClusterInstall']; - onClose: ClusterDeploymentDetailsNetworkingProps['onClose']; agents: AgentK8sResource[]; fetchInfraEnv: ClusterDeploymentDetailsNetworkingProps['fetchInfraEnv']; onEditHost: AgentTableActions['onEditHost']; @@ -38,18 +45,21 @@ type NetworkingFormProps = { isNutanix: ClusterDeploymentDetailsNetworkingProps['isNutanix']; }; -const NetworkingForm: React.FC = ({ +export const NetworkingForm = ({ clusterDeployment, agentClusterInstall, agents, - onClose, fetchInfraEnv, onEditHost, onEditRole, isPreviewOpen, onSetInstallationDiskId, isNutanix, -}) => { +}: NetworkingFormProps) => { + const { t } = useTranslation(); + const { activeStep, goToPrevStep, goToNextStep, close } = useWizardContext(); + const { syncError } = React.useContext(ClusterDeploymentWizardContext); + const { alerts } = useAlerts(); const [showFormErrors, setShowFormErrors] = React.useState(false); const [showClusterErrors, setShowClusterErrors] = React.useState(false); const [nextRequested, setNextRequested] = React.useState(false); @@ -59,16 +69,13 @@ const NetworkingForm: React.FC = ({ }); const { isValid, isValidating, isSubmitting, validateForm, setTouched, errors, touched, values } = useFormikContext(); - const { setCurrentStepId } = React.useContext(ClusterDeploymentWizardContext); - - const canContinue = canNextFromNetworkingStep(agentClusterInstall, agents); - const onBack = () => - isCIMFlow(clusterDeployment) - ? setCurrentStepId('hosts-selection') - : setCurrentStepId('hosts-discovery'); + const canContinue = React.useMemo( + () => canNextFromNetworkingStep(agentClusterInstall, agents), + [agentClusterInstall, agents], + ); - const onNext = async () => { + const onNext = React.useCallback(async () => { if (!showFormErrors) { const errors = await validateForm(); setTouched( @@ -83,7 +90,7 @@ const NetworkingForm: React.FC = ({ } } setNextRequested(true); - }; + }, [setTouched, showFormErrors, validateForm]); React.useEffect(() => { setNextRequested(false); @@ -94,46 +101,61 @@ const NetworkingForm: React.FC = ({ if (nextRequested) { setShowClusterErrors(true); if (canContinue) { - setCurrentStepId('review'); + void goToNextStep(); } } - }, [nextRequested, canContinue, setCurrentStepId]); + }, [nextRequested, canContinue, goToNextStep]); - const onSyncError = React.useCallback(() => setNextRequested(false), []); + React.useEffect(() => { + if (syncError) { + setNextRequested(false); + } + }, [syncError]); const errorFields = getFormikErrorFields(errors, touched); - const { t } = useTranslation(); - const footer = ( - - {showFormErrors && !!errorFields.length && ( - - {t('ai:The following fields are invalid or missing')}:{' '} - {errorFields.map((field: string) => clusterFieldLabels(t)[field] || field).join(', ')}. - - )} - + + const submittingText = React.useMemo(() => { + if (isSubmitting) { + return t('ai:Saving changes...'); + } else if (isValidating || nextRequested) { + return t('ai:Validating...'); + } + return undefined; + }, [isSubmitting, isValidating, nextRequested, t]); + + const footer = React.useMemo( + () => ( + + ), + [ + activeStep, + onNext, + nextRequested, + isSubmitting, + showFormErrors, + isValid, + isValidating, + submittingText, + goToPrevStep, + close, + t, + ], ); + useWizardFooter(footer); + return ( - + <> {t('ai:Networking')} @@ -154,19 +176,49 @@ const NetworkingForm: React.FC = ({ isNutanix={isNutanix} /> + + {showFormErrors && !!errorFields.length && ( + + + {t('ai:The following fields are invalid or missing')}:{' '} + {errorFields.map((field: string) => clusterFieldLabels(t)[field] || field).join(', ')} + . + + + )} + + {(showClusterErrors || showFormErrors) && !!alerts.length && ( + + + + )} + + {syncError && ( + + + + {syncError} + + + + )} - + ); }; -const ClusterDeploymentNetworkingStep: React.FC = ({ +const ClusterDeploymentNetworkingStep = ({ clusterDeployment, agentClusterInstall, agents, onSaveNetworking, ...rest -}) => { +}: ClusterDeploymentDetailsNetworkingProps) => { const { addAlert } = useAlerts(); const cdName = clusterDeployment?.metadata?.name; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentReviewStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentReviewStep.tsx index 01b927a44b..a9a40db220 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentReviewStep.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentReviewStep.tsx @@ -1,4 +1,12 @@ -import { Grid, GridItem } from '@patternfly/react-core'; +import { + Alert, + AlertVariant, + Grid, + GridItem, + useWizardContext, + useWizardFooter, + WizardFooter, +} from '@patternfly/react-core'; import * as React from 'react'; import { canNextFromReviewStep } from './wizardTransition'; import { @@ -17,9 +25,6 @@ import { HostsValidations, useAlerts, } from '../../../common'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; -import ClusterDeploymentWizardFooter from './ClusterDeploymentWizardFooter'; -import ClusterDeploymentWizardStep from './ClusterDeploymentWizardStep'; import { getSelectedVersion, getAICluster, getClusterDeploymentCpuArchitecture } from '../helpers'; import { isAgentOfCluster } from './helpers'; import { wizardStepNames } from './constants'; @@ -29,31 +34,31 @@ import { allClusterWizardSoftValidationIds, } from './wizardTransition'; import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; +import { ClusterDeploymentWizardContext } from './ClusterDeploymentWizardContext'; +import { ValidationSection } from './components/ValidationSection'; type ClusterDeploymentReviewStepProps = { clusterDeployment: ClusterDeploymentK8sResource; agentClusterInstall: AgentClusterInstallK8sResource; agents: AgentK8sResource[]; - onClose: VoidFunction; // eslint-disable-next-line onFinish: () => Promise; clusterImages: ClusterImageSetK8sResource[]; infraEnv?: InfraEnvK8sResource; }; -const ClusterDeploymentReviewStep: React.FC = ({ +const ClusterDeploymentReviewStep = ({ agentClusterInstall, agents, - onClose, onFinish, clusterDeployment, clusterImages, infraEnv, -}) => { +}: ClusterDeploymentReviewStepProps) => { const { addAlert, clearAlerts } = useAlerts(); - const { setCurrentStepId } = React.useContext(ClusterDeploymentWizardContext); + const { activeStep, goToPrevStep, goToStepByName, close } = useWizardContext(); + const { syncError } = React.useContext(ClusterDeploymentWizardContext); const [isSubmitting, setSubmitting] = React.useState(false); - const onBack = () => setCurrentStepId('networking'); const cdName = clusterDeployment.metadata?.name; const cdNamespace = clusterDeployment.metadata?.namespace; const cpuArchitecture = getClusterDeploymentCpuArchitecture(clusterDeployment, infraEnv); @@ -79,18 +84,17 @@ const ClusterDeploymentReviewStep: React.FC = }; const footer = ( - ); + useWizardFooter(footer); const openShiftVersion = getSelectedVersion(clusterImages, agentClusterInstall); @@ -100,75 +104,82 @@ const ClusterDeploymentReviewStep: React.FC = ); return ( - - - - Review and create - - - - - - - - - } - /> + + + Review and create + + + + + + + + + } + /> - - validationsInfo={cluster.validationsInfo} - setCurrentStepId={setCurrentStepId} - wizardStepNames={wizardStepNames(t)} - wizardStepsValidationsMap={wizardStepsValidationsMap} - /> - } - testId="cluster-validations" - /> - - hosts={cluster.hosts} - setCurrentStepId={setCurrentStepId} - wizardStepNames={wizardStepNames(t)} - allClusterWizardSoftValidationIds={allClusterWizardSoftValidationIds} - wizardStepsValidationsMap={wizardStepsValidationsMap} - /> - } - testId="host-validations" - /> - + + validationsInfo={cluster.validationsInfo} + setCurrentStepId={goToStepByName} + wizardStepNames={wizardStepNames(t)} + wizardStepsValidationsMap={wizardStepsValidationsMap} + /> + } + testId="cluster-validations" + /> + + hosts={cluster.hosts} + setCurrentStepId={goToStepByName} + wizardStepNames={wizardStepNames(t)} + allClusterWizardSoftValidationIds={allClusterWizardSoftValidationIds} + wizardStepsValidationsMap={wizardStepsValidationsMap} + /> + } + testId="host-validations" + /> + + + {syncError && ( + + + + {syncError} + + - - + )} + ); }; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizard.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizard.tsx index 8d5edf87ef..81daaf2172 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizard.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizard.tsx @@ -1,18 +1,20 @@ import * as React from 'react'; -import classNames from 'classnames'; -import { Grid, GridItem } from '@patternfly/react-core'; +import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; +import { Grid, GridItem, Wizard, WizardStep } from '@patternfly/react-core'; import { AlertsContextProvider, LoadingState } from '../../../common'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; import ClusterDeploymentDetailsStep from './ClusterDeploymentDetailsStep'; import ClusterDeploymentNetworkingStep from './ClusterDeploymentNetworkingStep'; import ClusterDeploymentHostSelectionStep from './ClusterDeploymentHostSelectionStep'; -import { ClusterDeploymentWizardProps, ClusterDeploymentWizardStepsType } from './types'; +import { ClusterDeploymentWizardProps } from './types'; import ClusterDeploymentHostsDiscoveryStep from './ClusterDeploymentHostsDiscoveryStep'; import { ACMFeatureSupportLevelProvider } from '../featureSupportLevels'; import ClusterDeploymentReviewStep from './ClusterDeploymentReviewStep'; import { YamlPreview, useYamlPreview } from '../YamlPreview'; +import { wizardStepNames } from './constants'; +import { ClusterDeploymentWizardContextProvider } from './ClusterDeploymentWizardContext'; +import { isCIMFlow } from './helpers'; -const ClusterDeploymentWizard: React.FC = ({ +export const ClusterDeploymentWizard = ({ className, onSaveDetails, onSaveNetworking, @@ -40,12 +42,8 @@ const ClusterDeploymentWizard: React.FC = ({ isNutanix, onChangeBMHHostname, ...rest -}) => { - const [currentStepId, setCurrentStepId] = React.useState( - initialStep || 'cluster-details', - ); - - const isAIFlow = infraEnv; +}: ClusterDeploymentWizardProps) => { + const { t } = useTranslation(); const { code, loadingResources } = useYamlPreview({ agentClusterInstall, @@ -55,115 +53,110 @@ const ClusterDeploymentWizard: React.FC = ({ fetchKlusterletAddonConfig, }); - const renderCurrentStep = () => { - const stepId: ClusterDeploymentWizardStepsType = !clusterDeployment - ? 'cluster-details' - : currentStepId; - - switch (stepId) { - case 'hosts-selection': - if (agentClusterInstall?.metadata?.name) { - return ( - - ); - } - return ; - case 'hosts-discovery': - if (isAIFlow) { - return onSaveISOParams && onCreateBMH ? ( - - ) : ( - - ); - } - return ; - case 'networking': - return ( - - ); - case 'review': - return ( - - ); - case 'cluster-details': - default: - return ( - - ); - } - }; + // if initialStep is set, it is either 'host-selection' or 'host-discovery', both at index 4 + // if initialStep is not set, start at 'cluster-details' which is at index 2 + const startIndex = initialStep ? 4 : 2; + const stepNames = wizardStepNames(t); + const isAIFlow = !!infraEnv; return ( - -
{renderCurrentStep()}
-
+ + + + + + + + {isCIMFlow(clusterDeployment) ? ( + + {agentClusterInstall?.metadata?.name ? ( + + ) : ( + + )} + + ) : ( + + {isAIFlow && onSaveISOParams && onCreateBMH ? ( + + ) : ( + + )} + + )} + + + + + + + +
@@ -177,5 +170,3 @@ const ClusterDeploymentWizard: React.FC = ({
); }; - -export default ClusterDeploymentWizard; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardContext.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardContext.tsx index e7f310bdea..ef76308660 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardContext.tsx +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardContext.tsx @@ -1,20 +1,33 @@ import React from 'react'; -import { ClusterDeploymentK8sResource } from '../../types'; -import { ClusterDeploymentWizardStepsType } from './types'; +import { AgentClusterInstallK8sResource } from '../../types'; type ClusterDeploymentWizardContextType = { - currentStepId: ClusterDeploymentWizardStepsType; - setCurrentStepId: (stepId: ClusterDeploymentWizardStepsType) => void; - clusterDeployment?: ClusterDeploymentK8sResource; + syncError?: string; }; -const ClusterDeploymentWizardContext = React.createContext({ - currentStepId: 'cluster-details', - setCurrentStepId: () => { - // console.error( - // 'Tried to use ClusterDeploymentWizardContext but there was no provider rendered.', - // ); - }, -}); +export const ClusterDeploymentWizardContext = + React.createContext({ + syncError: undefined, + }); -export default ClusterDeploymentWizardContext; +export const ClusterDeploymentWizardContextProvider = ({ + children, + agentClusterInstall, +}: { + children: React.ReactNode; + agentClusterInstall?: AgentClusterInstallK8sResource; +}) => { + const syncError = React.useMemo( + () => + agentClusterInstall?.status?.conditions?.find( + (c) => c.type === 'SpecSynced' && c.status === 'False', + )?.message, + [agentClusterInstall?.status?.conditions], + ); + + return ( + + {children} + + ); +}; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardFooter.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardFooter.tsx deleted file mode 100644 index 517e882986..0000000000 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardFooter.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import { Alert, AlertGroup, AlertVariant } from '@patternfly/react-core'; -import { - Alerts, - WizardFooter, - useAlerts, - WizardFooterGenericProps, - ClusterWizardStepValidationsAlert, -} from '../../../common'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; -import { ClusterWizardStepHostStatusDeterminationObject } from '../../../common/types/hosts'; -import { ValidationsInfo } from '../../../common/types/clusters'; -import { Cluster } from '@openshift-assisted/types/assisted-installer-service'; -import { ClusterWizardStepsType, wizardStepsValidationsMap } from './wizardTransition'; -import { AgentClusterInstallK8sResource } from '../../types/k8s/agent-cluster-install'; -import { AgentK8sResource } from '../../types/k8s/agent'; -import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; -import ValidationsRunningAlert from '../common/ValidationsRunningAlert'; - -type ValidationSectionProps = { - requireProxy?: boolean; - currentStepId: ClusterWizardStepsType; - validationsInfo?: ValidationsInfo; - clusterStatus?: Cluster['status']; - hosts: ClusterWizardStepHostStatusDeterminationObject[]; -}; - -const ValidationSection: React.FC = ({ - currentStepId, - clusterStatus, - validationsInfo, - hosts, - children, -}) => { - return ( - - {children} - {clusterStatus && ( - - - - )} - - ); -}; - -type ClusterDeploymentWizardFooterProps = React.ComponentProps & - WizardFooterGenericProps & { - agentClusterInstall?: AgentClusterInstallK8sResource; - agents?: AgentK8sResource[]; - showClusterErrors?: boolean; - onSyncError?: VoidFunction; - }; - -const ClusterDeploymentWizardFooter: React.FC = ({ - agentClusterInstall, - agents, - showClusterErrors, - children, - onSyncError, - isNextDisabled, - ...rest -}) => { - const { alerts } = useAlerts(); - const { currentStepId } = React.useContext(ClusterDeploymentWizardContext); - const clusterStatus = showClusterErrors - ? agentClusterInstall?.status?.debugInfo?.state - : undefined; - const validationsInfo = showClusterErrors - ? agentClusterInstall?.status?.validationsInfo - : undefined; - const hosts = React.useMemo( - () => - (agents || []).reduce((result, agent) => { - const status = agent.status?.debugInfo?.state; - if (status) { - result.push({ status, validationsInfo: agent.status?.validationsInfo }); - } - return result; - }, []), - [agents], - ); - - const syncError = agentClusterInstall?.status?.conditions?.find( - (c) => c.type === 'SpecSynced' && c.status === 'False', - )?.message; - - const { t } = useTranslation(); - React.useEffect(() => { - if (syncError && onSyncError) { - onSyncError(); - } - }, [syncError, onSyncError]); - - const alertsSection = alerts.length ? : undefined; - const errorsSection = ( - - {syncError && ( - - {syncError} - - )} - {children} - - ); - - return ( - - ); -}; - -export default ClusterDeploymentWizardFooter; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardNavigation.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardNavigation.tsx deleted file mode 100644 index e41f15a309..0000000000 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardNavigation.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; -import { WizardNav } from '@patternfly/react-core/deprecated'; -import { WizardNavItem } from '../../../common'; -import { wizardStepNames } from './constants'; -import ClusterDeploymentWizardContext from './ClusterDeploymentWizardContext'; -import { ClusterDeploymentWizardStepsType } from './types'; -import { isCIMFlow } from './helpers'; -import { useTranslation } from '../../../common/hooks/use-translation-wrapper'; - -const wizardSteps = Object.keys(wizardStepNames) as ClusterDeploymentWizardStepsType[]; - -const ClusterDeploymentWizardNavigation: React.FC<{ - // cluster?: Cluster -}> = () => { - const { currentStepId, setCurrentStepId, clusterDeployment } = React.useContext( - ClusterDeploymentWizardContext, - ); - const { t } = useTranslation(); - const stepNames = wizardStepNames(t); - return ( - - - !cluster || canNextHostDiscovery({ cluster }) } - isDisabled={false} - step={1} - onNavItemClick={() => setCurrentStepId('cluster-details')} - /> - - {isCIMFlow(clusterDeployment) ? ( - !cluster || canNextHostDiscovery({ cluster }) } - isCurrent={currentStepId === 'hosts-selection'} - step={2} - onNavItemClick={() => setCurrentStepId('hosts-selection')} - /> - ) : ( - !cluster || canNextHostDiscovery({ cluster }) } - isCurrent={currentStepId === 'hosts-discovery'} - step={2} - onNavItemClick={() => setCurrentStepId('hosts-discovery')} - /> - )} - !cluster || canNextHostDiscovery({ cluster }) } - isCurrent={currentStepId === 'networking'} - step={3} - onNavItemClick={() => setCurrentStepId('networking')} - /> - !cluster || canNextHostDiscovery({ cluster }) } - isCurrent={currentStepId === 'review'} - step={4} - onNavItemClick={() => setCurrentStepId('review')} - /> - - ); -}; - -export default ClusterDeploymentWizardNavigation; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardStep.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardStep.tsx deleted file mode 100644 index 61452f5987..0000000000 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/ClusterDeploymentWizardStep.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; -import { ClusterWizardStep } from '../../../common/components/clusterWizard'; -import ClusterDeploymentWizardNavigation from './ClusterDeploymentWizardNavigation'; - -type ClusterDeploymentWizardStepProps = { - footer: React.ReactNode; -}; - -const ClusterDeploymentWizardStep: React.FC = (props) => ( - } {...props} /> -); - -export default ClusterDeploymentWizardStep; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ClusterDeploymentHostsSelectionAdvanced.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ClusterDeploymentHostsSelectionAdvanced.tsx new file mode 100644 index 0000000000..16b4befdf4 --- /dev/null +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ClusterDeploymentHostsSelectionAdvanced.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import Measure from 'react-measure'; +import isMatch from 'lodash-es/isMatch.js'; +import { useFormikContext } from 'formik'; +import { Alert, AlertVariant, Grid, GridItem } from '@patternfly/react-core'; + +import { AgentK8sResource } from '../../../types'; +import { AGENT_LOCATION_LABEL_KEY, AGENT_NOLOCATION_VALUE } from '../../common'; +import { ClusterDeploymentHostsSelectionValues, ScaleUpFormValues } from '../types'; +import AgentsSelectionTable from '../../Agent/AgentsSelectionTable'; +import { AgentTableActions } from '../types'; +import { useTranslation } from '../../../../common/hooks/use-translation-wrapper'; +import { CpuArchitecture } from '../../../../common'; +import LocationsSelector from '../LocationsSelector'; +import LabelsSelector, { infraEnvLabelKeys } from '../LabelsSelector'; + +type ClusterDeploymentHostsSelectionAdvancedProps = { + availableAgents: AgentK8sResource[]; + onEditRole?: AgentTableActions['onEditRole']; + onEditHost?: AgentTableActions['onEditHost']; + onSetInstallationDiskId?: AgentTableActions['onSetInstallationDiskId']; + onHostSelect?: VoidFunction; + cpuArchitecture?: CpuArchitecture; +}; + +type FormValues = ClusterDeploymentHostsSelectionValues | ScaleUpFormValues; + +const ClusterDeploymentHostsSelectionAdvanced = ({ + availableAgents, + onEditRole, + onSetInstallationDiskId, + onEditHost, + onHostSelect, + cpuArchitecture, +}: ClusterDeploymentHostsSelectionAdvancedProps) => { + const { t } = useTranslation(); + const { values } = useFormikContext(); + const { locations, agentLabels } = values; + + const matchingAgents = React.useMemo( + () => + availableAgents.filter((agent) => { + const labels = agentLabels.reduce((acc, curr) => { + const label = curr.split('='); + acc[label[0]] = label[1]; + return acc; + }, {} as Record); + const matchesLocation = locations.length + ? locations.includes( + agent.metadata?.labels?.[AGENT_LOCATION_LABEL_KEY] || AGENT_NOLOCATION_VALUE, + ) + : true; + const matchesLabels = agent.metadata?.labels + ? isMatch(agent.metadata?.labels, labels) + : true; + return matchesLocation && matchesLabels; + }), + [availableAgents, locations, agentLabels], + ); + + return ( + <> + + + + + + + + {cpuArchitecture && ( + + )} + + + {({ measureRef, contentRect }) => ( +
+ +
+ )} +
+
+
+ + ); +}; + +export default ClusterDeploymentHostsSelectionAdvanced; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ClusterScaleUpAutoHostsSelection.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ClusterScaleUpAutoHostsSelection.tsx new file mode 100644 index 0000000000..e84ce087ee --- /dev/null +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ClusterScaleUpAutoHostsSelection.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Grid, GridItem } from '@patternfly/react-core'; +import { NumberInputField } from '../../../../common'; +import { AgentK8sResource } from '../../../types'; +import AgentsSelectionHostCountAlerts from '../../Agent/AgentsSelectionHostCountAlerts'; +import AgentsSelectionHostCountLabelIcon from '../../Agent/AgentsSelectionHostCountLabelIcon'; +import { useAgentsAutoSelection } from '../../Agent/AgentsSelectionUtils'; +import { useTranslation } from '../../../../common/hooks/use-translation-wrapper'; +import LocationsSelector from '../LocationsSelector'; + +type ClusterScaleUpAutoHostsSelectionProps = { + availableAgents: AgentK8sResource[]; +}; + +const ClusterScaleUpAutoHostsSelection: React.FC = ({ + availableAgents, +}) => { + const { matchingAgents, selectedAgents, hostCount } = useAgentsAutoSelection(availableAgents); + const { t } = useTranslation(); + + return ( + <> + + + } + idPostfix="hostcount" + name="hostCount" + minValue={1} + maxValue={matchingAgents.length} + isRequired + /> + + + + + + + + + ); +}; + +export default ClusterScaleUpAutoHostsSelection; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ValidationSection.tsx b/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ValidationSection.tsx new file mode 100644 index 0000000000..7953cf1bc0 --- /dev/null +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/components/ValidationSection.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { AlertGroup } from '@patternfly/react-core'; +import { Cluster } from '@openshift-assisted/types/./assisted-installer-service'; +import { ValidationsInfo, ClusterWizardStepValidationsAlert } from '../../../../common'; +import { ClusterWizardStepHostStatusDeterminationObject } from '../../../../common/types/hosts'; +import ValidationsRunningAlert from '../../common/ValidationsRunningAlert'; +import { ClusterWizardStepsType, wizardStepsValidationsMap } from '../wizardTransition'; + +type ValidationSectionProps = { + requireProxy?: boolean; + currentStepId: ClusterWizardStepsType; + validationsInfo?: ValidationsInfo; + clusterStatus?: Cluster['status']; + hosts: ClusterWizardStepHostStatusDeterminationObject[]; + children: React.ReactNode; +}; + +export const ValidationSection = ({ + currentStepId, + clusterStatus, + validationsInfo, + hosts, + children, +}: ValidationSectionProps) => { + return ( + + {children} + {clusterStatus && ( + + + + )} + + ); +}; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/constants.ts b/libs/ui-lib/lib/cim/components/ClusterDeployment/constants.ts index 2c7f4e7e29..4fdbbf7af1 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/constants.ts +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/constants.ts @@ -10,8 +10,10 @@ export const clusterHostsSelectionLabel = (t: TFunction): { [key in string]: str export const wizardStepNames = ( t: TFunction, ): { - [key in ClusterDeploymentWizardStepsType]: string; + [key in ClusterDeploymentWizardStepsType | 'installation-type' | 'automation']: string; } => ({ + 'installation-type': t('ai:Installation type'), + automation: t('ai:Automation'), 'cluster-details': t('ai:Cluster details'), 'hosts-selection': t('ai:Cluster hosts'), 'hosts-discovery': t('ai:Cluster hosts'), diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/index.ts b/libs/ui-lib/lib/cim/components/ClusterDeployment/index.ts index 42cdcda25e..835ddc895d 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/index.ts +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/index.ts @@ -4,17 +4,15 @@ export * from './AdditionalNTPSourcesDialogToggle'; export * from './LogsDownloadButton'; export * from './wizardTransition'; export * from './networkConfigurationValidation'; +export * from './ClusterDeploymentWizard'; export { default as ClusterDeploymentValidationsOverview } from './ClusterDeploymentValidationsOverview'; export { default as ClusterDeploymentDetails } from './ClusterDeploymentDetails'; export { default as ClusterDeploymentProgress } from './ClusterDeploymentProgress'; export { default as ClusterDeploymentCredentials } from './ClusterDeploymentCredentials'; export { default as ClusterDeploymentKubeconfigDownload } from './ClusterDeploymentKubeconfigDownload'; -export { default as ClusterDeploymentWizard } from './ClusterDeploymentWizard'; export { default as ClusterInstallationError } from './ClusterInstallationError'; export { default as NetworkConfiguration } from './NetworkConfiguration'; - export { default as ACMClusterDeploymentDetailsStep } from './ACMClusterDeploymentDetailsStep'; -export { default as ClusterDeploymentCreateProgress } from './ClusterDeploymentCreateProgress'; export { getTotalCompute } from './ShortCapacitySummary'; diff --git a/libs/ui-lib/lib/cim/components/ClusterDeployment/types.ts b/libs/ui-lib/lib/cim/components/ClusterDeployment/types.ts index 6e6e1cc619..37dd891c02 100644 --- a/libs/ui-lib/lib/cim/components/ClusterDeployment/types.ts +++ b/libs/ui-lib/lib/cim/components/ClusterDeployment/types.ts @@ -84,7 +84,6 @@ export type ScaleUpFormValues = Omit< export type ClusterDeploymentDetailsStepProps = ClusterDeploymentDetailsProps & { onSaveDetails: (values: ClusterDeploymentDetailsValues) => Promise; - onClose: () => void; isPreviewOpen: boolean; isNutanix: boolean; }; @@ -97,7 +96,6 @@ export type ClusterDeploymentDetailsNetworkingProps = Pick< agentClusterInstall: AgentClusterInstallK8sResource; agents: AgentK8sResource[]; onSaveNetworking: (values: ClusterDeploymentNetworkingValues) => Promise; - onClose: () => void; fetchInfraEnv: (name: string, namespace: string) => Promise; isPreviewOpen: boolean; isNutanix: boolean; @@ -114,7 +112,6 @@ export type ClusterDeploymentHostSelectionStepProps = Omit< 'onHostSelect' | 'onAutoSelectChange' > & { onSaveHostsSelection: (values: ClusterDeploymentHostsSelectionValues) => Promise; - onClose: () => void; }; export type ClusterDeploymentHostsDiscoveryStepProps = Omit< @@ -122,7 +119,6 @@ export type ClusterDeploymentHostsDiscoveryStepProps = Omit< 'usedHostnames' > & { onSaveHostsDiscovery: () => Promise; - onClose: () => void; }; export type ClusterDeploymentWizardProps = {