import React, { useEffect, useState } from 'react'; import { Button, Wizard, WizardFooterWrapper, WizardNavItem, WizardStep, useWizardContext, PageSection, PageSectionTypes, Flex, } from '@patternfly/react-core'; import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizard'; import useChrome from '@redhat-cloud-services/frontend-components/useChrome'; import { useNavigate, useSearchParams } from 'react-router-dom'; import DetailsStep from './steps/Details'; import FileSystemStep from './steps/FileSystem'; import { FileSystemContext } from './steps/FileSystem/FileSystemTable'; import FirewallStep from './steps/Firewall'; import FirstBootStep from './steps/FirstBoot'; import HostnameStep from './steps/Hostname'; import ImageOutputStep from './steps/ImageOutput'; import KernelStep from './steps/Kernel'; import LocaleStep from './steps/Locale'; import OscapStep from './steps/Oscap'; import PackagesStep from './steps/Packages'; import RegistrationStep from './steps/Registration'; import RepositoriesStep from './steps/Repositories'; import ReviewStep from './steps/Review'; import ReviewWizardFooter from './steps/Review/Footer/Footer'; import ServicesStep from './steps/Services'; import SnapshotStep from './steps/Snapshot'; import Aws from './steps/TargetEnvironment/Aws'; import Azure from './steps/TargetEnvironment/Azure'; import Gcp from './steps/TargetEnvironment/Gcp'; import TimezoneStep from './steps/Timezone'; import UsersStep from './steps/Users'; import { getHostArch, getHostDistro } from './utilities/getHostInfo'; import { useFilesystemValidation, useSnapshotValidation, useFirstBootValidation, useDetailsValidation, useRegistrationValidation, useHostnameValidation, useKernelValidation, useUsersValidation, useTimezoneValidation, useFirewallValidation, useServicesValidation, useLocaleValidation, } from './utilities/useValidation'; import { isAwsAccountIdValid, isAzureTenantGUIDValid, isAzureSubscriptionIdValid, isAzureResourceGroupValid, isGcpEmailValid, } from './validators'; import { RHEL_8, RHEL_10_BETA, RHEL_10, AARCH64, CENTOS_9, AMPLITUDE_MODULE_NAME, RHEL_9, } from '../../constants'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import './CreateImageWizard.scss'; import { changeDistribution, changeArchitecture, initializeWizard, selectAwsAccountId, selectAwsShareMethod, selectAwsSourceId, selectAzureResourceGroup, selectAzureShareMethod, selectAzureSource, selectAzureSubscriptionId, selectAzureTenantId, selectDistribution, selectGcpEmail, selectGcpShareMethod, selectImageTypes, addImageType, changeRegistrationType, } from '../../store/wizardSlice'; import isRhel from '../../Utilities/isRhel'; import { resolveRelPath } from '../../Utilities/path'; import { useFlag, useGetEnvironment } from '../../Utilities/useGetEnvironment'; import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader'; type CustomWizardFooterPropType = { disableBack?: boolean; disableNext: boolean; beforeNext?: () => boolean; optional?: boolean; }; export const CustomWizardFooter = ({ disableBack: disableBack, disableNext: disableNext, beforeNext, optional: optional, }: CustomWizardFooterPropType) => { const { goToNextStep, goToPrevStep, goToStepById, close, activeStep } = useWizardContext(); const { analytics } = useChrome(); const nextBtnID = 'wizard-next-btn'; const backBtnID = 'wizard-back-btn'; const reviewAndFinishBtnID = 'wizard-review-and-finish-btn'; const cancelBtnID = 'wizard-cancel-btn'; return ( {optional && ( )} ); }; type CreateImageWizardProps = { isEdit?: boolean; }; const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => { const { analytics, isBeta } = useChrome(); const navigate = useNavigate(); const dispatch = useAppDispatch(); const [searchParams] = useSearchParams(); const { isFedoraEnv } = useGetEnvironment(); // Feature flags const complianceEnabled = useFlag('image-builder.compliance.enabled'); // IMPORTANT: Ensure the wizard starts with a fresh initial state useEffect(() => { dispatch(initializeWizard()); if (isFedoraEnv) { dispatch(changeDistribution(CENTOS_9)); dispatch(changeRegistrationType('register-later')); } if (searchParams.get('release') === 'rhel8') { dispatch(changeDistribution(RHEL_8)); } if (searchParams.get('release') === 'rhel9') { dispatch(changeDistribution(RHEL_9)); } if (searchParams.get('release') === 'rhel10beta') { dispatch(changeDistribution(RHEL_10_BETA)); } if (searchParams.get('release') === 'rhel10') { dispatch(changeDistribution(RHEL_10)); } if (searchParams.get('arch') === AARCH64) { dispatch(changeArchitecture(AARCH64)); } if (searchParams.get('target') === 'iso') { dispatch(addImageType('image-installer')); } if (searchParams.get('target') === 'qcow2') { dispatch(addImageType('guest-image')); } const initializeHostDistro = async () => { const distro = await getHostDistro(); dispatch(changeDistribution(distro)); }; const initializeHostArch = async () => { const arch = await getHostArch(); dispatch(changeArchitecture(arch)); }; if (process.env.IS_ON_PREMISE && !isEdit) { if (!searchParams.get('release')) { initializeHostDistro(); } initializeHostArch(); } // This useEffect hook should run *only* on mount and therefore has an empty // dependency array. eslint's exhaustive-deps rule does not support this use. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /* * * Selectors * * */ const distribution = useAppSelector(selectDistribution); // Image Output const targetEnvironments = useAppSelector(selectImageTypes); // AWS const awsShareMethod = useAppSelector(selectAwsShareMethod); const awsAccountId = useAppSelector(selectAwsAccountId); const awsSourceId = useAppSelector(selectAwsSourceId); // GCP const gcpShareMethod = useAppSelector(selectGcpShareMethod); const gcpEmail = useAppSelector(selectGcpEmail); // AZURE const azureShareMethod = useAppSelector(selectAzureShareMethod); const azureTenantId = useAppSelector(selectAzureTenantId); const azureSubscriptionId = useAppSelector(selectAzureSubscriptionId); const azureResourceGroup = useAppSelector(selectAzureResourceGroup); const azureSource = useAppSelector(selectAzureSource); // Registration const registrationValidation = useRegistrationValidation(); // Snapshots const snapshotValidation = useSnapshotValidation(); // Filesystem const [filesystemPristine, setFilesystemPristine] = useState(true); const fileSystemValidation = useFilesystemValidation(); // Timezone const timezoneValidation = useTimezoneValidation(); // Locale const localeValidation = useLocaleValidation(); // Hostname const hostnameValidation = useHostnameValidation(); // Kernel const kernelValidation = useKernelValidation(); // Firewall const firewallValidation = useFirewallValidation(); // Services const servicesValidation = useServicesValidation(); // Firstboot const firstBootValidation = useFirstBootValidation(); // Details const detailsValidation = useDetailsValidation(); // Users const usersValidation = useUsersValidation(); let startIndex = 1; // default index if (isEdit) { startIndex = 22; } const [wasRegisterVisited, setWasRegisterVisited] = useState(false); // Duplicating some of the logic from the Wizard component to allow for custom nav items status // for original code see https://github.com/patternfly/patternfly-react/blob/184c55f8d10e1d94ffd72e09212db56c15387c5e/packages/react-core/src/components/Wizard/WizardNavInternal.tsx#L128 const CustomStatusNavItem = ( step: WizardStepType, activeStep: WizardStepType, steps: WizardStepType[], goToStepByIndex: (index: number) => void ) => { const isVisitOptional = 'parentId' in step && step.parentId === 'step-optional-steps'; useEffect(() => { if (process.env.IS_ON_PREMISE) { if (step.id === 'step-oscap' && step.isVisited) { setWasRegisterVisited(true); } } else if (step.id === 'step-register' && step.isVisited) { setWasRegisterVisited(true); } }, [step.id, step.isVisited]); const hasVisitedNextStep = steps.some( (s) => s.index > step.index && s.isVisited ); // Only this code is different from the original const status = (step?.id !== activeStep?.id && step?.status) || 'default'; return ( { goToStepByIndex(step.index); if (isEdit && step.id === 'wizard-additional-packages') { analytics.track( `${AMPLITUDE_MODULE_NAME} - Additional Packages Revisited in Edit`, { module: AMPLITUDE_MODULE_NAME, isPreview: isBeta(), } ); } }} status={status} /> ); }; return ( <> navigate(resolveRelPath(''))} isVisitRequired > } > target === 'aws' || target === 'gcp' || target === 'azure' ) } steps={[ } isHidden={!targetEnvironments.includes('aws')} > , } isHidden={!targetEnvironments.includes('gcp')} > , } isHidden={!targetEnvironments.includes('azure')} > , ]} /> } > , } > , { if (fileSystemValidation.disabledNext) { setFilesystemPristine(false); return false; } return true; }} disableNext={ !filesystemPristine && fileSystemValidation.disabledNext } optional={true} /> } > , } > , } > , } > , } > , } > , } > , } > , } > , } > , } > , } > , ]} /> } > } > ); }; export default CreateImageWizard;