debian-image-builder-frontend/src/Components/CreateImageWizard/CreateImageWizard.tsx
regexowl 2caddb66c7 Wizard: "Always blue" update to the Registration step
This updates the Registration step as per recent mocks.

There is now only one checkbox which switches between (register everything now / register later). When the checkbox is not checked "register later" option gets selected, activation key dropdown gets disabled and information about activation keys is hidden to support the fact that no activation key will be used.

When the checkbox is checked it either loads previously used activation key in the case of "on edit" scenario, looks for a recently used activation key in localStorage or in a case there are no activationKeys automatically creates and selects one.

The Next button gets disabled only when "register-now" is selected, but there is no activationKey selected. As the option to clear the dropdown got removed this should occur only when fetching/creating activation key.
2024-08-14 15:26:08 +02:00

448 lines
14 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import {
Button,
Wizard,
WizardFooterWrapper,
WizardNavItem,
WizardStep,
useWizardContext,
PageSection,
} from '@patternfly/react-core';
import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizard';
import { useFlag } from '@unleash/proxy-client-react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import DetailsStep from './steps/Details';
import FileSystemStep, { FileSystemContext } from './steps/FileSystem';
import FirstBootStep from './steps/FirstBoot';
import ImageOutputStep from './steps/ImageOutput';
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 SnapshotStep from './steps/Snapshot';
import Aws from './steps/TargetEnvironment/Aws';
import Azure from './steps/TargetEnvironment/Azure';
import Gcp from './steps/TargetEnvironment/Gcp';
import {
useFilesystemValidation,
useFirstBootValidation,
useDetailsValidation,
} from './utilities/useValidation';
import {
isAwsAccountIdValid,
isAzureTenantGUIDValid,
isAzureSubscriptionIdValid,
isAzureResourceGroupValid,
isGcpEmailValid,
} from './validators';
import { RHEL_8, AARCH64 } from '../../constants';
import { useListFeaturesQuery } from '../../store/contentSourcesApi';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import './CreateImageWizard.scss';
import {
changeDistribution,
changeArchitecture,
initializeWizard,
selectAwsAccountId,
selectAwsShareMethod,
selectAwsSourceId,
selectAzureResourceGroup,
selectAzureShareMethod,
selectAzureSource,
selectAzureSubscriptionId,
selectAzureTenantId,
selectGcpEmail,
selectGcpShareMethod,
selectImageTypes,
addImageType,
selectSnapshotDate,
selectUseLatest,
selectRegistrationType,
selectActivationKey,
} from '../../store/wizardSlice';
import { resolveRelPath } from '../../Utilities/path';
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
type CustomWizardFooterPropType = {
disableBack?: boolean;
disableNext: boolean;
beforeNext?: () => boolean;
};
export const CustomWizardFooter = ({
disableBack: disableBack,
disableNext: disableNext,
beforeNext,
}: CustomWizardFooterPropType) => {
const { goToNextStep, goToPrevStep, close } = useWizardContext();
return (
<WizardFooterWrapper>
<Button
ouiaId="wizard-next-btn"
variant="primary"
onClick={() => {
if (!beforeNext || beforeNext()) goToNextStep();
}}
isDisabled={disableNext}
>
Next
</Button>
<Button
ouiaId="wizard-back-btn"
variant="secondary"
onClick={goToPrevStep}
isDisabled={disableBack}
>
Back
</Button>
<Button ouiaId="wizard-cancel-btn" variant="link" onClick={close}>
Cancel
</Button>
</WizardFooterWrapper>
);
};
type CreateImageWizardProps = {
isEdit?: boolean;
};
const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [searchParams] = useSearchParams();
// Remove this and all fallthrough logic when snapshotting is enabled in Prod-stable
// =========================TO REMOVE=======================
const { data, isSuccess, isFetching, isError } =
useListFeaturesQuery(undefined);
const snapshotsFlag = useFlag('image-builder.snapshots.enabled');
const snapshottingEnabled = useMemo(() => {
if (!snapshotsFlag) return false;
// The below checks if other environments permit the snapshot step
return !(
!isError &&
!isFetching &&
isSuccess &&
data?.snapshots?.accessible === false &&
data?.snapshots?.enabled === false
);
}, [data, isSuccess, isFetching, isError, snapshotsFlag]);
// =========================TO REMOVE=======================
const isFirstBootEnabled = useFlag('image-builder.firstboot.enabled');
// IMPORTANT: Ensure the wizard starts with a fresh initial state
useEffect(() => {
dispatch(initializeWizard());
if (searchParams.get('release') === 'rhel8') {
dispatch(changeDistribution(RHEL_8));
}
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'));
}
// 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 *
* */
// 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 registrationType = useAppSelector(selectRegistrationType);
const activationKey = useAppSelector(selectActivationKey);
// Snapshots
const snapshotDate = useAppSelector(selectSnapshotDate);
const useLatest = useAppSelector(selectUseLatest);
const snapshotStepRequiresChoice = !useLatest && !snapshotDate;
// Filesystem
const [filesystemPristine, setFilesystemPristine] = useState(true);
const fileSystemValidation = useFilesystemValidation();
// Firstboot
const firstBootValidation = useFirstBootValidation();
// Details
const detailsValidation = useDetailsValidation();
let startIndex = 1; // default index
if (isEdit) {
startIndex = 15;
}
// 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 isVisitRequired = true;
const hasVisitedNextStep = steps.some(
(s) => s.index > step.index && s.isVisited
);
// Only this code is different from the original
const status =
(step.isVisited && step.id !== activeStep?.id && step.status) ||
'default';
return (
<WizardNavItem
key={step.id}
id={step.id}
content={step.name}
isCurrent={activeStep?.id === step.id}
isDisabled={
step.isDisabled ||
(isVisitRequired && !step.isVisited && !hasVisitedNextStep)
}
isVisited={step.isVisited}
stepIndex={step.index}
onClick={() => goToStepByIndex(step.index)}
status={status}
/>
);
};
return (
<>
<ImageBuilderHeader inWizard />
<PageSection isWidthLimited>
<Wizard
startIndex={startIndex}
onClose={() => navigate(resolveRelPath(''))}
isVisitRequired
>
<WizardStep
name="Image output"
id="step-image-output"
footer={
<CustomWizardFooter
disableNext={targetEnvironments.length === 0}
disableBack={true}
/>
}
>
<ImageOutputStep />
</WizardStep>
<WizardStep
name="Target Environment"
id="step-target-environment"
isHidden={
!targetEnvironments.find(
(target) =>
target === 'aws' || target === 'gcp' || target === 'azure'
)
}
steps={[
<WizardStep
name="Amazon Web Services"
id="wizard-target-aws"
key="wizard-target-aws"
footer={
<CustomWizardFooter
disableNext={
awsShareMethod === 'manual'
? !isAwsAccountIdValid(awsAccountId)
: awsSourceId === undefined
}
/>
}
isHidden={!targetEnvironments.includes('aws')}
>
<Aws />
</WizardStep>,
<WizardStep
name="Google Cloud Platform"
id="wizard-target-gcp"
key="wizard-target-gcp"
footer={
<CustomWizardFooter
disableNext={
gcpShareMethod === 'withGoogle' &&
!isGcpEmailValid(gcpEmail)
}
/>
}
isHidden={!targetEnvironments.includes('gcp')}
>
<Gcp />
</WizardStep>,
<WizardStep
name="Azure"
id="wizard-target-azure"
key="wizard-target-azure"
footer={
<CustomWizardFooter
disableNext={
azureShareMethod === 'manual'
? !isAzureTenantGUIDValid(azureTenantId) ||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
!isAzureResourceGroupValid(azureResourceGroup)
: azureShareMethod === 'sources'
? !isAzureTenantGUIDValid(azureTenantId) ||
!isAzureSubscriptionIdValid(azureSubscriptionId) ||
!isAzureResourceGroupValid(azureResourceGroup)
: azureSource === undefined
}
/>
}
isHidden={!targetEnvironments.includes('azure')}
>
<Azure />
</WizardStep>,
]}
/>
<WizardStep
name="Register"
id="step-register"
footer={
<CustomWizardFooter
disableNext={
registrationType === 'register-now' && !activationKey
}
/>
}
>
<RegistrationStep />
</WizardStep>
<WizardStep
name="OpenSCAP"
id="step-oscap"
footer={<CustomWizardFooter disableNext={false} />}
>
<OscapStep />
</WizardStep>
<WizardStep
name="File system configuration"
id="step-file-system"
footer={
<CustomWizardFooter
beforeNext={() => {
if (fileSystemValidation.disabledNext) {
setFilesystemPristine(false);
return false;
}
return true;
}}
disableNext={
!filesystemPristine && fileSystemValidation.disabledNext
}
/>
}
>
<FileSystemContext.Provider value={filesystemPristine}>
<FileSystemStep />
</FileSystemContext.Provider>
</WizardStep>
<WizardStep
name="Content"
id="step-content"
steps={[
<WizardStep
name="Repository snapshot"
id="wizard-repository-snapshot"
key="wizard-repository-snapshot"
isHidden={!snapshottingEnabled}
footer={
<CustomWizardFooter
disableNext={snapshotStepRequiresChoice}
/>
}
>
<SnapshotStep />
</WizardStep>,
<WizardStep
name="Custom repositories"
id="wizard-custom-repositories"
key="wizard-custom-repositories"
isDisabled={snapshotStepRequiresChoice}
footer={<CustomWizardFooter disableNext={false} />}
>
<RepositoriesStep />
</WizardStep>,
<WizardStep
name="Additional packages"
id="wizard-additional-packages"
key="wizard-additional-packages"
isDisabled={snapshotStepRequiresChoice}
footer={<CustomWizardFooter disableNext={false} />}
>
<PackagesStep />
</WizardStep>,
]}
/>
<WizardStep
name="First boot script configuration"
id="wizard-first-boot"
key="wizard-first-boot"
navItem={customStatusNavItem}
status={firstBootValidation.disabledNext ? 'error' : 'default'}
isHidden={!isFirstBootEnabled}
footer={
<CustomWizardFooter
disableNext={firstBootValidation.disabledNext}
/>
}
>
<FirstBootStep />
</WizardStep>
<WizardStep
name="Details"
id={'step-details'}
isDisabled={snapshotStepRequiresChoice}
navItem={customStatusNavItem}
status={detailsValidation.disabledNext ? 'error' : 'default'}
footer={
<CustomWizardFooter
disableNext={detailsValidation.disabledNext}
/>
}
>
<DetailsStep />
</WizardStep>
<WizardStep
name="Review"
id="step-review"
isDisabled={snapshotStepRequiresChoice}
footer={<ReviewWizardFooter />}
>
{/* Intentional prop drilling for simplicity - To be removed */}
<ReviewStep snapshottingEnabled={snapshottingEnabled} />
</WizardStep>
</Wizard>
</PageSection>
</>
);
};
export default CreateImageWizard;