From 581ce4d5604de94da2a91d8bee78e949baf257e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Ka=C5=88kovsk=C3=BD?= Date: Mon, 20 Nov 2023 14:44:56 +0100 Subject: [PATCH] Port deprecated Wizard component to new PF5 implementation --- src/components/AnacondaWizard.jsx | 433 +++++++++++------- src/components/review/ReviewConfiguration.jsx | 4 +- 2 files changed, 262 insertions(+), 175 deletions(-) diff --git a/src/components/AnacondaWizard.jsx b/src/components/AnacondaWizard.jsx index c90537d9d5..42521ccf44 100644 --- a/src/components/AnacondaWizard.jsx +++ b/src/components/AnacondaWizard.jsx @@ -25,17 +25,16 @@ import { PageSection, PageSectionTypes, PageSectionVariants, - Stack -} from "@patternfly/react-core"; -import { + Stack, + useWizardContext, Wizard, - WizardFooter, - WizardContextConsumer -} from "@patternfly/react-core/deprecated"; + WizardFooterWrapper, + WizardStep +} from "@patternfly/react-core"; import { AnacondaPage } from "./AnacondaPage.jsx"; import { InstallationMethod, getPageProps as getInstallationMethodProps } from "./storage/InstallationMethod.jsx"; -import { getDefaultScenario } from "./storage/InstallationScenario.jsx"; +import { getDefaultScenario, getScenario } from "./storage/InstallationScenario.jsx"; import { MountPointMapping, getPageProps as getMountPointMappingProps } from "./storage/MountPointMapping.jsx"; import { DiskEncryption, getStorageEncryptionState, getPageProps as getDiskEncryptionProps } from "./storage/DiskEncryption.jsx"; import { InstallationLanguage, getPageProps as getInstallationLanguageProps } from "./localization/InstallationLanguage.jsx"; @@ -43,7 +42,6 @@ import { Accounts, getPageProps as getAccountsProps, getAccountsState, accountsT import { InstallationProgress } from "./installation/InstallationProgress.jsx"; import { ReviewConfiguration, ReviewConfigurationConfirmModal, getPageProps as getReviewConfigurationProps } from "./review/ReviewConfiguration.jsx"; import { exitGui } from "../helpers/exit.js"; -import { usePageLocation } from "hooks"; import { getRequiredMountPoints, } from "../apis/storage_devicetree.js"; @@ -69,6 +67,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim const [storageScenarioId, setStorageScenarioId] = useState(window.sessionStorage.getItem("storage-scenario-id") || getDefaultScenario().id); const [accounts, setAccounts] = useState(getAccountsState()); const [showWizard, setShowWizard] = useState(true); + const [isStepDisabled, setIsStepDisabled] = useState({}); const osRelease = useContext(OsReleaseContext); const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO"; @@ -103,74 +102,120 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim } } }, [localizationData]); - const stepsOrder = [ - { - component: InstallationLanguage, - data: { dispatch, languages: localizationData.languages, language: localizationData.language, commonLocales: localizationData.commonLocales }, - ...getInstallationLanguageProps({ isBootIso, osRelease }) - }, - { - component: InstallationMethod, - data: { - deviceData: storageData.devices, - diskSelection: storageData.diskSelection, - dispatch, - storageScenarioId, - setStorageScenarioId: (scenarioId) => { - window.sessionStorage.setItem("storage-scenario-id", scenarioId); - setStorageScenarioId(scenarioId); - } + const stepsOrder = useMemo(() => { + return [ + { + component: InstallationLanguage, + data: { dispatch, languages: localizationData.languages, language: localizationData.language, commonLocales: localizationData.commonLocales }, + ...getInstallationLanguageProps({ isBootIso, osRelease }) }, - ...getInstallationMethodProps({ isBootIso, osRelease, isFormValid }) - }, - { - id: "disk-configuration", - label: _("Disk configuration"), - steps: [{ - component: MountPointMapping, + { + component: InstallationMethod, data: { deviceData: storageData.devices, diskSelection: storageData.diskSelection, dispatch, - partitioningData: storageData.partitioning, - requiredMountPoints, - reusePartitioning, - setReusePartitioning, + storageScenarioId, + setStorageScenarioId: (scenarioId) => { + window.sessionStorage.setItem("storage-scenario-id", scenarioId); + setStorageScenarioId(scenarioId); + } }, - ...getMountPointMappingProps({ storageScenarioId }) - }, { - component: DiskEncryption, + ...getInstallationMethodProps({ isBootIso, osRelease, isFormValid }) + }, + { + id: "disk-configuration", + label: _("Disk configuration"), + steps: [{ + component: MountPointMapping, + data: { + deviceData: storageData.devices, + diskSelection: storageData.diskSelection, + dispatch, + partitioningData: storageData.partitioning, + requiredMountPoints, + reusePartitioning, + setReusePartitioning, + }, + ...getMountPointMappingProps({ storageScenarioId }) + }, { + component: DiskEncryption, + data: { + storageEncryption, + setStorageEncryption, + passwordPolicies: runtimeData.passwordPolicies, + }, + ...getDiskEncryptionProps({ storageScenarioId }) + }] + }, + { + component: Accounts, data: { - storageEncryption, - setStorageEncryption, + accounts, + setAccounts, passwordPolicies: runtimeData.passwordPolicies, }, - ...getDiskEncryptionProps({ storageScenarioId }) - }] - }, - { - component: Accounts, - data: { - accounts, - setAccounts, - passwordPolicies: runtimeData.passwordPolicies, + ...getAccountsProps({ isBootIso }) }, - ...getAccountsProps({ isBootIso }) - }, - { - component: ReviewConfiguration, - data: { - deviceData: storageData.devices, - diskSelection: storageData.diskSelection, - requests: storageData.partitioning ? storageData.partitioning.requests : null, - language, - localizationData, - storageScenarioId, - accounts, + { + component: ReviewConfiguration, + data: { + deviceData: storageData.devices, + diskSelection: storageData.diskSelection, + requests: storageData.partitioning ? storageData.partitioning.requests : null, + language, + localizationData, + storageScenarioId, + accounts, + }, + ...getReviewConfigurationProps({ storageScenarioId }) }, - ...getReviewConfigurationProps({ storageScenarioId }) - }, - ]; + ]; + }, [accounts, dispatch, isBootIso, isFormValid, language, localizationData, osRelease, requiredMountPoints, reusePartitioning, runtimeData.passwordPolicies, storageData.devices, storageData.diskSelection, storageData.partitioning, storageEncryption, storageScenarioId]); + + useEffect(() => { + setIsStepDisabled(prevState => { + const updatedState = { ...prevState }; + + const updateStateForStep = (step, index, isSubStep = false) => { + if (updatedState[step.id] === undefined) { + updatedState[step.id] = isSubStep || index !== 0; + } + + if (step.steps) { + step.steps.forEach((subStep, subIndex) => { + updateStateForStep(subStep, subIndex, true); + }); + } + }; + + stepsOrder.forEach((step, index) => { + updateStateForStep(step, index); + }); + Object.keys(updatedState).forEach(stepId => { + if (!stepsOrder.some(step => step.id === stepId || (step.steps && step.steps.some(subStep => subStep.id === stepId)))) { + delete updatedState[stepId]; + } + }); + + return updatedState; + }); + }, [stepsOrder]); + + const updateStepDisabledState = (currentStepId) => { + const newStepDisabledState = { ...isStepDisabled }; + Object.keys(newStepDisabledState).forEach(stepId => { + newStepDisabledState[stepId] = isStepFollowedBy(currentStepId, stepId); + }); + setIsStepDisabled(newStepDisabledState); + }; + + const componentProps = { + isFormDisabled, + onCritFail, + setIsFormDisabled, + setIsFormValid, + }; const getFlattenedStepsIds = (steps) => { const stepIds = []; @@ -189,9 +234,39 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim }; const flattenedStepsIds = getFlattenedStepsIds(stepsOrder); - const { path } = usePageLocation(); - const firstStepId = stepsOrder.filter(step => !step.isHidden)[0].id; - const currentStepId = path[0] || firstStepId; + const findPreviousVisibleStep = (currentStepId) => { + let previousVisibleStepId = null; + + for (let i = 0; i < flattenedStepsIds.length; i++) { + const step = flattenedStepsIds[i]; + if (step === currentStepId) { + break; + } + previousVisibleStepId = step; + } + + return previousVisibleStepId; + }; + + const findNextVisibleStep = (currentStepId) => { + let nextVisibleStepId = null; + let foundCurrentStep = false; + + for (let i = 0; i < flattenedStepsIds.length; i++) { + const step = flattenedStepsIds[i]; + + if (foundCurrentStep) { + nextVisibleStepId = step; + break; + } + + if (step === currentStepId) { + foundCurrentStep = true; + continue; + } + } + return nextVisibleStepId; + }; const isStepFollowedBy = (earlierStepId, laterStepId) => { const earlierStepIdx = flattenedStepsIds.findIndex(s => s === earlierStepId); @@ -199,62 +274,64 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim return earlierStepIdx < laterStepIdx; }; - const canJumpToStep = (stepId, currentStepId) => { - return stepId === currentStepId || isStepFollowedBy(stepId, currentStepId); - }; - - const createSteps = (stepsOrder) => { - const steps = stepsOrder.filter(s => !s.isHidden).map(s => { - let step = ({ + const createSteps = (stepsOrder, componentProps) => { + return stepsOrder.map(s => { + let stepProps = { id: s.id, + isHidden: s.isHidden, + isDisabled: isStepDisabled[s.id], name: s.label, stepNavItemProps: { id: s.id }, - canJumpTo: canJumpToStep(s.id, currentStepId), - }); + ...(s.steps?.length && { isExpandable: true }), + }; if (s.component) { - step = ({ - ...step, - component: ( + stepProps = { + children: ( setStepNotification({ step: s.id, ...ex })} - isFormDisabled={isFormDisabled} - setIsFormDisabled={setIsFormDisabled} + {...componentProps} {...s.data} /> ), - }); + ...stepProps + }; } else if (s.steps) { - step.steps = createSteps(s.steps); + const subSteps = createSteps(s.steps, componentProps); + stepProps = { + ...stepProps, + steps: [...subSteps] + }; } - return step; + return ( + + ); }); - return steps; }; - const steps = createSteps(stepsOrder); + const steps = createSteps(stepsOrder, componentProps); const goToStep = (newStep, prevStep) => { - if (prevStep.prevId !== newStep.id) { + if (prevStep.id !== newStep.id) { // first reset validation state to default setIsFormValid(false); } // Reset the applied partitioning when going back from a step after creating partitioning to a step // before creating partitioning. - if ((prevStep.prevId === "accounts" || isStepFollowedBy("accounts", prevStep.prevId)) && + if ((prevStep.id === "accounts" || isStepFollowedBy("accounts", prevStep.id)) && isStepFollowedBy(newStep.id, "accounts")) { setIsFormDisabled(true); resetPartitioning() .then( + () => updateStepDisabledState(newStep.id), () => cockpit.location.go([newStep.id]), () => onCritFail({ context: cockpit.format(N_("Error was hit when going back from $0."), prevStep.prevName) }) ) .always(() => setIsFormDisabled(false)); } else { + updateStepDisabledState(newStep.id); cockpit.location.go([newStep.id]); } }; @@ -267,11 +344,17 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim ); } + const firstVisibleStepIndex = steps.findIndex(step => !step.props.isHidden) + 1; + return ( } - hideClose - mainAriaLabel={`${title} content`} - navAriaLabel={`${title} steps`} - onBack={goToStep} - onGoToStep={goToStep} - onNext={goToStep} - steps={steps} - isNavExpandable - /> + onStepChange={((event, currentStep, prevStep) => goToStep(currentStep, prevStep))} + > + {steps} + ); }; const Footer = ({ + findNextVisibleStep, + findPreviousVisibleStep, onCritFail, isFormValid, setIsFormValid, @@ -310,13 +391,19 @@ const Footer = ({ stepsOrder, storageEncryption, storageScenarioId, + updateStepDisabledState, accounts, }) => { const [nextWaitsConfirmation, setNextWaitsConfirmation] = useState(false); const [quitWaitsConfirmation, setQuitWaitsConfirmation] = useState(false); + const { activeStep, goToNextStep, goToPrevStep } = useWizardContext(); const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO"; - const goToNextStep = (activeStep, onNext) => { + const onNext = (activeStep, goToNextStep) => { + const nextStepAction = () => { + updateStepDisabledState(findNextVisibleStep(activeStep.id)); + goToNextStep(); + }; // first reset validation state to default setIsFormValid(true); @@ -330,7 +417,7 @@ const Footer = ({ setStepNotification({ step: activeStep.id, ...ex }); }, onSuccess: () => { - onNext(); + nextStepAction(); // Reset the state after the onNext call. Otherwise, // React will try to render the current step again. @@ -353,7 +440,7 @@ const Footer = ({ setStepNotification({ step: activeStep.id, ...ex }); }, onSuccess: () => { - onNext(); + nextStepAction(); // Reset the state after the onNext call. Otherwise, // React will try to render the current step again. @@ -366,85 +453,85 @@ const Footer = ({ .then(cryptedPassword => { const users = accountsToDbusUsers({ ...accounts, password: cryptedPassword }); setUsers(users); - onNext(); + nextStepAction(); }, onCritFail({ context: N_("Password ecryption failed.") })); } else { - onNext(); + nextStepAction(); } }; - const goToPreviousStep = (activeStep, onBack, errorHandler) => { + const onBack = (goToPrevStep, errorHandler) => { // first reset validation state to default - setIsFormValid(true); - onBack(); + setIsFormValid(false); + updateStepDisabledState(findPreviousVisibleStep(activeStep.id)); + goToPrevStep(); }; + const isFirstScreen = ( + activeStep.id === "installation-language" || (activeStep.id === "installation-method" && !isBootIso) + ); + + const nextButtonText = ( + activeStep.id === "installation-review" + ? getScenario(storageScenarioId).buttonLabel + : _("Next") + ); + + const footerHelperText = stepsOrder.find(step => step.id === activeStep.id)?.footerHelperText; + return ( - - - {({ activeStep, onNext, onBack }) => { - const currentStep = stepsOrder.find(s => s.id === activeStep.id); - const footerHelperText = currentStep?.footerHelperText; - const isFirstScreen = stepsOrder.filter(step => !step.isHidden)[0].id === activeStep.id; - const nextButtonText = currentStep?.nextButtonText || _("Next"); - const nextButtonVariant = currentStep?.nextButtonVariant || "primary"; - - return ( - - {activeStep.id === "installation-review" && - nextWaitsConfirmation && - { setShowWizard(false); cockpit.location.go(["installation-progress"]) }} - setNextWaitsConfirmation={setNextWaitsConfirmation} - storageScenarioId={storageScenarioId} - />} - {quitWaitsConfirmation && - } - {footerHelperText} - - - - - - - ); - }} - - + + + {activeStep.id === "installation-review" && + nextWaitsConfirmation && + { setShowWizard(false); cockpit.location.go(["installation-progress"]) }} + setNextWaitsConfirmation={setNextWaitsConfirmation} + storageScenarioId={storageScenarioId} + />} + {quitWaitsConfirmation && + } + {footerHelperText} + + + + + + + ); }; diff --git a/src/components/review/ReviewConfiguration.jsx b/src/components/review/ReviewConfiguration.jsx index 78cbb7d95a..33e4246cba 100644 --- a/src/components/review/ReviewConfiguration.jsx +++ b/src/components/review/ReviewConfiguration.jsx @@ -187,7 +187,7 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, language, local ); }; -export const ReviewConfigurationConfirmModal = ({ idPrefix, onNext, setNextWaitsConfirmation, storageScenarioId }) => { +export const ReviewConfigurationConfirmModal = ({ idPrefix, goToNextStep, setNextWaitsConfirmation, storageScenarioId }) => { const scenario = getScenario(storageScenarioId); return ( { setNextWaitsConfirmation(false); - onNext(); + goToNextStep(); }} variant={scenario.buttonVariant} >