/* * Copyright (C) 2022 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import React, { useContext, useEffect, useMemo, useState } from "react"; import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js"; import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; import { Label } from "@patternfly/react-core/dist/esm/components/Label/index.js"; import { useWizardContext, useWizardFooter } from "@patternfly/react-core/dist/esm/components/Wizard/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { getDeviceChildren } from "../../helpers/storage.js"; import { OsReleaseContext, PageContext, PayloadContext, StorageContext, UserInterfaceContext, } from "../../contexts/Common.jsx"; import { useOriginalDevices, usePlannedActions, } from "../../hooks/Storage.jsx"; import { EmptyStatePanel } from "cockpit-components-empty-state"; import { AnacondaWizardFooter } from "../AnacondaWizardFooter.jsx"; import { DateAndTimeReviewDescription } from "../datetime/index.js"; import { usePageComplete as useDatetimePageComplete } from "../datetime/usePageComplete.js"; import { InstallationLanguageReviewDescription } from "../localization/index.js"; import { usePageComplete as useLocalizationPageComplete } from "../localization/usePageComplete.js"; import { usePageComplete as useSoftwarePageComplete } from "../software/usePageComplete.js"; import { StorageInstallationReviewSummary, StorageScenarioReviewDescription, } from "../storage/index.js"; import { useScenario } from "../storage/installation-method/InstallationScenario.jsx"; import { usePageComplete as useStorageInstallationPageComplete } from "../storage/installation-method/usePageComplete.jsx"; import { AccountsReviewDescription } from "../users/index.js"; import { usePageComplete as useUsersPageComplete } from "../users/usePageComplete.jsx"; import { ReviewDescriptionListItem } from "./Common.jsx"; import { HostnameRow } from "./Hostname.jsx"; import "./ReviewConfiguration.scss"; const _ = cockpit.gettext; const SCREEN_ID = "anaconda-screen-review"; const IncompleteStepIndicator = () => ( ); const ReviewDescriptionList = ({ children }) => { return ( {children} ); }; export const ReviewConfiguration = ({ autoProceedBlockedRef, automatedInstall, goToProgressPage, pauseAtSummary }) => { const osRelease = useContext(OsReleaseContext); const userInterfaceConfig = useContext(UserInterfaceContext); const hiddenScreens = userInterfaceConfig.hidden_webui_pages || []; const { goToStepById } = useWizardContext(); const { appliedPartitioning } = useContext(StorageContext); const languagePageHidden = hiddenScreens.includes("anaconda-screen-language"); const localizationComplete = useLocalizationPageComplete({ automatedInstall, isHidden: languagePageHidden }); const datetimePageHidden = hiddenScreens.includes("anaconda-screen-date-time"); const datetimeComplete = useDatetimePageComplete({ automatedInstall, isHidden: datetimePageHidden }); const { environments, selection, type: payloadType } = useContext(PayloadContext) ?? {}; const softwarePageHidden = payloadType !== "DNF" || hiddenScreens.includes("anaconda-screen-software-selection"); const softwareSelectionComplete = useSoftwarePageComplete({ automatedInstall, isHidden: softwarePageHidden }); const { complete: storageComplete, validationPending: storageValidationPending, } = useStorageInstallationPageComplete(); const accountsPageHidden = hiddenScreens.includes("anaconda-screen-accounts"); const usersComplete = useUsersPageComplete({ isHidden: accountsPageHidden }); const pages = [ { complete: localizationComplete, id: "anaconda-screen-language" }, { complete: datetimeComplete, id: "anaconda-screen-date-time" }, { complete: softwareSelectionComplete, id: "anaconda-screen-software-selection" }, { complete: storageComplete, id: "anaconda-screen-method" }, { complete: usersComplete, id: "anaconda-screen-accounts" }, ]; const reviewValidationPending = pages.some(p => p.complete === undefined); const allValidatedReviewPagesComplete = pages.every(p => p.complete === true); const firstIncompleteStepId = !reviewValidationPending ? (pages.find(p => p.complete !== true)?.id ?? null) : null; const jumpToFirstIncomplete = () => { if (!firstIncompleteStepId) { return; } goToStepById(firstIncompleteStepId); }; // Auto-proceed to installation when kickstart is used without inst.pauseatsummary. // This is a one-shot check: once validations complete and any issue is found // (incomplete pages), auto-proceed is permanently disabled so the user must // manually click "Begin installation" after fixing the issue. // Storage warnings do NOT block auto-proceed, matching GTK/TUI behavior. useEffect(() => { if (!automatedInstall || pauseAtSummary || autoProceedBlockedRef.current) { return; } // Wait until all validations have finished if (reviewValidationPending || storageValidationPending) { return; } if (allValidatedReviewPagesComplete) { goToProgressPage(); } else { autoProceedBlockedRef.current = true; } }, [automatedInstall, goToProgressPage, pauseAtSummary, allValidatedReviewPagesComplete, storageValidationPending, autoProceedBlockedRef, reviewValidationPending]); // Display custom footer const getFooter = useMemo(() => ( ), [allValidatedReviewPagesComplete, goToProgressPage, reviewValidationPending]); useWizardFooter(getFooter); const languageDescription = localizationComplete ? : ; const timezoneDescription = datetimeComplete ? : ; const installationScenarioDescription = ; const softwareDescription = useMemo(() => { if (!softwareSelectionComplete) { return ; } const envId = selection?.environment; if (!envId) { return ""; } const env = environments?.find(e => e.id === envId); return env?.name || envId; }, [softwareSelectionComplete, environments, selection?.environment]); const storageDescription = ( {!storageComplete && ( )} {appliedPartitioning && } ); const accountDescription = usersComplete ? : ; return ( {reviewValidationPending ? ( ) : ( <> {firstIncompleteStepId && {_("Jump to first incomplete step")} )} isInline title={_("Configuration is incomplete")} variant="danger" > {_("Some installation steps still need to be completed before you can continue.")} } {!hiddenScreens.includes("anaconda-screen-date-time") && } {!softwarePageHidden && } {!accountsPageHidden && } {appliedPartitioning && } )} ); }; const useConfirmationCheckboxLabel = () => { const [scenarioConfirmationLabel, setScenarioConfirmationLabel] = useState(null); const originalDevices = useOriginalDevices(); const plannedActions = usePlannedActions(); const { diskSelection } = useContext(StorageContext); const usableDisks = diskSelection.usableDisks; const selectedDisks = diskSelection.selectedDisks; const relevantDevices = selectedDisks .map(disk => getDeviceChildren({ device: disk, deviceData: originalDevices })) .flat(Infinity); const deviceHasAction = (actionType, device) => plannedActions.find(action => action["device-id"].v === device && action["action-type"].v === actionType); const deletedDevices = relevantDevices.filter(device => deviceHasAction("destroy", device)); const resizedDevices = relevantDevices.filter(device => deviceHasAction("resize", device)); const allDevicesDeleted = deletedDevices.length > 0 && deletedDevices.length === relevantDevices.length; const someDevicesDeleted = deletedDevices.length > 0 && deletedDevices.length < relevantDevices.length; const someDevicesResized = resizedDevices.length > 0; useEffect(() => { const allDevicesDeletedText = cockpit.ngettext( "I understand that all existing data will be erased", "I understand that all existing data will be erased from the selected disks", usableDisks.length ); const someDevicesDeletedText = _("I understand that some existing data will be erased"); const someDevicesResizedText = _("I understand that some partitions will be modified"); if (allDevicesDeleted) { setScenarioConfirmationLabel(allDevicesDeletedText); } else if (someDevicesDeleted) { setScenarioConfirmationLabel(someDevicesDeletedText); } else if (someDevicesResized) { setScenarioConfirmationLabel(someDevicesResizedText); } else { setScenarioConfirmationLabel(""); } }, [allDevicesDeleted, someDevicesDeleted, someDevicesResized, usableDisks.length]); return scenarioConfirmationLabel; }; const CustomFooter = ({ goToProgressPage, pageValidationOk }) => { const { setIsFormValid } = useContext(PageContext) ?? {}; const { getButtonLabel } = useScenario(); const buttonLabel = getButtonLabel?.(); const scenarioConfirmationLabel = useConfirmationCheckboxLabel(); const installationIsClean = scenarioConfirmationLabel === ""; const [isConfirmed, setIsConfirmed] = useState(false); const confirmationCheckbox = ( !installationIsClean && setIsConfirmed(checked)} /> ); useEffect(() => { const isConfirmationValid = isConfirmed || installationIsClean; setIsFormValid(isConfirmationValid && pageValidationOk); }, [setIsFormValid, isConfirmed, installationIsClean, pageValidationOk]); return ( goToProgressPage()} /> ); };