From 0b2139396edb7b65a8d92f59a338b1f7f1e918a8 Mon Sep 17 00:00:00 2001 From: Katerina Koukiou Date: Mon, 23 Dec 2024 14:16:15 +0100 Subject: [PATCH] storage: split logic for different scenario into seperate files --- .../storage/CockpitStorageIntegration.jsx | 3 +- src/components/storage/InstallationMethod.jsx | 3 +- .../storage/InstallationScenario.jsx | 310 +----------------- src/components/storage/scenarios/EraseAll.jsx | 54 +++ .../storage/scenarios/MountPointMapping.jsx | 70 ++++ .../storage/scenarios/ReinstallFedora.jsx | 128 ++++++++ .../scenarios/UseConfiguredStorage.jsx | 97 ++++++ .../storage/scenarios/UseFreeSpace.jsx | 82 +++++ src/components/storage/scenarios/helpers.js | 24 ++ src/components/storage/scenarios/index.js | 13 + src/hooks/Storage.jsx | 4 +- 11 files changed, 476 insertions(+), 312 deletions(-) create mode 100644 src/components/storage/scenarios/EraseAll.jsx create mode 100644 src/components/storage/scenarios/MountPointMapping.jsx create mode 100644 src/components/storage/scenarios/ReinstallFedora.jsx create mode 100644 src/components/storage/scenarios/UseConfiguredStorage.jsx create mode 100644 src/components/storage/scenarios/UseFreeSpace.jsx create mode 100644 src/components/storage/scenarios/helpers.js create mode 100644 src/components/storage/scenarios/index.js diff --git a/src/components/storage/CockpitStorageIntegration.jsx b/src/components/storage/CockpitStorageIntegration.jsx index abbdeaa05c..9511c31cec 100644 --- a/src/components/storage/CockpitStorageIntegration.jsx +++ b/src/components/storage/CockpitStorageIntegration.jsx @@ -79,7 +79,8 @@ import { import { EmptyStatePanel } from "cockpit-components-empty-state"; -import { checkConfiguredStorage, checkUseFreeSpace } from "./InstallationScenario.jsx"; +import { checkConfiguredStorage } from "./scenarios/UseConfiguredStorage.jsx"; +import { checkUseFreeSpace } from "./scenarios/UseFreeSpace.jsx"; import "./CockpitStorageIntegration.scss"; diff --git a/src/components/storage/InstallationMethod.jsx b/src/components/storage/InstallationMethod.jsx index 7046a419c2..900865e739 100644 --- a/src/components/storage/InstallationMethod.jsx +++ b/src/components/storage/InstallationMethod.jsx @@ -42,8 +42,9 @@ import { getNewPartitioning } from "../../hooks/Storage.jsx"; import { AnacondaWizardFooter } from "../AnacondaWizardFooter.jsx"; import { InstallationDestination } from "./InstallationDestination.jsx"; -import { InstallationScenario, scenarios } from "./InstallationScenario.jsx"; +import { InstallationScenario } from "./InstallationScenario.jsx"; import { ReclaimSpaceModal } from "./ReclaimSpaceModal.jsx"; +import { scenarios } from "./scenarios/index.js"; const _ = cockpit.gettext; diff --git a/src/components/storage/InstallationScenario.jsx b/src/components/storage/InstallationScenario.jsx index 8c16e971ba..14246215ea 100644 --- a/src/components/storage/InstallationScenario.jsx +++ b/src/components/storage/InstallationScenario.jsx @@ -19,26 +19,21 @@ import cockpit from "cockpit"; import React, { useContext, useEffect, useMemo, useState } from "react"; import { - Checkbox, FormGroup, FormSection, Radio, Title, } from "@patternfly/react-core"; -import { getAutopartReuseDBusRequest } from "../../apis/storage_partitioning.js"; - import { setStorageScenarioAction } from "../../actions/storage-actions.js"; import { debug } from "../../helpers/log.js"; import { - bootloaderTypes, - getDeviceAncestors, getLockedLUKSDevices, } from "../../helpers/storage.js"; +import { AvailabilityState } from "./scenarios/helpers.js"; import { - DialogsContext, StorageContext, StorageDefaultsContext, SystemTypeContext @@ -55,314 +50,13 @@ import { useUsablePartitions, } from "../../hooks/Storage.jsx"; -import { StorageReview } from "../review/StorageReview.jsx"; import { EncryptedDevices } from "./EncryptedDevices.jsx"; -import { helpConfiguredStorage, helpEraseAll, helpHomeReuse, helpMountPointMapping, helpUseFreeSpace } from "./HelpAutopartOptions.jsx"; +import { scenarios } from "./scenarios/index.js"; import "./InstallationScenario.scss"; const _ = cockpit.gettext; -function AvailabilityState (available = false, hidden = true, reason = null, hint = null, enforceAction = false) { - this.available = available; - this.enforceAction = enforceAction; - this.hidden = hidden; - this.reason = reason; - this.hint = hint; -} - -const checkEraseAll = ({ diskTotalSpace, requiredSize, selectedDisks }) => { - const availability = new AvailabilityState(); - - availability.available = !!selectedDisks.length; - availability.hidden = false; - - if (diskTotalSpace < requiredSize) { - availability.available = false; - availability.reason = _("Not enough space on selected disks."); - availability.hint = cockpit.format(_( - "The installation needs $1 of disk space; " + - "however, the capacity of the selected disks is only $0." - ), cockpit.format_bytes(diskTotalSpace), cockpit.format_bytes(requiredSize)); - } - - return availability; -}; - -export const checkUseFreeSpace = ({ diskFreeSpace, diskTotalSpace, requiredSize, selectedDisks }) => { - const availability = new AvailabilityState(); - - availability.hidden = false; - availability.available = !!selectedDisks.length; - - if (diskFreeSpace > 0 && diskTotalSpace > 0) { - availability.hidden = diskFreeSpace === diskTotalSpace; - } - if (diskFreeSpace < requiredSize) { - availability.enforceAction = true; - availability.reason = _("Not enough free space on the selected disks."); - availability.hint = cockpit.format( - _("To use this option, resize or remove existing partitions to free up at least $0."), - cockpit.format_bytes(requiredSize) - ); - } - return availability; -}; - -const getMissingNonmountablePartitions = (usablePartitions, mountPointConstraints) => { - const existingNonmountablePartitions = usablePartitions - .filter(device => !device.formatData.mountable.v) - .map(device => device.formatData.type.v); - - const missingNonmountablePartitions = mountPointConstraints.filter(constraint => - constraint.required.v && - !constraint["mount-point"].v && - !existingNonmountablePartitions.includes(constraint["required-filesystem-type"].v)) - .map(constraint => constraint.description); - - return missingNonmountablePartitions; -}; - -const checkMountPointMapping = ({ mountPointConstraints, selectedDisks, usablePartitions }) => { - const availability = new AvailabilityState(); - - availability.hidden = false; - availability.available = !!selectedDisks.length; - - const missingNMParts = getMissingNonmountablePartitions(usablePartitions, mountPointConstraints); - const hasFilesystems = usablePartitions - .filter(device => device.formatData.mountable.v || device.formatData.type.v === "luks").length > 0; - - if (!hasFilesystems) { - // No usable devices on the selected disks: hide the scenario to reduce UI clutter - availability.hidden = true; - } else if (missingNMParts.length) { - availability.available = false; - availability.reason = cockpit.format(_("Some required partitions are missing: $0"), missingNMParts.join(", ")); - } - return availability; -}; - -const checkHomeReuse = ({ autopartScheme, devices, originalExistingSystems, selectedDisks }) => { - const availability = new AvailabilityState(); - let reusedOS = null; - - availability.hidden = false; - availability.available = !!selectedDisks.length; - - const isCompleteOSOnDisks = (osData, disks) => { - const osDisks = osData.devices.v.map(deviceId => getDeviceAncestors(devices, deviceId)) - .reduce((disks, ancestors) => disks.concat(ancestors)) - .filter(dev => devices[dev].type.v === "disk") - .reduce((uniqueDisks, disk) => uniqueDisks.includes(disk) ? uniqueDisks : [...uniqueDisks, disk], []); - const missingDisks = osDisks.filter(disk => !disks.includes(disk)); - return missingDisks.length === 0; - }; - - const getUnknownMountPoints = (scheme, existingOS) => { - const reuseRequest = getAutopartReuseDBusRequest(scheme); - const isBootloader = (device) => bootloaderTypes.includes(devices[device].formatData.type.v); - const existingMountPoints = Object.entries(existingOS["mount-points"].v) - .map(([mountPoint, device]) => isBootloader(device) ? "bootloader" : mountPoint); - - const managedMountPoints = reuseRequest["reformatted-mount-points"].v - .concat(reuseRequest["reused-mount-points"].v, reuseRequest["removed-mount-points"].v); - - const unknownMountPoints = existingMountPoints.filter(i => !managedMountPoints.includes(i)); - return unknownMountPoints; - }; - - // Check that exactly one Linux OS is present and it is Fedora Linux - // (Stronger check for mountpoints uniqueness is in the backend - const linuxSystems = originalExistingSystems.filter(osdata => osdata["os-name"].v.includes("Linux")) - .filter(osdata => isCompleteOSOnDisks(osdata, selectedDisks)); - if (linuxSystems.length === 0) { - availability.available = false; - availability.hidden = true; - debug("home reuse: No existing Linux system found."); - } else if (linuxSystems.length > 1) { - availability.available = false; - availability.hidden = true; - debug("home reuse: Multiple existing Linux systems found."); - } else { - reusedOS = linuxSystems[0]; - if (!linuxSystems.some(osdata => osdata["os-name"].v.includes("Fedora"))) { - availability.available = false; - availability.hidden = true; - debug("home reuse: No existing Fedora Linux system found."); - } - } - - debug(`home reuse: Default scheme is ${autopartScheme}.`); - if (reusedOS) { - // Check that required autopartitioning scheme matches reused OS. - // Check just "/home". To be more generic we could check all reused devices (as the backend). - const homeDevice = reusedOS["mount-points"].v["/home"]; - const homeDeviceType = devices[homeDevice]?.type.v; - const requiredSchemeTypes = { - BTRFS: "btrfs subvolume", - LVM: "lvmlv", - LVM_THINP: "lvmthinlv", - PLAIN: "partition", - }; - if (homeDeviceType !== requiredSchemeTypes[autopartScheme]) { - availability.available = false; - availability.hidden = true; - debug(`home reuse: No reusable existing Linux system found, reused devices must have ${requiredSchemeTypes[autopartScheme]} type`); - } - } - - if (reusedOS) { - // Check that existing system does not have mountpoints unexpected - // by the required autopartitioning scheme - const unknownMountPoints = getUnknownMountPoints(autopartScheme, reusedOS); - if (unknownMountPoints.length > 0) { - availability.available = false; - availability.hidden = true; - console.info(`home reuse: Unknown existing mountpoints found ${unknownMountPoints}`); - } - } - - // TODO checks: - // - luks - partitions are unlocked - enforce? allow opt-out? - // - size ? - // - Windows system along (forbidden for now?) - - return availability; -}; - -export const checkConfiguredStorage = ({ - devices, - mountPointConstraints, - newMountPoints, - partitioning, - storageScenarioId, -}) => { - const availability = new AvailabilityState(); - - const currentPartitioningMatches = storageScenarioId === "use-configured-storage"; - availability.hidden = partitioning === undefined || !currentPartitioningMatches; - - availability.available = ( - newMountPoints === undefined || - ( - mountPointConstraints - ?.filter(m => m.required.v) - .every(m => { - const allDirs = []; - const getNestedDirs = (object) => { - if (!object) { - return; - } - const { content, dir, subvolumes } = object; - - if (dir) { - allDirs.push(dir); - } - if (content) { - getNestedDirs(content); - } - if (subvolumes) { - Object.keys(subvolumes).forEach(sv => getNestedDirs(subvolumes[sv])); - } - }; - - if (m["mount-point"].v) { - Object.keys(newMountPoints).forEach(key => getNestedDirs(newMountPoints[key])); - - return allDirs.includes(m["mount-point"].v); - } - - if (m["required-filesystem-type"].v === "biosboot") { - const biosboot = Object.keys(devices).find(d => devices[d].formatData.type.v === "biosboot"); - - return biosboot !== undefined; - } - - return false; - }) - ) - ); - - availability.review = ; - - return availability; -}; - -const ReclaimSpace = ({ availability }) => { - const { isReclaimSpaceCheckboxChecked, setIsReclaimSpaceCheckboxChecked } = useContext(DialogsContext); - - useEffect(() => { - setIsReclaimSpaceCheckboxChecked(availability.enforceAction); - }, [availability.enforceAction, setIsReclaimSpaceCheckboxChecked]); - - return ( - setIsReclaimSpaceCheckboxChecked(value)} - /> - ); -}; - -export const scenarios = [{ - buttonLabel: _("Reinstall Fedora"), - buttonVariant: "danger", - check: checkHomeReuse, - default: false, - detail: helpHomeReuse, - id: "home-reuse", - // CLEAR_PARTITIONS_NONE = 0 - initializationMode: 0, - label: _("Reinstall Fedora"), -}, { - buttonLabel: _("Erase data and install"), - buttonVariant: "danger", - check: checkEraseAll, - default: true, - detail: helpEraseAll, - id: "erase-all", - // CLEAR_PARTITIONS_ALL = 1 - initializationMode: 1, - label: _("Use entire disk"), -}, { - action: ReclaimSpace, - buttonLabel: _("Install"), - buttonVariant: "primary", - canReclaimSpace: true, - check: checkUseFreeSpace, - default: false, - detail: helpUseFreeSpace, - id: "use-free-space", - // CLEAR_PARTITIONS_NONE = 0 - initializationMode: 0, - label: _("Share disk with other operating system"), -}, { - buttonLabel: _("Apply mount point assignment and install"), - buttonVariant: "danger", - check: checkMountPointMapping, - default: false, - detail: helpMountPointMapping, - id: "mount-point-mapping", - // CLEAR_PARTITIONS_NONE = 0 - initializationMode: 0, - label: _("Mount point assignment"), -}, { - buttonLabel: _("Install"), - buttonVariant: "danger", - check: checkConfiguredStorage, - default: false, - detail: helpConfiguredStorage, - id: "use-configured-storage", - // CLEAR_PARTITIONS_NONE = 0 - initializationMode: 0, - label: _("Use configured storage"), -} -]; - export const useScenario = () => { const { storageScenarioId } = useContext(StorageContext); const [scenario, setScenario] = useState({}); diff --git a/src/components/storage/scenarios/EraseAll.jsx b/src/components/storage/scenarios/EraseAll.jsx new file mode 100644 index 0000000000..ed006e5326 --- /dev/null +++ b/src/components/storage/scenarios/EraseAll.jsx @@ -0,0 +1,54 @@ +/* + * 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 + * 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 { AvailabilityState } from "./helpers.js"; + +import { helpEraseAll } from "../HelpAutopartOptions.jsx"; + +const _ = cockpit.gettext; + +const checkEraseAll = ({ diskTotalSpace, requiredSize, selectedDisks }) => { + const availability = new AvailabilityState(); + + availability.available = !!selectedDisks.length; + availability.hidden = false; + + if (diskTotalSpace < requiredSize) { + availability.available = false; + availability.reason = _("Not enough space on selected disks."); + availability.hint = cockpit.format(_( + "The installation needs $1 of disk space; " + + "however, the capacity of the selected disks is only $0." + ), cockpit.format_bytes(diskTotalSpace), cockpit.format_bytes(requiredSize)); + } + + return availability; +}; + +export const scenarioEraseAll = { + buttonLabel: _("Erase data and install"), + buttonVariant: "danger", + check: checkEraseAll, + default: true, + detail: helpEraseAll, + id: "erase-all", + // CLEAR_PARTITIONS_ALL = 1 + initializationMode: 1, + label: _("Use entire disk"), +}; diff --git a/src/components/storage/scenarios/MountPointMapping.jsx b/src/components/storage/scenarios/MountPointMapping.jsx new file mode 100644 index 0000000000..c128550376 --- /dev/null +++ b/src/components/storage/scenarios/MountPointMapping.jsx @@ -0,0 +1,70 @@ +/* + * 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 + * 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 { AvailabilityState } from "./helpers.js"; + +import { helpMountPointMapping } from "../HelpAutopartOptions.jsx"; + +const _ = cockpit.gettext; + +const checkMountPointMapping = ({ mountPointConstraints, selectedDisks, usablePartitions }) => { + const availability = new AvailabilityState(); + + availability.hidden = false; + availability.available = !!selectedDisks.length; + + const missingNMParts = getMissingNonmountablePartitions(usablePartitions, mountPointConstraints); + const hasFilesystems = usablePartitions + .filter(device => device.formatData.mountable.v || device.formatData.type.v === "luks").length > 0; + + if (!hasFilesystems) { + // No usable devices on the selected disks: hide the scenario to reduce UI clutter + availability.hidden = true; + } else if (missingNMParts.length) { + availability.available = false; + availability.reason = cockpit.format(_("Some required partitions are missing: $0"), missingNMParts.join(", ")); + } + return availability; +}; + +const getMissingNonmountablePartitions = (usablePartitions, mountPointConstraints) => { + const existingNonmountablePartitions = usablePartitions + .filter(device => !device.formatData.mountable.v) + .map(device => device.formatData.type.v); + + const missingNonmountablePartitions = mountPointConstraints.filter(constraint => + constraint.required.v && + !constraint["mount-point"].v && + !existingNonmountablePartitions.includes(constraint["required-filesystem-type"].v)) + .map(constraint => constraint.description); + + return missingNonmountablePartitions; +}; + +export const scenarioMountPointMapping = { + buttonLabel: _("Apply mount point assignment and install"), + buttonVariant: "danger", + check: checkMountPointMapping, + default: false, + detail: helpMountPointMapping, + id: "mount-point-mapping", + // CLEAR_PARTITIONS_NONE = 0 + initializationMode: 0, + label: _("Mount point assignment"), +}; diff --git a/src/components/storage/scenarios/ReinstallFedora.jsx b/src/components/storage/scenarios/ReinstallFedora.jsx new file mode 100644 index 0000000000..ad05cf88b4 --- /dev/null +++ b/src/components/storage/scenarios/ReinstallFedora.jsx @@ -0,0 +1,128 @@ +/* + * 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 + * 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 { getAutopartReuseDBusRequest } from "../../../apis/storage_partitioning.js"; + +import { debug } from "../../../helpers/log.js"; +import { bootloaderTypes, getDeviceAncestors } from "../../../helpers/storage.js"; +import { AvailabilityState } from "./helpers.js"; + +import { helpHomeReuse } from "../HelpAutopartOptions.jsx"; + +const _ = cockpit.gettext; + +const checkHomeReuse = ({ autopartScheme, devices, originalExistingSystems, selectedDisks }) => { + const availability = new AvailabilityState(); + let reusedOS = null; + + availability.hidden = false; + availability.available = !!selectedDisks.length; + + const isCompleteOSOnDisks = (osData, disks) => { + const osDisks = osData.devices.v.map(deviceId => getDeviceAncestors(devices, deviceId)) + .reduce((disks, ancestors) => disks.concat(ancestors)) + .filter(dev => devices[dev].type.v === "disk") + .reduce((uniqueDisks, disk) => uniqueDisks.includes(disk) ? uniqueDisks : [...uniqueDisks, disk], []); + const missingDisks = osDisks.filter(disk => !disks.includes(disk)); + return missingDisks.length === 0; + }; + + const getUnknownMountPoints = (scheme, existingOS) => { + const reuseRequest = getAutopartReuseDBusRequest(scheme); + const isBootloader = (device) => bootloaderTypes.includes(devices[device].formatData.type.v); + const existingMountPoints = Object.entries(existingOS["mount-points"].v) + .map(([mountPoint, device]) => isBootloader(device) ? "bootloader" : mountPoint); + + const managedMountPoints = reuseRequest["reformatted-mount-points"].v + .concat(reuseRequest["reused-mount-points"].v, reuseRequest["removed-mount-points"].v); + + const unknownMountPoints = existingMountPoints.filter(i => !managedMountPoints.includes(i)); + return unknownMountPoints; + }; + + // Check that exactly one Linux OS is present and it is Fedora Linux + // (Stronger check for mountpoints uniqueness is in the backend + const linuxSystems = originalExistingSystems.filter(osdata => osdata["os-name"].v.includes("Linux")) + .filter(osdata => isCompleteOSOnDisks(osdata, selectedDisks)); + if (linuxSystems.length === 0) { + availability.available = false; + availability.hidden = true; + debug("home reuse: No existing Linux system found."); + } else if (linuxSystems.length > 1) { + availability.available = false; + availability.hidden = true; + debug("home reuse: Multiple existing Linux systems found."); + } else { + reusedOS = linuxSystems[0]; + if (!linuxSystems.some(osdata => osdata["os-name"].v.includes("Fedora"))) { + availability.available = false; + availability.hidden = true; + debug("home reuse: No existing Fedora Linux system found."); + } + } + + debug(`home reuse: Default scheme is ${autopartScheme}.`); + if (reusedOS) { + // Check that required autopartitioning scheme matches reused OS. + // Check just "/home". To be more generic we could check all reused devices (as the backend). + const homeDevice = reusedOS["mount-points"].v["/home"]; + const homeDeviceType = devices[homeDevice]?.type.v; + const requiredSchemeTypes = { + BTRFS: "btrfs subvolume", + LVM: "lvmlv", + LVM_THINP: "lvmthinlv", + PLAIN: "partition", + }; + if (homeDeviceType !== requiredSchemeTypes[autopartScheme]) { + availability.available = false; + availability.hidden = true; + debug(`home reuse: No reusable existing Linux system found, reused devices must have ${requiredSchemeTypes[autopartScheme]} type`); + } + } + + if (reusedOS) { + // Check that existing system does not have mountpoints unexpected + // by the required autopartitioning scheme + const unknownMountPoints = getUnknownMountPoints(autopartScheme, reusedOS); + if (unknownMountPoints.length > 0) { + availability.available = false; + availability.hidden = true; + console.info(`home reuse: Unknown existing mountpoints found ${unknownMountPoints}`); + } + } + + // TODO checks: + // - luks - partitions are unlocked - enforce? allow opt-out? + // - size ? + // - Windows system along (forbidden for now?) + + return availability; +}; + +export const scenarioReinstallFedora = { + buttonLabel: _("Reinstall Fedora"), + buttonVariant: "danger", + check: checkHomeReuse, + default: false, + detail: helpHomeReuse, + id: "home-reuse", + // CLEAR_PARTITIONS_NONE = 0 + initializationMode: 0, + label: _("Reinstall Fedora"), +}; diff --git a/src/components/storage/scenarios/UseConfiguredStorage.jsx b/src/components/storage/scenarios/UseConfiguredStorage.jsx new file mode 100644 index 0000000000..1c7169a270 --- /dev/null +++ b/src/components/storage/scenarios/UseConfiguredStorage.jsx @@ -0,0 +1,97 @@ +/* + * 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 + * 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 from "react"; + +import { AvailabilityState } from "./helpers.js"; + +import { StorageReview } from "../../review/StorageReview.jsx"; +import { helpConfiguredStorage } from "../HelpAutopartOptions.jsx"; + +const _ = cockpit.gettext; + +export const checkConfiguredStorage = ({ + devices, + mountPointConstraints, + newMountPoints, + partitioning, + storageScenarioId, +}) => { + const availability = new AvailabilityState(); + + const currentPartitioningMatches = storageScenarioId === "use-configured-storage"; + availability.hidden = partitioning === undefined || !currentPartitioningMatches; + + availability.available = ( + newMountPoints === undefined || + ( + mountPointConstraints + ?.filter(m => m.required.v) + .every(m => { + const allDirs = []; + const getNestedDirs = (object) => { + if (!object) { + return; + } + const { content, dir, subvolumes } = object; + + if (dir) { + allDirs.push(dir); + } + if (content) { + getNestedDirs(content); + } + if (subvolumes) { + Object.keys(subvolumes).forEach(sv => getNestedDirs(subvolumes[sv])); + } + }; + + if (m["mount-point"].v) { + Object.keys(newMountPoints).forEach(key => getNestedDirs(newMountPoints[key])); + + return allDirs.includes(m["mount-point"].v); + } + + if (m["required-filesystem-type"].v === "biosboot") { + const biosboot = Object.keys(devices).find(d => devices[d].formatData.type.v === "biosboot"); + + return biosboot !== undefined; + } + + return false; + }) + ) + ); + + availability.review = ; + + return availability; +}; + +export const scenarioConfiguredStorage = { + buttonLabel: _("Install"), + buttonVariant: "danger", + check: checkConfiguredStorage, + default: false, + detail: helpConfiguredStorage, + id: "use-configured-storage", + // CLEAR_PARTITIONS_NONE = 0 + initializationMode: 0, + label: _("Use configured storage"), +}; diff --git a/src/components/storage/scenarios/UseFreeSpace.jsx b/src/components/storage/scenarios/UseFreeSpace.jsx new file mode 100644 index 0000000000..1f21c90706 --- /dev/null +++ b/src/components/storage/scenarios/UseFreeSpace.jsx @@ -0,0 +1,82 @@ +/* + * 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 + * 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, { useContext, useEffect } from "react"; +import { Checkbox } from "@patternfly/react-core"; + +import { AvailabilityState } from "./helpers.js"; + +import { DialogsContext } from "../../../contexts/Common.jsx"; + +import { helpUseFreeSpace } from "../HelpAutopartOptions.jsx"; + +const _ = cockpit.gettext; + +export const checkUseFreeSpace = ({ diskFreeSpace, diskTotalSpace, requiredSize, selectedDisks }) => { + const availability = new AvailabilityState(); + + availability.hidden = false; + availability.available = !!selectedDisks.length; + + if (diskFreeSpace > 0 && diskTotalSpace > 0) { + availability.hidden = diskFreeSpace === diskTotalSpace; + } + if (diskFreeSpace < requiredSize) { + availability.enforceAction = true; + availability.reason = _("Not enough free space on the selected disks."); + availability.hint = cockpit.format( + _("To use this option, resize or remove existing partitions to free up at least $0."), + cockpit.format_bytes(requiredSize) + ); + } + return availability; +}; + +const ReclaimSpace = ({ availability }) => { + const { isReclaimSpaceCheckboxChecked, setIsReclaimSpaceCheckboxChecked } = useContext(DialogsContext); + + useEffect(() => { + setIsReclaimSpaceCheckboxChecked(availability.enforceAction); + }, [availability.enforceAction, setIsReclaimSpaceCheckboxChecked]); + + return ( + setIsReclaimSpaceCheckboxChecked(value)} + /> + ); +}; + +export const scenarioUseFreeSpace = { + action: ReclaimSpace, + buttonLabel: _("Install"), + buttonVariant: "primary", + canReclaimSpace: true, + check: checkUseFreeSpace, + default: false, + detail: helpUseFreeSpace, + id: "use-free-space", + // CLEAR_PARTITIONS_NONE = 0 + initializationMode: 0, + label: _("Share disk with other operating system"), +}; diff --git a/src/components/storage/scenarios/helpers.js b/src/components/storage/scenarios/helpers.js new file mode 100644 index 0000000000..fab4381904 --- /dev/null +++ b/src/components/storage/scenarios/helpers.js @@ -0,0 +1,24 @@ +/* +* 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 +* 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 . +*/ + +export function AvailabilityState (available = false, hidden = true, reason = null, hint = null, enforceAction = false) { + this.available = available; + this.enforceAction = enforceAction; + this.hidden = hidden; + this.reason = reason; + this.hint = hint; +} diff --git a/src/components/storage/scenarios/index.js b/src/components/storage/scenarios/index.js new file mode 100644 index 0000000000..442ef506be --- /dev/null +++ b/src/components/storage/scenarios/index.js @@ -0,0 +1,13 @@ +import { scenarioEraseAll } from "./EraseAll.jsx"; +import { scenarioMountPointMapping } from "./MountPointMapping.jsx"; +import { scenarioReinstallFedora } from "./ReinstallFedora.jsx"; +import { scenarioConfiguredStorage } from "./UseConfiguredStorage.jsx"; +import { scenarioUseFreeSpace } from "./UseFreeSpace.jsx"; + +export const scenarios = [ + scenarioReinstallFedora, + scenarioEraseAll, + scenarioUseFreeSpace, + scenarioMountPointMapping, + scenarioConfiguredStorage, +]; diff --git a/src/hooks/Storage.jsx b/src/hooks/Storage.jsx index 873fa9e2cf..6097773cf3 100644 --- a/src/hooks/Storage.jsx +++ b/src/hooks/Storage.jsx @@ -43,10 +43,10 @@ import { import { getDeviceAncestors } from "../helpers/storage.js"; -import { scenarios } from "../components/storage/InstallationScenario.jsx"; - import { StorageContext } from "../contexts/Common.jsx"; +import { scenarios } from "../components/storage/scenarios/index.js"; + export const useDiskTotalSpace = ({ devices, selectedDisks }) => { const [diskTotalSpace, setDiskTotalSpace] = useState();