From c6f8b873b646e93381669b3da1a61895833a084c Mon Sep 17 00:00:00 2001 From: Katerina Koukiou Date: Thu, 18 Jan 2024 12:53:44 +0100 Subject: [PATCH] Re-implement 'Modify storage' target to open cockpit storage in-page --- src/components/AnacondaWizard.jsx | 131 +++++- src/components/Common.jsx | 1 + src/components/app.jsx | 34 +- src/components/review/ReviewConfiguration.jsx | 9 +- .../storage/CockpitStorageIntegration.jsx | 392 ++++++++++++++++++ .../storage/CockpitStorageIntegration.scss | 48 +++ src/components/storage/DiskEncryption.jsx | 2 +- .../storage/HelpAutopartOptions.jsx | 2 + .../storage/InstallationDestination.jsx | 15 +- src/components/storage/InstallationMethod.jsx | 11 + .../storage/InstallationScenario.jsx | 125 ++---- src/components/storage/ModifyStorage.jsx | 139 +------ test/vm.install | 2 +- 13 files changed, 685 insertions(+), 226 deletions(-) create mode 100644 src/components/storage/CockpitStorageIntegration.jsx create mode 100644 src/components/storage/CockpitStorageIntegration.scss diff --git a/src/components/AnacondaWizard.jsx b/src/components/AnacondaWizard.jsx index d6abeec7ec..4e13f01a64 100644 --- a/src/components/AnacondaWizard.jsx +++ b/src/components/AnacondaWizard.jsx @@ -34,7 +34,8 @@ import { import { AnacondaPage } from "./AnacondaPage.jsx"; import { InstallationMethod, getPageProps as getInstallationMethodProps } from "./storage/InstallationMethod.jsx"; -import { getDefaultScenario } from "./storage/InstallationScenario.jsx"; +import { getDefaultScenario, scenarios, AvailabilityState } from "./storage/InstallationScenario.jsx"; +import { CockpitStorageIntegration } from "./storage/CockpitStorageIntegration.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"; @@ -44,7 +45,15 @@ import { ReviewConfiguration, ReviewConfigurationConfirmModal, getPageProps as g import { exitGui } from "../helpers/exit.js"; import { getMountPointConstraints, + getDevices, + getDiskFreeSpace, + getDiskTotalSpace, + getRequiredDeviceSize, } from "../apis/storage_devicetree.js"; +import { + getRequiredSpace +} from "../apis/payloads.js"; +import { findDuplicatesInArray } from "../helpers/utils.js"; import { applyStorage, resetPartitioning, @@ -54,10 +63,11 @@ import { SystemTypeContext, OsReleaseContext } from "./Common.jsx"; const _ = cockpit.gettext; const N_ = cockpit.noop; -export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtimeData, onCritFail, title, conf }) => { +export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtimeData, onCritFail, showStorage, setShowStorage, title, conf }) => { const [isFormDisabled, setIsFormDisabled] = useState(false); const [isFormValid, setIsFormValid] = useState(false); const [mountPointConstraints, setMountPointConstraints] = useState(); + const [requiredSize, setRequiredSize] = useState(); const [reusePartitioning, setReusePartitioning] = useState(false); const [stepNotification, setStepNotification] = useState(); const [storageEncryption, setStorageEncryption] = useState(getStorageEncryptionState()); @@ -67,6 +77,77 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim const [currentStepId, setCurrentStepId] = useState(); const osRelease = useContext(OsReleaseContext); const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO"; + const [scenarioAvailability, setScenarioAvailability] = useState(Object.fromEntries( + scenarios.map((s) => [s.id, new AvailabilityState()]) + )); + const [diskTotalSpace, setDiskTotalSpace] = useState(); + const [diskFreeSpace, setDiskFreeSpace] = useState(); + const [hasFilesystems, setHasFilesystems] = useState(); + const [duplicateDeviceNames, setDuplicateDeviceNames] = useState([]); + const selectedDisks = storageData.diskSelection.selectedDisks; + + useEffect(() => { + const updateSizes = async () => { + const diskTotalSpace = await getDiskTotalSpace({ diskNames: selectedDisks }).catch(console.error); + const diskFreeSpace = await getDiskFreeSpace({ diskNames: selectedDisks }).catch(console.error); + const devices = await getDevices().catch(console.error); + const _duplicateDeviceNames = findDuplicatesInArray(devices); + + setDuplicateDeviceNames(_duplicateDeviceNames); + setDiskTotalSpace(diskTotalSpace); + setDiskFreeSpace(diskFreeSpace); + }; + updateSizes(); + }, [selectedDisks]); + + useEffect(() => { + if ([diskTotalSpace, diskFreeSpace, hasFilesystems, mountPointConstraints, requiredSize].some(itm => itm === undefined)) { + return; + } + + setScenarioAvailability(oldAvailability => { + const newAvailability = {}; + + for (const scenario of scenarios) { + const availability = scenario.check({ + oldPartitioning: oldAvailability[scenario.id].partitioning, + diskFreeSpace, + diskTotalSpace, + duplicateDeviceNames, + partitioning: storageData.partitioning.path, + hasFilesystems, + requiredSize, + storageScenarioId, + }); + newAvailability[scenario.id] = availability; + } + return newAvailability; + }); + }, [ + diskFreeSpace, + diskTotalSpace, + duplicateDeviceNames, + hasFilesystems, + mountPointConstraints, + requiredSize, + storageData.partitioning.path, + storageData.devices, + storageScenarioId, + ]); + + useEffect(() => { + getDevices().then(res => { + const _duplicateDeviceNames = findDuplicatesInArray(res); + setDuplicateDeviceNames(_duplicateDeviceNames); + setIsFormValid(_duplicateDeviceNames.length === 0); + }, onCritFail({ context: N_("Failed to get device names.") })); + }, [storageData.devices, onCritFail, setIsFormValid]); + + useEffect(() => { + const hasFilesystems = selectedDisks.some(device => storageData.devices[device]?.children.v.some(child => storageData.devices[child]?.formatData.mountable.v || storageData.devices[child]?.formatData.type.v === "luks")); + + setHasFilesystems(hasFilesystems); + }, [selectedDisks, storageData.devices]); const availableDevices = useMemo(() => { return Object.keys(storageData.devices); @@ -80,6 +161,16 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim updateMountPointConstraints(); }, []); + useEffect(() => { + const updateRequiredSize = async () => { + const requiredSpace = await getRequiredSpace().catch(console.error); + const requiredSize = await getRequiredDeviceSize({ requiredSpace }).catch(console.error); + + setRequiredSize(requiredSize); + }; + updateRequiredSize(); + }, []); + useEffect(() => { if (!currentStepId) { return; @@ -94,7 +185,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim * but for custom mount assignment we try to reuse the partitioning when possible. */ setReusePartitioning(false); - }, [availableDevices, storageData.diskSelection.selectedDisks]); + }, [availableDevices, selectedDisks]); const language = useMemo(() => { for (const l of Object.keys(localizationData.languages)) { @@ -105,6 +196,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim } } }, [localizationData]); + const stepsOrder = [ { component: InstallationLanguage, @@ -116,12 +208,17 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim data: { deviceData: storageData.devices, diskSelection: storageData.diskSelection, + // HACK - is there a more official source for this? + isEfi: mountPointConstraints?.some(c => c["required-filesystem-type"]?.v === "efi"), dispatch, + requiredSize, + scenarioAvailability, storageScenarioId, setStorageScenarioId: (scenarioId) => { window.sessionStorage.setItem("storage-scenario-id", scenarioId); setStorageScenarioId(scenarioId); - } + }, + setShowStorage, }, ...getInstallationMethodProps({ isBootIso, osRelease, isFormValid }) }, @@ -273,14 +370,36 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim ); } - const firstVisibleStepIndex = steps.findIndex(step => !step.props.isHidden) + 1; + const startIndex = steps.findIndex(step => { + // Find the first step that is not hidden if the Wizard is opening for the first time. + // Otherwise, find the first step that was last visited. + return currentStepId ? step.props.id === currentStepId : !step.props.isHidden; + }) + 1; + + if (showStorage) { + return ( + + ); + } + + console.info({ storageScenarioId, part: storageData.partitioning }); return ( { return ( diff --git a/src/components/app.jsx b/src/components/app.jsx index 564d4d46e7..c8973e6ed3 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -25,7 +25,7 @@ import { import { read_os_release as readOsRelease } from "os-release.js"; import { WithDialogs } from "dialogs.jsx"; -import { AddressContext, LanguageContext, SystemTypeContext, OsReleaseContext } from "./Common.jsx"; +import { AddressContext, LanguageContext, SystemTypeContext, TargetSystemRootContext, OsReleaseContext } from "./Common.jsx"; import { AnacondaHeader } from "./AnacondaHeader.jsx"; import { AnacondaWizard } from "./AnacondaWizard.jsx"; import { CriticalError, errorHandlerWithContext, bugzillaPrefiledReportURL } from "./Error.jsx"; @@ -56,6 +56,7 @@ export const Application = () => { const [storeInitilized, setStoreInitialized] = useState(false); const criticalError = state?.error?.criticalError; const [jsError, setJsEroor] = useState(); + const [showStorage, setShowStorage] = useState(false); const onCritFail = useCallback((contextData) => { return errorHandlerWithContext(contextData, exc => dispatch(setCriticalErrorAction(exc))); @@ -136,6 +137,7 @@ export const Application = () => { reportLinkURL={bzReportURL} />} {!jsError && <> + {!showStorage && { isConnected={state.network.connected} onCritFail={onCritFail} /> - + } - - - + + + + + } diff --git a/src/components/review/ReviewConfiguration.jsx b/src/components/review/ReviewConfiguration.jsx index 78cbb7d95a..493121dc9b 100644 --- a/src/components/review/ReviewConfiguration.jsx +++ b/src/components/review/ReviewConfiguration.jsx @@ -177,7 +177,14 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, language, local {diskSelection.selectedDisks.map(disk => { - return ; + return ( + + ); })} diff --git a/src/components/storage/CockpitStorageIntegration.jsx b/src/components/storage/CockpitStorageIntegration.jsx new file mode 100644 index 0000000000..7e91e759a0 --- /dev/null +++ b/src/components/storage/CockpitStorageIntegration.jsx @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + HelperTextItem, + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with This program; If not, see . + */ +import cockpit from "cockpit"; +import React, { useEffect, useState } from "react"; + +import { + ActionList, + Alert, + Button, + Card, + CardBody, + Flex, + FlexItem, + HelperText, + HelperTextItem, + List, + ListItem, + Modal, + PageSection, + PageSectionVariants, + Text, + TextContent, + Title, +} from "@patternfly/react-core"; +import { ArrowLeftIcon } from "@patternfly/react-icons"; + +import { EmptyStatePanel } from "cockpit-components-empty-state"; + +import { + runStorageTask, + scanDevicesWithTask, +} from "../../apis/storage.js"; +import { + setBootloaderDrive, +} from "../../apis/storage_bootloader.js"; +import { + setInitializationMode, +} from "../../apis/storage_disk_initialization.js"; +import { + applyStorage, + createPartitioning, + gatherRequests, + setManualPartitioningRequests +} from "../../apis/storage_partitioning.js"; + +import { getDevicesAction } from "../../actions/storage-actions.js"; + +import "./CockpitStorageIntegration.scss"; + +const _ = cockpit.gettext; +const idPrefix = "cockpit-storage-integration"; + +const ReturnToInstallationButton = ({ onAction }) => ( + +); + +export const CockpitStorageIntegration = ({ + deviceData, + selectedDisks, + dispatch, + mountPointConstraints, + onCritFail, + requiredSize, + scenarioAvailability, + setShowStorage, + setScenarioAvailability, + setStorageScenarioId, +}) => { + const [showDialog, setShowDialog] = useState(false); + + return ( + <> + + + {_("Configure storage")} + + + +
+