diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index b04e0187ef..e0e7095705 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -31,7 +31,7 @@ import { Product } from "~/types/software"; import { PATHS } from "~/router"; import { PRODUCT } from "~/routes/paths"; import type { Config } from "~/api"; -import type { Progress, State } from "~/model/status"; +import type { Progress, Stage } from "~/model/status"; import App from "./App"; import { System } from "~/model/system/network"; @@ -51,7 +51,7 @@ const network: System = { accessPoints: [], }; const mockProgresses: jest.Mock = jest.fn(); -const mockState: jest.Mock = jest.fn(); +const mockState: jest.Mock = jest.fn(); const mockSelectedProduct: jest.Mock = jest.fn(); jest.mock("~/hooks/api", () => ({ @@ -62,7 +62,7 @@ jest.mock("~/hooks/api", () => ({ }), useStatus: (): ReturnType => ({ - state: mockState(), + stage: mockState(), progresses: mockProgresses(), }), diff --git a/web/src/components/overview/StorageSection.tsx b/web/src/components/overview/StorageSection.tsx index 4558855d33..207a127b58 100644 --- a/web/src/components/overview/StorageSection.tsx +++ b/web/src/components/overview/StorageSection.tsx @@ -24,7 +24,7 @@ import React from "react"; import { Content } from "@patternfly/react-core"; import { deviceLabel } from "~/components/storage/utils"; import { useAvailableDevices, useDevices, useIssues } from "~/hooks/model/system/storage"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import { _ } from "~/i18n"; import type { Storage } from "~/model/system"; import type { ConfigModel } from "~/model/storage/config-model"; diff --git a/web/src/components/storage/BootSection.tsx b/web/src/components/storage/BootSection.tsx index 8bb12d7879..f7786e7271 100644 --- a/web/src/components/storage/BootSection.tsx +++ b/web/src/components/storage/BootSection.tsx @@ -26,7 +26,7 @@ import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import Link from "~/components/core/Link"; import Icon from "~/components/layout/Icon"; import { useAvailableDrives } from "~/hooks/model/system/storage"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import configModel from "~/model/storage/config-model"; import { STORAGE } from "~/routes/paths"; import { deviceLabel, formattedPath } from "~/components/storage/utils"; @@ -76,8 +76,8 @@ export default function BootSection() { const config = useConfigModel(); const devices = useAvailableDrives(); - const isDefaultBoot = configModel.hasDefaultBoot(config); - const bootDevice = configModel.bootDevice(config); + const isDefaultBoot = configModel.boot.isDefault(config); + const bootDevice = configModel.boot.findDevice(config); const device = devices.find((d) => d.name === bootDevice?.name); return ( diff --git a/web/src/components/storage/BootSelection.tsx b/web/src/components/storage/BootSelection.tsx index 2398c26bab..9339a1243f 100644 --- a/web/src/components/storage/BootSelection.tsx +++ b/web/src/components/storage/BootSelection.tsx @@ -27,14 +27,13 @@ import { DevicesFormSelect } from "~/components/storage"; import { Page, SubtleContent } from "~/components/core"; import { deviceLabel, formattedPath } from "~/components/storage/utils"; import { useCandidateDevices, useDevices } from "~/hooks/model/system/storage"; -import { useModel } from "~/hooks/storage/model"; -import { useConfigModel } from "~/hooks/model/storage"; -import { isDrive } from "~/storage/device"; import { + useConfigModel, useSetBootDevice, useSetDefaultBootDevice, - useDisableBootConfig, -} from "~/hooks/storage/boot"; + useDisableBoot, +} from "~/hooks/model/storage/config-model"; +import { isDrive } from "~/model/storage/device"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; @@ -77,21 +76,20 @@ export default function BootSelection() { const [state, setState] = useState({ load: false }); const navigate = useNavigate(); const devices = useDevices(); - const model = useModel(); const config = useConfigModel(); const allCandidateDevices = useCandidateDevices(); const setBootDevice = useSetBootDevice(); const setDefaultBootDevice = useSetDefaultBootDevice(); - const disableBootConfig = useDisableBootConfig(); + const disableBootConfig = useDisableBoot(); - const candidateDevices = filteredCandidates(allCandidateDevices, model); + const candidateDevices = filteredCandidates(allCandidateDevices, config); useEffect(() => { - if (state.load || !model) return; + if (state.load || !config) return; const bootModel = config.boot; - const isDefaultBoot = configModel.hasDefaultBoot(config); - const bootDevice = configModel.bootDevice(config); + const isDefaultBoot = configModel.boot.isDefault(config); + const bootDevice = configModel.boot.findDevice(config); let selectedOption: string; if (!bootModel.configure) { @@ -118,9 +116,9 @@ export default function BootSelection() { candidateDevices: candidates, selectedOption, }); - }, [devices, candidateDevices, model, state.load, config]); + }, [devices, candidateDevices, config, state.load]); - if (!state.load || !model) return; + if (!state.load || !config) return; const onSubmit = async (e) => { e.preventDefault(); diff --git a/web/src/components/storage/ConfigEditor.tsx b/web/src/components/storage/ConfigEditor.tsx index f0f96abbe7..1365e5adda 100644 --- a/web/src/components/storage/ConfigEditor.tsx +++ b/web/src/components/storage/ConfigEditor.tsx @@ -28,7 +28,7 @@ import VolumeGroupEditor from "~/components/storage/VolumeGroupEditor"; import MdRaidEditor from "~/components/storage/MdRaidEditor"; import { useReset } from "~/hooks/model/config/storage"; import ConfigureDeviceMenu from "./ConfigureDeviceMenu"; -import { useModel } from "~/hooks/storage/model"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import { _ } from "~/i18n"; const NoDevicesConfiguredAlert = () => { @@ -57,10 +57,10 @@ const NoDevicesConfiguredAlert = () => { }; export default function ConfigEditor() { - const model = useModel(); - const drives = model.drives; - const mdRaids = model.mdRaids; - const volumeGroups = model.volumeGroups; + const config = useConfigModel(); + const drives = config.drives; + const mdRaids = config.mdRaids; + const volumeGroups = config.volumeGroups; if (!drives.length && !mdRaids.length && !volumeGroups.length) { return ; diff --git a/web/src/components/storage/ConfigureDeviceMenu.tsx b/web/src/components/storage/ConfigureDeviceMenu.tsx index dedadc7741..5594776e56 100644 --- a/web/src/components/storage/ConfigureDeviceMenu.tsx +++ b/web/src/components/storage/ConfigureDeviceMenu.tsx @@ -25,14 +25,12 @@ import { useNavigate } from "react-router"; import MenuButton, { MenuButtonItem } from "~/components/core/MenuButton"; import { Divider, Flex, MenuItemProps } from "@patternfly/react-core"; import { useAvailableDevices } from "~/hooks/model/system/storage"; -import { useModel } from "~/hooks/storage/model"; -import { useAddDrive } from "~/hooks/storage/drive"; -import { useAddReusedMdRaid } from "~/hooks/storage/md-raid"; +import { useConfigModel, useAddDrive, useAddMdRaid } from "~/hooks/model/storage/config-model"; import { STORAGE as PATHS } from "~/routes/paths"; import { sprintf } from "sprintf-js"; import { _, n_ } from "~/i18n"; import DeviceSelectorModal from "./DeviceSelectorModal"; -import { isDrive } from "~/storage/device"; +import { isDrive } from "~/model/storage/device"; import { Icon } from "../layout"; import type { Storage } from "~/model/system"; @@ -126,12 +124,12 @@ export default function ConfigureDeviceMenu(): React.ReactNode { const navigate = useNavigate(); - const model = useModel(); + const config = useConfigModel(); const addDrive = useAddDrive(); - const addReusedMdRaid = useAddReusedMdRaid(); + const addReusedMdRaid = useAddMdRaid(); const allDevices = useAvailableDevices(); - const usedDevicesNames = model.drives.concat(model.mdRaids).map((d) => d.name); + const usedDevicesNames = config.drives.concat(config.mdRaids).map((d) => d.name); const usedDevicesCount = usedDevicesNames.length; const devices = allDevices.filter((d) => !usedDevicesNames.includes(d.name)); const withRaids = !!allDevices.filter((d) => !isDrive(d)).length; diff --git a/web/src/components/storage/DeviceEditorContent.tsx b/web/src/components/storage/DeviceEditorContent.tsx index c8dee4214e..483aeeac9a 100644 --- a/web/src/components/storage/DeviceEditorContent.tsx +++ b/web/src/components/storage/DeviceEditorContent.tsx @@ -25,7 +25,7 @@ import UnusedMenu from "~/components/storage/UnusedMenu"; import FilesystemMenu from "~/components/storage/FilesystemMenu"; import PartitionsSection from "~/components/storage/PartitionsSection"; import SpacePolicyMenu from "~/components/storage/SpacePolicyMenu"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import configModel from "~/model/storage/config-model"; type DeviceEditorContentProps = { @@ -39,7 +39,7 @@ export default function DeviceEditorContent({ }: DeviceEditorContentProps): React.ReactNode { const config = useConfigModel(); const device = config[collection][index]; - const isUsed = configModel.isUsedDevice(config, device.name); + const isUsed = configModel.partitionable.isUsed(config, device.name); if (!isUsed) return ; diff --git a/web/src/components/storage/DeviceSelectorModal.tsx b/web/src/components/storage/DeviceSelectorModal.tsx index 739e138de2..ffa5169151 100644 --- a/web/src/components/storage/DeviceSelectorModal.tsx +++ b/web/src/components/storage/DeviceSelectorModal.tsx @@ -35,7 +35,7 @@ import { import { deviceSize } from "~/components/storage/utils"; import { sortCollection } from "~/utils"; import { _ } from "~/i18n"; -import { deviceSystems } from "~/storage/device"; +import { deviceSystems } from "~/model/storage/device"; import type { Storage } from "~/model/system"; type DeviceSelectorProps = { diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index 80162c6b3e..f337c51def 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -27,10 +27,9 @@ import DriveHeader from "~/components/storage/DriveHeader"; import DeviceEditorContent from "~/components/storage/DeviceEditorContent"; import SearchedDeviceMenu from "~/components/storage/SearchedDeviceMenu"; import { CustomToggleProps } from "~/components/core/MenuButton"; -import { useDeleteDrive } from "~/hooks/storage/drive"; import { Button, Flex, FlexItem } from "@patternfly/react-core"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; -import { useDrive } from "~/hooks/storage/model"; +import { useDrive, useDeleteDrive } from "~/hooks/model/storage/config-model"; import { useDevice } from "~/hooks/model/system/storage"; import type { ConfigModel } from "~/model/storage/config-model"; import type { Storage as System } from "~/model/system"; @@ -69,23 +68,24 @@ const DriveDeviceMenuToggle = forwardRef( ); type DriveDeviceMenuProps = { - drive: ConfigModel.Drive; - selected: System.Device; + index: number; }; /** * Internal component that renders generic actions available for a Drive device. */ -const DriveDeviceMenu = ({ drive, selected }: DriveDeviceMenuProps) => { +const DriveDeviceMenu = ({ index }: DriveDeviceMenuProps) => { + const driveModel = useDrive(index); + const drive = useDevice(driveModel.name); const deleteDrive = useDeleteDrive(); - const deleteFn = (device: ConfigModel.Drive) => deleteDrive(device.name); + const deleteFn = () => deleteDrive(index); return ( } + toggle={} /> ); }; @@ -107,7 +107,7 @@ export default function DriveEditor({ index }: DriveEditorProps) { if (drive === undefined) return null; return ( - }> + }> ); diff --git a/web/src/components/storage/DriveHeader.tsx b/web/src/components/storage/DriveHeader.tsx index afc7d64c4b..3ff6c1f500 100644 --- a/web/src/components/storage/DriveHeader.tsx +++ b/web/src/components/storage/DriveHeader.tsx @@ -22,9 +22,8 @@ import { sprintf } from "sprintf-js"; import { deviceLabel } from "./utils"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import configModel from "~/model/storage/config-model"; -import partitionableModel from "~/model/storage/partitionable-model"; import { _ } from "~/i18n"; import type { ConfigModel } from "~/model/storage/config-model"; import type { Storage } from "~/model/system"; @@ -41,10 +40,10 @@ const Text = (drive: ConfigModel.Drive): string => { return _("Format disk %s"); } - const isBoot = configModel.isBootDevice(config, drive.name); + const isBoot = configModel.boot.hasDevice(config, drive.name); const hasPv = configModel.isTargetDevice(config, drive.name); - const isRoot = !!partitionableModel.findPartition(drive, "/"); - const hasFs = !!partitionableModel.usedMountPaths(drive).length; + const isRoot = !!configModel.partitionable.findPartition(drive, "/"); + const hasFs = !!configModel.partitionable.usedMountPaths(drive).length; if (isRoot) { if (hasPv) { diff --git a/web/src/components/storage/EncryptionSection.tsx b/web/src/components/storage/EncryptionSection.tsx index 5e40189aa8..e1fbd7e140 100644 --- a/web/src/components/storage/EncryptionSection.tsx +++ b/web/src/components/storage/EncryptionSection.tsx @@ -25,7 +25,7 @@ import { Content, Flex, Split, Stack } from "@patternfly/react-core"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { Link } from "~/components/core"; import Icon from "~/components/layout/Icon"; -import { useEncryption } from "~/queries/storage/config-model"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import { STORAGE } from "~/routes/paths"; import { _ } from "~/i18n"; import PasswordCheck from "~/components/users/PasswordCheck"; @@ -39,7 +39,8 @@ function encryptionLabel(method?: ConfigModel.EncryptionMethod) { } export default function EncryptionSection() { - const { encryption } = useEncryption(); + const configModel = useConfigModel(); + const encryption = configModel?.encryption; const method = encryption?.method; const password = encryption?.password; diff --git a/web/src/components/storage/EncryptionSettingsPage.tsx b/web/src/components/storage/EncryptionSettingsPage.tsx index c3dd20a591..9eebb88db1 100644 --- a/web/src/components/storage/EncryptionSettingsPage.tsx +++ b/web/src/components/storage/EncryptionSettingsPage.tsx @@ -26,7 +26,7 @@ import { ActionGroup, Alert, Checkbox, Content, Form } from "@patternfly/react-c import { NestedContent, Page, PasswordAndConfirmationInput } from "~/components/core"; import PasswordCheck from "~/components/users/PasswordCheck"; import { useEncryptionMethods } from "~/hooks/model/system/storage"; -import { useEncryption } from "~/queries/storage/config-model"; +import { useConfigModel, useSetEncryption } from "~/hooks/model/storage/config-model"; import { isEmpty } from "radashi"; import { _ } from "~/i18n"; import type { ConfigModel } from "~/model/storage/config-model"; @@ -37,8 +37,9 @@ import type { ConfigModel } from "~/model/storage/config-model"; export default function EncryptionSettingsPage() { const navigate = useNavigate(); const location = useLocation(); - const { encryption: encryptionConfig, enable, disable } = useEncryption(); const methods = useEncryptionMethods(); + const configModel = useConfigModel(); + const setEncryption = useSetEncryption(); const [errors, setErrors] = useState([]); const [isEnabled, setIsEnabled] = useState(false); @@ -49,12 +50,12 @@ export default function EncryptionSettingsPage() { const formId = "encryptionSettingsForm"; useEffect(() => { - if (encryptionConfig) { + if (configModel?.encryption) { setIsEnabled(true); - setMethod(encryptionConfig.method); - setPassword(encryptionConfig.password || ""); + setMethod(configModel.encryption.method); + setPassword(configModel.encryption.password || ""); } - }, [encryptionConfig]); + }, [configModel]); const changePassword = (_, v: string) => setPassword(v); @@ -81,7 +82,7 @@ export default function EncryptionSettingsPage() { return; } - const commit = () => (isEnabled ? enable(method, password) : disable()); + const commit = () => (isEnabled ? setEncryption({ method, password }) : setEncryption(null)); commit(); navigate({ pathname: "..", search: location.search }); @@ -128,7 +129,7 @@ at the new file systems, including data, programs, and system files.", { @@ -442,7 +446,10 @@ export default function FormattableDevicePage() { const onSubmit = () => { const data = toData(value); - addFilesystem(collection, Number(index), data); + const location = createPartitionableLocation(collection, index); + if (!location) return; + + addFilesystem(location.collection, location.index, data); navigate(PATHS.root); }; diff --git a/web/src/components/storage/LogicalVolumePage.tsx b/web/src/components/storage/LogicalVolumePage.tsx index 544a0ac006..363c69c008 100644 --- a/web/src/components/storage/LogicalVolumePage.tsx +++ b/web/src/components/storage/LogicalVolumePage.tsx @@ -52,21 +52,22 @@ import SelectTypeaheadCreatable from "~/components/core/SelectTypeaheadCreatable import AutoSizeText from "~/components/storage/AutoSizeText"; import { deviceSize, filesystemLabel, parseToBytes } from "~/components/storage/utils"; import configModel from "~/model/storage/config-model"; -import { useSolvedConfigModel, useConfigModel } from "~/hooks/model/storage"; -import { useMissingMountPaths } from "~/hooks/storage/model"; +import { + useSolvedConfigModel, + useConfigModel, + useMissingMountPaths, + useVolumeGroup, + useAddLogicalVolume, + useEditLogicalVolume, +} from "~/hooks/model/storage/config-model"; import { useVolumeTemplate } from "~/hooks/model/system/storage"; -import { useVolumeGroup } from "~/hooks/storage/volume-group"; -import { useAddLogicalVolume, useEditLogicalVolume } from "~/hooks/storage/logical-volume"; -import { addLogicalVolume, editLogicalVolume } from "~/storage/logical-volume"; -import { buildLogicalVolumeName } from "~/storage/api-model"; import { STORAGE as PATHS } from "~/routes/paths"; import { unique } from "radashi"; import { compact } from "~/utils"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import SizeModeSelect, { SizeMode, SizeRange } from "~/components/storage/SizeModeSelect"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; const NO_VALUE = ""; const BTRFS_SNAPSHOTS = "btrfsSnapshots"; @@ -353,9 +354,9 @@ function useSolvedModel(value: FormValue): ConfigModel.Config | null { if (data.filesystem && !mountPointError) { if (mountPath) { - sparseModel = editLogicalVolume(config, vgName, mountPath, data); + sparseModel = configModel.logicalVolume.edit(config, vgName, mountPath, data); } else { - sparseModel = addLogicalVolume(config, vgName, data); + sparseModel = configModel.logicalVolume.add(config, vgName, data); } } @@ -644,7 +645,7 @@ export default function LogicalVolumePage() { setAutoRefreshFilesystem(true); setAutoRefreshSize(true); setMountPoint(value); - setName(buildLogicalVolumeName(value)); + setName(configModel.logicalVolume.generateName(value)); } }; diff --git a/web/src/components/storage/LvmPage.tsx b/web/src/components/storage/LvmPage.tsx index b166456f37..ed7a59fe00 100644 --- a/web/src/components/storage/LvmPage.tsx +++ b/web/src/components/storage/LvmPage.tsx @@ -36,23 +36,20 @@ import { } from "@patternfly/react-core"; import { Page, SubtleContent } from "~/components/core"; import { useAvailableDevices } from "~/hooks/model/system/storage"; -import { useModel } from "~/hooks/storage/model"; -import { - useVolumeGroup, - useAddVolumeGroup, - useEditVolumeGroup, -} from "~/hooks/storage/volume-group"; import { deviceLabel } from "./utils"; import { contentDescription, filesystemLabels, typeDescription } from "./utils/device"; import { STORAGE as PATHS } from "~/routes/paths"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -import { deviceSystems, isDrive } from "~/storage/device"; -import partitionableModel from "~/model/storage/partitionable-model"; -import volumeGroupModel from "~/model/storage/volume-group-model"; -import { useConfigModel } from "~/hooks/model/storage"; -import type { Data } from "~/storage"; -import type { ConfigModel } from "~/model/storage/config-model"; +import { deviceSystems, isDrive } from "~/model/storage/device"; +import configModel from "~/model/storage/config-model"; +import { + useConfigModel, + useVolumeGroup, + useAddVolumeGroup, + useEditVolumeGroup, +} from "~/hooks/model/storage/config-model"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; import type { Storage } from "~/model/system"; /** @@ -62,15 +59,15 @@ import type { Storage } from "~/model/system"; */ function useLvmTargetDevices(): Storage.Device[] { const availableDevices = useAvailableDevices(); - const model = useModel(); + const config = useConfigModel(); const targetDevices = useMemo(() => { return availableDevices.filter((candidate) => { - const collection = isDrive(candidate) ? model.drives : model.mdRaids; + const collection = isDrive(candidate) ? config.drives : config.mdRaids; const device = collection.find((d) => d.name === candidate.name); return !device || !device.filesystem; }); - }, [availableDevices, model]); + }, [availableDevices, config]); return targetDevices; } @@ -101,7 +98,6 @@ export default function LvmPage() { const { id } = useParams(); const navigate = useNavigate(); const config = useConfigModel(); - const model = useModel(); const volumeGroup = useVolumeGroup(id); const addVolumeGroup = useAddVolumeGroup(); const editVolumeGroup = useEditVolumeGroup(); @@ -114,21 +110,21 @@ export default function LvmPage() { useEffect(() => { if (volumeGroup) { setName(volumeGroup.vgName); - const targetNames = volumeGroupModel - .filterTargetDevices(volumeGroup, config) + const targetNames = configModel.volumeGroup + .filterTargetDevices(config, volumeGroup) .map((d) => d.name); const targetDevices = allDevices.filter((d) => targetNames.includes(d.name)); setSelectedDevices(targetDevices); - } else if (model && !model.volumeGroups.length) { + } else if (config && !config.volumeGroups.length) { setName("system"); - const potentialTargets = model.drives.concat(model.mdRaids); + const potentialTargets = config.drives.concat(config.mdRaids); const targetNames = potentialTargets - .filter(partitionableModel.isAddingPartitions) + .filter(configModel.partitionable.isAddingPartitions) .map((d) => d.name); const targetDevices = allDevices.filter((d) => targetNames.includes(d.name)); setSelectedDevices(targetDevices); } - }, [model, config, volumeGroup, allDevices]); + }, [config, volumeGroup, allDevices]); const updateName = (_, value) => setName(value); @@ -141,7 +137,7 @@ export default function LvmPage() { }; const checkErrors = (): string[] => { - return [vgNameError(name, model, volumeGroup), targetDevicesError(selectedDevices)].filter( + return [vgNameError(name, config, volumeGroup), targetDevicesError(selectedDevices)].filter( (e) => e, ); }; diff --git a/web/src/components/storage/MdRaidEditor.tsx b/web/src/components/storage/MdRaidEditor.tsx index c23394385b..f796566e31 100644 --- a/web/src/components/storage/MdRaidEditor.tsx +++ b/web/src/components/storage/MdRaidEditor.tsx @@ -27,19 +27,13 @@ import MdRaidHeader from "~/components/storage/MdRaidHeader"; import DeviceEditorContent from "~/components/storage/DeviceEditorContent"; import SearchedDeviceMenu from "~/components/storage/SearchedDeviceMenu"; import { CustomToggleProps } from "~/components/core/MenuButton"; -import { useDeleteMdRaid } from "~/hooks/storage/md-raid"; import { Button, Flex, FlexItem } from "@patternfly/react-core"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; -import { useMdRaid } from "~/hooks/storage/model"; +import { useMdRaid, useDeleteMdRaid } from "~/hooks/model/storage/config-model"; import { useDevice } from "~/hooks/model/system/storage"; import type { ConfigModel } from "~/model/storage/config-model"; import type { Storage } from "~/model/system"; -type MdRaidDeviceMenuProps = { - raid: ConfigModel.MdRaid; - selected: Storage.Device; -}; - type MdRaidDeviceMenuToggleProps = CustomToggleProps & { raid: ConfigModel.MdRaid; device: Storage.Device; @@ -73,19 +67,23 @@ const MdRaidDeviceMenuToggle = forwardRef( }, ); +type MdRaidDeviceMenuProps = { index: number }; + /** * Internal component that renders generic actions available for an MdRaid device. */ -const MdRaidDeviceMenu = ({ raid, selected }: MdRaidDeviceMenuProps): React.ReactNode => { +const MdRaidDeviceMenu = ({ index }: MdRaidDeviceMenuProps): React.ReactNode => { + const raidModel = useMdRaid(index); + const raid = useDevice(raidModel.name); const deleteMdRaid = useDeleteMdRaid(); - const deleteFn = (device: ConfigModel.MdRaid) => deleteMdRaid(device.name); + const deleteFn = () => deleteMdRaid(index); return ( } + toggle={} /> ); }; @@ -97,10 +95,8 @@ type MdRaidEditorProps = { index: number }; * actions related to a specific MdRaid device within the storage ConfigEditor. */ export default function MdRaidEditor({ index }: MdRaidEditorProps) { - const raidModel = useMdRaid(index); - const raid = useDevice(raidModel.name); return ( - }> + }> ); diff --git a/web/src/components/storage/MdRaidHeader.tsx b/web/src/components/storage/MdRaidHeader.tsx index 13afddc47c..9ec7c0c792 100644 --- a/web/src/components/storage/MdRaidHeader.tsx +++ b/web/src/components/storage/MdRaidHeader.tsx @@ -22,9 +22,8 @@ import { sprintf } from "sprintf-js"; import { deviceLabel } from "./utils"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import configModel from "~/model/storage/config-model"; -import partitionableModel from "~/model/storage/partitionable-model"; import { _ } from "~/i18n"; import type { ConfigModel } from "~/model/storage/config-model"; import type { Storage } from "~/model/system"; @@ -41,10 +40,10 @@ const Text = (raid: ConfigModel.MdRaid): string => { return _("Format RAID %s"); } - const isBoot = configModel.isBootDevice(config, raid.name); + const isBoot = configModel.boot.hasDevice(config, raid.name); const hasPv = configModel.isTargetDevice(config, raid.name); - const isRoot = !!partitionableModel.findPartition(raid, "/"); - const hasFs = !!partitionableModel.usedMountPaths(raid).length; + const isRoot = !!configModel.partitionable.findPartition(raid, "/"); + const hasFs = !!configModel.partitionable.usedMountPaths(raid).length; if (isRoot) { if (hasPv) { diff --git a/web/src/components/storage/NewVgMenuOption.tsx b/web/src/components/storage/NewVgMenuOption.tsx index bf9cdc3070..c0486a430b 100644 --- a/web/src/components/storage/NewVgMenuOption.tsx +++ b/web/src/components/storage/NewVgMenuOption.tsx @@ -23,23 +23,25 @@ import React from "react"; import { Flex } from "@patternfly/react-core"; import { MenuButtonItem } from "~/components/core/MenuButton"; -import { useConvertToVolumeGroup } from "~/hooks/storage/volume-group"; import { deviceBaseName, formattedPath } from "~/components/storage/utils"; import { sprintf } from "sprintf-js"; import { _, n_, formatList } from "~/i18n"; -import { useConfigModel } from "~/hooks/model/storage"; -import partitionableModel from "~/model/storage/partitionable-model"; +import { + useConfigModel, + useAddVolumeGroupFromPartitionable, +} from "~/hooks/model/storage/config-model"; +import configModel from "~/model/storage/config-model"; import type { ConfigModel } from "~/model/storage/config-model"; export type NewVgMenuOptionProps = { device: ConfigModel.Drive | ConfigModel.MdRaid }; export default function NewVgMenuOption({ device }: NewVgMenuOptionProps): React.ReactNode { const config = useConfigModel(); - const convertToVg = useConvertToVolumeGroup(); + const convertToVg = useAddVolumeGroupFromPartitionable(); if (device.filesystem) return; - const vgs = partitionableModel.filterVolumeGroups(device, config); + const vgs = configModel.partitionable.filterVolumeGroups(config, device); const paths = device.partitions.filter((p) => !p.name).map((p) => formattedPath(p.mountPath)); const displayName = deviceBaseName(device, true); diff --git a/web/src/components/storage/PartitionPage.tsx b/web/src/components/storage/PartitionPage.tsx index 8a3f415f41..11d52f46bd 100644 --- a/web/src/components/storage/PartitionPage.tsx +++ b/web/src/components/storage/PartitionPage.tsx @@ -51,29 +51,30 @@ import SizeModeSelect, { SizeMode, SizeRange } from "~/components/storage/SizeMo import AlertOutOfSync from "~/components/core/AlertOutOfSync"; import ResourceNotFound from "~/components/core/ResourceNotFound"; import configModel from "~/model/storage/config-model"; -import { useAddPartition, useEditPartition } from "~/hooks/storage/partition"; +import { useVolumeTemplate, useDevice } from "~/hooks/model/system/storage"; + import { + useConfigModel, + useSolvedConfigModel, useMissingMountPaths, - useDrive as useDriveModel, - useMdRaid as useMdRaidModel, -} from "~/hooks/storage/model"; + usePartitionable, + useAddPartition, + useEditPartition, +} from "~/hooks/model/storage/config-model"; import { - addPartition as addPartitionHelper, - editPartition as editPartitionHelper, -} from "~/storage/partition"; -import { useVolumeTemplate, useDevice } from "~/hooks/model/system/storage"; - -import { useSolvedConfigModel } from "~/queries/storage/config-model"; -import { useConfigModel } from "~/hooks/model/storage"; -import { findDevice } from "~/storage/api-model"; -import { deviceSize, deviceLabel, filesystemLabel, parseToBytes } from "~/components/storage/utils"; + deviceSize, + deviceLabel, + filesystemLabel, + parseToBytes, + findPartitionableDevice, + createPartitionableLocation, +} from "~/components/storage/utils"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { STORAGE as PATHS, STORAGE } from "~/routes/paths"; import { isUndefined, unique } from "radashi"; import { compact } from "~/utils"; -import partitionableModel from "~/model/storage/partitionable-model"; -import type { ConfigModel } from "~/model/storage/config-model"; +import type { ConfigModel, Partitionable } from "~/model/storage/config-model"; import type { Storage as System } from "~/model/system"; const NO_VALUE = ""; @@ -196,10 +197,12 @@ function toFormValue(partitionConfig: ConfigModel.Partition): FormValue { }; } -function useDeviceModelFromParams() { +function useDeviceModelFromParams(): Partitionable.Device | null { const { collection, index } = useParams(); - const deviceModel = collection === "drives" ? useDriveModel : useMdRaidModel; - return deviceModel(Number(index)); + const location = createPartitionableLocation(collection, index); + const deviceModel = usePartitionable(location.collection, location.index); + + return deviceModel; } function useDeviceFromParams(): System.Device { @@ -230,7 +233,7 @@ function useDefaultFilesystem(mountPoint: string): string { function useInitialPartitionConfig(): ConfigModel.Partition | null { const { partitionId: mountPath } = useParams(); const device = useDeviceModelFromParams(); - return mountPath && device ? partitionableModel.findPartition(device, mountPath) : null; + return mountPath && device ? configModel.partitionable.findPartition(device, mountPath) : null; } function useInitialFormValue(): FormValue | null { @@ -257,7 +260,7 @@ function useUnusedPartitions(): System.Device[] { const allPartitions = device.partitions || []; const initialPartitionConfig = useInitialPartitionConfig(); const deviceModel = useDeviceModelFromParams(); - const configuredPartitionConfigs = partitionableModel + const configuredPartitionConfigs = configModel.partitionable .filterConfiguredExistingPartitions(deviceModel) .filter((p) => p.name !== initialPartitionConfig?.name) .map((p) => p.name); @@ -408,15 +411,20 @@ function useSolvedModel(value: FormValue): ConfigModel.Config | null { if (device && !errors.length && value.target === NEW_PARTITION && value.filesystem !== NO_VALUE) { if (initialPartitionConfig) { - sparseModel = editPartitionHelper( + sparseModel = configModel.partition.edit( model, modelCollection, - index, + Number(index), initialPartitionConfig.mountPath, partitionConfig, ); } else { - sparseModel = addPartitionHelper(model, modelCollection, index, partitionConfig); + sparseModel = configModel.partition.add( + model, + modelCollection, + Number(index), + partitionConfig, + ); } } @@ -425,12 +433,12 @@ function useSolvedModel(value: FormValue): ConfigModel.Config | null { } function useSolvedPartitionConfig(value: FormValue): ConfigModel.Partition | undefined { - const model = useSolvedModel(value); const { collection, index } = useParams(); + const model = useSolvedModel(value); if (!model) return; - const container = findDevice(model, collection, index); - return container?.partitions?.find((p) => p.mountPath === value.mountPoint); + const device = findPartitionableDevice(model, collection, index); + return device?.partitions?.find((p) => p.mountPath === value.mountPoint); } function useSolvedSizes(value: FormValue): SizeRange { @@ -802,11 +810,18 @@ const PartitionPageForm = () => { const onSubmit = () => { const partitionConfig = toPartitionConfig(value); - const modelCollection = collection === "drives" ? "drives" : "mdRaids"; + const partitionableLocation = createPartitionableLocation(collection, index); + if (!partitionableLocation) return; if (initialValue) - editPartition(modelCollection, index, initialValue.mountPoint, partitionConfig); - else addPartition(modelCollection, index, partitionConfig); + editPartition( + partitionableLocation.collection, + partitionableLocation.index, + initialValue.mountPoint, + partitionConfig, + ); + else + addPartition(partitionableLocation.collection, partitionableLocation.index, partitionConfig); navigate({ pathname: PATHS.root, search: location.search }); }; diff --git a/web/src/components/storage/PartitionsSection.tsx b/web/src/components/storage/PartitionsSection.tsx index 138ca8fcb2..5a7ce59247 100644 --- a/web/src/components/storage/PartitionsSection.tsx +++ b/web/src/components/storage/PartitionsSection.tsx @@ -39,8 +39,7 @@ import Text from "~/components/core/Text"; import MenuButton from "~/components/core/MenuButton"; import MountPathMenuItem from "~/components/storage/MountPathMenuItem"; import { STORAGE as PATHS } from "~/routes/paths"; -import { useDeletePartition } from "~/hooks/storage/partition"; -import { useDevice } from "~/hooks/storage/model"; +import { usePartitionable, useDeletePartition } from "~/hooks/model/storage/config-model"; import * as driveUtils from "~/components/storage/utils/drive"; import { generateEncodedPath } from "~/utils"; import * as partitionUtils from "~/components/storage/utils/partition"; @@ -51,7 +50,7 @@ import { IconProps } from "../layout/Icon"; import { sprintf } from "sprintf-js"; import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { toggle } from "radashi"; -import partitionableModel from "~/model/storage/partitionable-model"; +import configModel from "~/model/storage/config-model"; import type { ConfigModel } from "~/model/storage/config-model"; type PartitionMenuItemProps = { @@ -62,7 +61,7 @@ type PartitionMenuItemProps = { }; const PartitionMenuItem = ({ device, mountPath, collection, index }: PartitionMenuItemProps) => { - const partition = partitionableModel.findPartition(device, mountPath); + const partition = configModel.partitionable.findPartition(device, mountPath); const editPath = generateEncodedPath(PATHS.editPartition, { collection, index, @@ -222,7 +221,7 @@ export default function PartitionsSection({ collection, index }: PartitionsSecti const { uiState, setUiState } = useStorageUiState(); const toggleId = useId(); const contentId = useId(); - const device = useDevice(collection, index); + const device = usePartitionable(collection, index); const uiIndex = `${collection[0]}${index}`; const expanded = uiState.get("expanded")?.split(","); const isExpanded = expanded?.includes(uiIndex); diff --git a/web/src/components/storage/ProposalFailedInfo.test.tsx b/web/src/components/storage/ProposalFailedInfo.test.tsx index d4e0005ca2..79e0a070b7 100644 --- a/web/src/components/storage/ProposalFailedInfo.test.tsx +++ b/web/src/components/storage/ProposalFailedInfo.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProposalFailedInfo from "./ProposalFailedInfo"; -import { LogicalVolume } from "~/storage/data"; +import { LogicalVolume } from "~/model/storage/config-model/data"; import { Issue, IssueSeverity, IssueSource } from "~/model/issue"; import { apiModel } from "~/api/storage/types"; diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index b9d182bda6..a28b32e892 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -22,7 +22,7 @@ import React from "react"; import { Alert, Content } from "@patternfly/react-core"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import * as partitionUtils from "~/components/storage/utils/partition"; import { _, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index 8c8f8fd016..d466e67f9d 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -55,7 +55,7 @@ import { useAvailableDevices } from "~/hooks/model/system/storage"; import { useIssues } from "~/hooks/model/issue"; import { useReset } from "~/hooks/model/config/storage"; import { useProposal } from "~/hooks/model/proposal/storage"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import { useZFCPSupported } from "~/queries/storage/zfcp"; import { useDASDSupported } from "~/queries/storage/dasd"; import { STORAGE as PATHS } from "~/routes/paths"; diff --git a/web/src/components/storage/ProposalResultSection.tsx b/web/src/components/storage/ProposalResultSection.tsx index 1e63fa1606..c674478eb4 100644 --- a/web/src/components/storage/ProposalResultSection.tsx +++ b/web/src/components/storage/ProposalResultSection.tsx @@ -24,7 +24,7 @@ import React from "react"; import { Skeleton, Stack, Tab, Tabs, TabTitleText } from "@patternfly/react-core"; import SmallWarning from "~/components/core/SmallWarning"; import { Page, NestedContent } from "~/components/core"; -import DevicesManager from "~/storage/devices-manager"; +import DevicesManager from "~/model/storage/devices-manager"; import ProposalResultTable from "~/components/storage/ProposalResultTable"; import { ProposalActionsDialog } from "~/components/storage"; import { _, n_, formatList } from "~/i18n"; diff --git a/web/src/components/storage/ProposalResultTable.tsx b/web/src/components/storage/ProposalResultTable.tsx index 1994f7444d..b8b33cf918 100644 --- a/web/src/components/storage/ProposalResultTable.tsx +++ b/web/src/components/storage/ProposalResultTable.tsx @@ -29,13 +29,13 @@ import { toDevice, toPartitionSlot, } from "~/components/storage/device-utils"; -import DevicesManager from "~/storage/devices-manager"; +import DevicesManager from "~/model/storage/devices-manager"; import { TreeTable } from "~/components/core"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { deviceChildren, deviceSize } from "~/components/storage/utils"; import { TreeTableColumn } from "~/components/core/TreeTable"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import type { Storage as Proposal } from "~/model/proposal"; type TableItem = Proposal.Device | Proposal.UnusedSlot; diff --git a/web/src/components/storage/SearchedDeviceMenu.tsx b/web/src/components/storage/SearchedDeviceMenu.tsx index bb00321195..97660e4910 100644 --- a/web/src/components/storage/SearchedDeviceMenu.tsx +++ b/web/src/components/storage/SearchedDeviceMenu.tsx @@ -24,17 +24,18 @@ import React, { useState } from "react"; import MenuButton, { CustomToggleProps, MenuButtonItem } from "~/components/core/MenuButton"; import NewVgMenuOption from "./NewVgMenuOption"; import { useAvailableDevices } from "~/hooks/model/system/storage"; -import { useConfigModel } from "~/hooks/model/storage"; -import { useSwitchToDrive } from "~/hooks/storage/drive"; -import { useSwitchToMdRaid } from "~/hooks/storage/md-raid"; +import { + useConfigModel, + useAddDriveFromMdRaid, + useAddMdRaidFromDrive, +} from "~/hooks/model/storage/config-model"; import { deviceBaseName, formattedPath } from "~/components/storage/utils"; import configModel from "~/model/storage/config-model"; -import partitionableModel from "~/model/storage/partitionable-model"; import { sprintf } from "sprintf-js"; import { _, formatList } from "~/i18n"; import DeviceSelectorModal from "./DeviceSelectorModal"; import { MenuItemProps } from "@patternfly/react-core"; -import { isDrive } from "~/storage/device"; +import { isDrive } from "~/model/storage/device"; import type { Storage } from "~/model/system"; import type { ConfigModel } from "~/model/storage/config-model"; @@ -49,12 +50,12 @@ const useOnlyOneOption = ( const isTargetDevice = configModel.isTargetDevice(config, device.name); if ( - !partitionableModel.usedMountPaths(device).length && - (isTargetDevice || configModel.isExplicitBootDevice(config, device.name)) + !configModel.partitionable.usedMountPaths(device).length && + (isTargetDevice || configModel.boot.hasExplicitDevice(config, device.name)) ) return true; - return partitionableModel.isReusingPartitions(device); + return configModel.partitionable.isReusingPartitions(device); }; type ChangeDeviceTitleProps = { @@ -73,7 +74,7 @@ const ChangeDeviceTitle = ({ modelDevice }: ChangeDeviceTitleProps) => { return sprintf(_("Change the disk to format as %s"), formattedPath(modelDevice.mountPath)); } - const mountPaths = partitionableModel.usedMountPaths(modelDevice); + const mountPaths = configModel.partitionable.usedMountPaths(modelDevice); const hasMountPaths = mountPaths.length > 0; if (!hasMountPaths) { @@ -104,11 +105,11 @@ type ChangeDeviceDescriptionProps = { const ChangeDeviceDescription = ({ modelDevice, device }: ChangeDeviceDescriptionProps) => { const config = useConfigModel(); const name = baseName(device); - const volumeGroups = partitionableModel.filterVolumeGroups(modelDevice, config); - const isExplicitBoot = configModel.isExplicitBootDevice(config, modelDevice.name); - const isBoot = configModel.isBootDevice(config, modelDevice.name); - const mountPaths = partitionableModel.usedMountPaths(modelDevice); - const isReusingPartitions = partitionableModel.isReusingPartitions(modelDevice); + const volumeGroups = configModel.partitionable.filterVolumeGroups(config, modelDevice); + const isExplicitBoot = configModel.boot.hasExplicitDevice(config, modelDevice.name); + const isBoot = configModel.boot.hasDevice(config, modelDevice.name); + const mountPaths = configModel.partitionable.usedMountPaths(modelDevice); + const isReusingPartitions = configModel.partitionable.isReusingPartitions(modelDevice); const hasMountPaths = mountPaths.length > 0; const hasPv = volumeGroups.length > 0; const vgName = volumeGroups[0]?.vgName; @@ -240,8 +241,8 @@ const RemoveEntryOption = ({ device, onClick }: RemoveEntryOptionProps): React.R // from being fully reasonable or understandable for the user. const onlyToBoot = entries.find( (e) => - configModel.isExplicitBootDevice(config, e.name) && - !configModel.isUsedDevice(config, e.name), + configModel.boot.hasExplicitDevice(config, e.name) && + !configModel.partitionable.isUsed(config, e.name), ); return !onlyToBoot; }; @@ -251,13 +252,13 @@ const RemoveEntryOption = ({ device, onClick }: RemoveEntryOptionProps): React.R if (!hasAdditionalDrives(config)) return; let description; - const isExplicitBoot = configModel.isExplicitBootDevice(config, device.name); + const isExplicitBoot = configModel.boot.hasExplicitDevice(config, device.name); const hasPv = configModel.isTargetDevice(config, device.name); const isDisabled = isExplicitBoot || hasPv; // If these cases, the target device cannot be changed and this disabled button would only provide // information that is redundant to the one already displayed at the disabled "change device" one. - if (!partitionableModel.usedMountPaths(device).length && (hasPv || isExplicitBoot)) return; + if (!configModel.partitionable.usedMountPaths(device).length && (hasPv || isExplicitBoot)) return; if (isExplicitBoot) { if (hasPv) { @@ -298,7 +299,7 @@ const targetDevices = ( const device = collection.find((d) => d.name === availableDev.name); if (!device) return true; - if (modelDevice.filesystem) return !configModel.isUsedDevice(config, device.name); + if (modelDevice.filesystem) return !configModel.partitionable.isUsed(config, device.name); return !device.filesystem; }); @@ -323,8 +324,8 @@ export default function SearchedDeviceMenu({ deleteFn, }: SearchedDeviceMenuProps): React.ReactNode { const [isSelectorOpen, setIsSelectorOpen] = useState(false); - const switchToDrive = useSwitchToDrive(); - const switchToMdRaid = useSwitchToMdRaid(); + const switchToDrive = useAddDriveFromMdRaid(); + const switchToMdRaid = useAddMdRaidFromDrive(); const changeTargetFn = (device: Storage.Device) => { const hook = isDrive(device) ? switchToDrive : switchToMdRaid; hook(modelDevice.name, { name: device.name }); diff --git a/web/src/components/storage/SpaceActionsTable.tsx b/web/src/components/storage/SpaceActionsTable.tsx index c8ba95ecfe..65c71951cc 100644 --- a/web/src/components/storage/SpaceActionsTable.tsx +++ b/web/src/components/storage/SpaceActionsTable.tsx @@ -39,8 +39,8 @@ import { DeviceName, DeviceDetails, DeviceSize, toDevice } from "~/components/st import { Icon } from "~/components/layout"; import { TreeTableColumn } from "~/components/core/TreeTable"; import { Table, Td, Th, Tr, Thead, Tbody } from "@patternfly/react-table"; -import { useConfigModel } from "~/hooks/model/storage"; -import { supportShrink } from "~/storage/device"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; +import { supportShrink } from "~/model/storage/device"; import type { Storage as Proposal } from "~/model/proposal"; import type { ConfigModel } from "~/model/storage/config-model"; diff --git a/web/src/components/storage/SpacePolicyMenu.tsx b/web/src/components/storage/SpacePolicyMenu.tsx index 8f4318f191..6759f36e0c 100644 --- a/web/src/components/storage/SpacePolicyMenu.tsx +++ b/web/src/components/storage/SpacePolicyMenu.tsx @@ -26,13 +26,12 @@ import MenuButton, { CustomToggleProps } from "~/components/core/MenuButton"; import Text from "~/components/core/Text"; import Icon from "~/components/layout/Icon"; import { useNavigate } from "react-router"; -import { useSetSpacePolicy } from "~/hooks/storage/space-policy"; import { SPACE_POLICIES } from "~/components/storage/utils"; import { STORAGE as PATHS } from "~/routes/paths"; import * as driveUtils from "~/components/storage/utils/drive"; import { generateEncodedPath } from "~/utils"; import { isEmpty } from "radashi"; -import { useDevice as useDeviceModel } from "~/hooks/storage/model"; +import { usePartitionable, useSetSpacePolicy } from "~/hooks/model/storage/config-model"; import { useDevice } from "~/hooks/model/system/storage"; import type { ConfigModel } from "~/model/storage/config-model"; @@ -79,7 +78,7 @@ type SpacePolicyMenuProps = { export default function SpacePolicyMenu({ collection, index }: SpacePolicyMenuProps) { const navigate = useNavigate(); const setSpacePolicy = useSetSpacePolicy(); - const deviceModel = useDeviceModel(collection, index); + const deviceModel = usePartitionable(collection, index); const device = useDevice(deviceModel.name); const existingPartitions = device.partitions?.length; diff --git a/web/src/components/storage/SpacePolicySelection.tsx b/web/src/components/storage/SpacePolicySelection.tsx index 9527f4ab60..2a222d80a4 100644 --- a/web/src/components/storage/SpacePolicySelection.tsx +++ b/web/src/components/storage/SpacePolicySelection.tsx @@ -25,16 +25,15 @@ import { ActionGroup, Content, Form } from "@patternfly/react-core"; import { useNavigate, useParams } from "react-router"; import { Page } from "~/components/core"; import SpaceActionsTable, { SpacePolicyAction } from "~/components/storage/SpaceActionsTable"; -import { deviceChildren } from "~/components/storage/utils"; +import { createPartitionableLocation, deviceChildren } from "~/components/storage/utils"; import { _ } from "~/i18n"; import { useDevices } from "~/hooks/model/system/storage"; -import { useDrive as useDriveModel, useMdRaid as useMdRaidModel } from "~/hooks/storage/model"; -import { useSetSpacePolicy } from "~/hooks/storage/space-policy"; +import { usePartitionable, useSetSpacePolicy } from "~/hooks/model/storage/config-model"; import { toDevice } from "./device-utils"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { sprintf } from "sprintf-js"; import type { Storage as Proposal } from "~/model/proposal"; -import type { ConfigModel } from "~/model/storage/config-model"; +import type { ConfigModel, Partitionable } from "~/model/storage/config-model"; const partitionAction = (partition: ConfigModel.Partition) => { if (partition.delete) return "delete"; @@ -43,10 +42,12 @@ const partitionAction = (partition: ConfigModel.Partition) => { return undefined; }; -function useDeviceModelFromParams(): ConfigModel.Drive | ConfigModel.MdRaid | null { +function useDeviceModelFromParams(): Partitionable.Device | null { const { collection, index } = useParams(); - const deviceModel = collection === "drives" ? useDriveModel : useMdRaidModel; - return deviceModel(Number(index)); + const location = createPartitionableLocation(collection, index); + const deviceModel = usePartitionable(location.collection, location.index); + + return deviceModel; } /** @@ -94,7 +95,10 @@ export default function SpacePolicySelection() { const onSubmit = (e) => { e.preventDefault(); - setSpacePolicy(collection, index, { type: "custom", actions }); + const location = createPartitionableLocation(collection, index); + if (!location) return; + + setSpacePolicy(location.collection, location.index, { type: "custom", actions }); navigate(".."); }; diff --git a/web/src/components/storage/UnsupportedModelInfo.tsx b/web/src/components/storage/UnsupportedModelInfo.tsx index db81d4a6f5..43b5ad92ba 100644 --- a/web/src/components/storage/UnsupportedModelInfo.tsx +++ b/web/src/components/storage/UnsupportedModelInfo.tsx @@ -23,7 +23,7 @@ import React from "react"; import { Alert, Button, Content, Stack, StackItem } from "@patternfly/react-core"; import { _ } from "~/i18n"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import { useReset } from "~/hooks/model/config/storage"; /** diff --git a/web/src/components/storage/VolumeGroupEditor.tsx b/web/src/components/storage/VolumeGroupEditor.tsx index 7cfb094293..9c9919ed4c 100644 --- a/web/src/components/storage/VolumeGroupEditor.tsx +++ b/web/src/components/storage/VolumeGroupEditor.tsx @@ -47,23 +47,25 @@ import Icon, { IconProps } from "~/components/layout/Icon"; import { STORAGE as PATHS } from "~/routes/paths"; import { baseName, formattedPath } from "~/components/storage/utils"; import { contentDescription } from "~/components/storage/utils/volume-group"; -import { useDeleteVolumeGroup } from "~/hooks/storage/volume-group"; -import { useDeleteLogicalVolume } from "~/hooks/storage/logical-volume"; import { generateEncodedPath } from "~/utils"; import { isEmpty } from "radashi"; import { sprintf } from "sprintf-js"; import { _, n_, formatList } from "~/i18n"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { useConfigModel } from "~/hooks/model/storage"; -import volumeGroupModel from "~/model/storage/volume-group-model"; +import { + useConfigModel, + useDeleteVolumeGroup, + useDeleteLogicalVolume, +} from "~/hooks/model/storage/config-model"; +import configModel from "~/model/storage/config-model"; import type { ConfigModel } from "~/model/storage/config-model"; const DeleteVgOption = ({ vg }: { vg: ConfigModel.VolumeGroup }) => { const config = useConfigModel(); const deleteVolumeGroup = useDeleteVolumeGroup(); const lvs = vg.logicalVolumes.map((lv) => formattedPath(lv.mountPath)); - const targetDevices = volumeGroupModel.filterTargetDevices(vg, config); + const targetDevices = configModel.volumeGroup.filterTargetDevices(config, vg); const convert = targetDevices.length === 1 && !!lvs.length; let description; diff --git a/web/src/components/storage/device-utils.tsx b/web/src/components/storage/device-utils.tsx index 9ea27505db..749ab0d5a1 100644 --- a/web/src/components/storage/device-utils.tsx +++ b/web/src/components/storage/device-utils.tsx @@ -27,7 +27,7 @@ import { Label } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { deviceBaseName, deviceSize } from "~/components/storage/utils"; -import { deviceSystems, isLogicalVolume, isMd, isPartition } from "~/storage/device"; +import { deviceSystems, isLogicalVolume, isMd, isPartition } from "~/model/storage/device"; import type { Storage as System } from "~/model/system"; import type { Storage as Proposal } from "~/model/proposal"; diff --git a/web/src/components/storage/utils.ts b/web/src/components/storage/utils.ts index 19056ae392..4c6ff3d2a6 100644 --- a/web/src/components/storage/utils.ts +++ b/web/src/components/storage/utils.ts @@ -30,7 +30,8 @@ import xbytes from "xbytes"; import { _, N_ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import type { ConfigModel } from "~/model/storage/config-model"; +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Partitionable } from "~/model/storage/config-model"; import type { Storage as System } from "~/model/system"; import type { Storage as Proposal } from "~/model/proposal"; @@ -360,6 +361,29 @@ const sizeDescription = (size: ConfigModel.Size): string => { return `${minSize}`; }; +function createPartitionableLocation( + collection: string, + index: number | string, +): Partitionable.Location | null { + if (!configModel.partitionable.isCollectionName(collection) || isNaN(Number(index))) { + console.log("Invalid location: ", collection, index); + return null; + } + + return { collection, index: Number(index) }; +} + +function findPartitionableDevice( + config: ConfigModel.Config, + collection: string, + index: number | string, +): Partitionable.Device | null { + if (!configModel.partitionable.isCollectionName(collection)) return null; + if (isNaN(Number(index))) return null; + + return configModel.partitionable.find(config, collection, Number(index)); +} + export { DEFAULT_SIZE_UNIT, SIZE_METHODS, @@ -382,4 +406,6 @@ export { isTransactionalRoot, isTransactionalSystem, volumeLabel, + createPartitionableLocation, + findPartitionableDevice, }; diff --git a/web/src/components/storage/utils/drive.tsx b/web/src/components/storage/utils/drive.tsx index cb770cb8f5..a3be68f890 100644 --- a/web/src/components/storage/utils/drive.tsx +++ b/web/src/components/storage/utils/drive.tsx @@ -22,9 +22,8 @@ import { _, n_, formatList } from "~/i18n"; import { SpacePolicy, SPACE_POLICIES, baseName, formattedPath } from "~/components/storage/utils"; -import { useConfigModel } from "~/hooks/model/storage"; +import { useConfigModel } from "~/hooks/model/storage/config-model"; import configModel from "~/model/storage/config-model"; -import partitionableModel from "~/model/storage/partitionable-model"; import { sprintf } from "sprintf-js"; import type { ConfigModel } from "~/model/storage/config-model"; @@ -68,9 +67,9 @@ const resizeTextFor = (partitions) => { const SummaryForSpacePolicy = (drive: ConfigModel.Drive): string | undefined => { const config = useConfigModel(); const isTargetDevice = configModel.isTargetDevice(config, drive.name); - const isBoot = configModel.isBootDevice(config, drive.name); - const isAddingPartitions = partitionableModel.isAddingPartitions(drive); - const isReusingPartitions = partitionableModel.isReusingPartitions(drive); + const isBoot = configModel.boot.hasDevice(config, drive.name); + const isAddingPartitions = configModel.partitionable.isAddingPartitions(drive); + const isReusingPartitions = configModel.partitionable.isReusingPartitions(drive); const { spacePolicy } = drive; switch (spacePolicy) { @@ -126,9 +125,9 @@ const ContentActionsDescription = ( ): string => { const config = useConfigModel(); const isTargetDevice = configModel.isTargetDevice(config, drive.name); - const isBoot = configModel.isBootDevice(config, drive.name); - const isAddingPartitions = partitionableModel.isAddingPartitions(drive); - const isReusingPartitions = partitionableModel.isReusingPartitions(drive); + const isBoot = configModel.boot.hasDevice(config, drive.name); + const isAddingPartitions = configModel.partitionable.isAddingPartitions(drive); + const isReusingPartitions = configModel.partitionable.isReusingPartitions(drive); if (!policyId) policyId = drive.spacePolicy; diff --git a/web/src/hooks/model/storage.ts b/web/src/hooks/model/storage.ts deleted file mode 100644 index 5657602a5b..0000000000 --- a/web/src/hooks/model/storage.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useSuspenseQuery } from "@tanstack/react-query"; -import { solveStorageModel, getStorageModel } from "~/api"; -import type { ConfigModel } from "~/model/storage/config-model"; - -const configModelQuery = { - queryKey: ["storageModel"], - queryFn: getStorageModel, -}; - -function useConfigModel(): ConfigModel.Config | null { - return useSuspenseQuery(configModelQuery).data; -} - -const solvedConfigModelQuery = (config?: ConfigModel.Config) => ({ - queryKey: ["solvedStorageModel", JSON.stringify(config)], - queryFn: () => (config ? solveStorageModel(config) : Promise.resolve(null)), - staleTime: Infinity, -}); - -function useSolvedConfigModel(config?: ConfigModel.Config): ConfigModel.Config | null { - return useSuspenseQuery(solvedConfigModelQuery(config)).data; -} - -export { configModelQuery, useConfigModel, useSolvedConfigModel }; diff --git a/web/src/hooks/model/storage/config-model.ts b/web/src/hooks/model/storage/config-model.ts new file mode 100644 index 0000000000..6b3e07b4bd --- /dev/null +++ b/web/src/hooks/model/storage/config-model.ts @@ -0,0 +1,358 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import { useCallback } from "react"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { useSystem } from "~/hooks/model/system/storage"; +import { solveStorageModel, getStorageModel, putStorageModel } from "~/api"; +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Data, Partitionable } from "~/model/storage/config-model"; + +const configModelQuery = { + queryKey: ["storageModel"], + queryFn: getStorageModel, +}; + +function useConfigModel(): ConfigModel.Config | null { + return useSuspenseQuery(configModelQuery).data; +} + +const solvedConfigModelQuery = (config?: ConfigModel.Config) => ({ + queryKey: ["solvedStorageModel", JSON.stringify(config)], + queryFn: () => (config ? solveStorageModel(config) : Promise.resolve(null)), + staleTime: Infinity, +}); + +function useSolvedConfigModel(config?: ConfigModel.Config): ConfigModel.Config | null { + return useSuspenseQuery(solvedConfigModelQuery(config)).data; +} + +function useMissingMountPaths(): string[] { + const productMountPoints = useSystem()?.productMountPoints; + const { data } = useSuspenseQuery({ + ...configModelQuery, + select: useCallback( + (data: ConfigModel.Config | null): string[] => { + const currentMountPaths = data ? configModel.usedMountPaths(data) : []; + return (productMountPoints || []).filter((p) => !currentMountPaths.includes(p)); + }, + [productMountPoints], + ), + }); + return data; +} + +type SetBootDeviceFn = (deviceName: string) => void; + +function useSetBootDevice(): SetBootDeviceFn { + const config = useConfigModel(); + return (deviceName: string) => putStorageModel(configModel.boot.setDevice(config, deviceName)); +} + +type SetDefaultBootDeviceFn = () => void; + +function useSetDefaultBootDevice(): SetDefaultBootDeviceFn { + const config = useConfigModel(); + return () => putStorageModel(configModel.boot.setDefault(config)); +} + +type DisableBootConfigFn = () => void; + +function useDisableBoot(): DisableBootConfigFn { + const config = useConfigModel(); + return () => putStorageModel(configModel.boot.disable(config)); +} + +function usePartitionable( + collection: Partitionable.CollectionName, + index: number, +): Partitionable.Device | null { + const { data } = useSuspenseQuery({ + ...configModelQuery, + select: useCallback( + (data: ConfigModel.Config | null): Partitionable.Device | null => + data ? configModel.partitionable.find(data, collection, index) : null, + [collection, index], + ), + }); + return data; +} + +function useDrive(index: number): ConfigModel.Drive | null { + const { data } = useSuspenseQuery({ + ...configModelQuery, + select: useCallback( + (data: ConfigModel.Config | null): ConfigModel.Drive | null => + data ? configModel.drive.find(data, index) : null, + [index], + ), + }); + return data; +} + +type AddDriveFn = (data: Data.Drive) => void; + +function useAddDrive(): AddDriveFn { + const config = useConfigModel(); + return (data: Data.Drive) => { + putStorageModel(configModel.drive.add(config, data)); + }; +} + +type DeleteDriveFn = (inex: number) => void; + +function useDeleteDrive(): DeleteDriveFn { + const config = useConfigModel(); + return (index: number) => { + putStorageModel(configModel.drive.remove(config, index)); + }; +} + +type AddDriveFromMdRaidFn = (oldName: string, drive: Data.Drive) => void; + +function useAddDriveFromMdRaid(): AddDriveFromMdRaidFn { + const config = useConfigModel(); + return (oldName: string, drive: Data.Drive) => { + putStorageModel(configModel.drive.addFromMdRaid(config, oldName, drive)); + }; +} + +function useMdRaid(index: number): ConfigModel.MdRaid | null { + const { data } = useSuspenseQuery({ + ...configModelQuery, + select: useCallback( + (data: ConfigModel.Config | null): ConfigModel.MdRaid | null => + data ? configModel.mdRaid.find(data, index) : null, + [index], + ), + }); + return data; +} + +type AddReusedMdRaidFn = (data: Data.MdRaid) => void; + +function useAddMdRaid(): AddReusedMdRaidFn { + const config = useConfigModel(); + return (data: Data.MdRaid) => { + putStorageModel(configModel.mdRaid.add(config, data)); + }; +} + +type DeleteMdRaidFn = (index: number) => void; + +function useDeleteMdRaid(): DeleteMdRaidFn { + const config = useConfigModel(); + return (index: number) => { + putStorageModel(configModel.mdRaid.remove(config, index)); + }; +} + +type AddMdRaidFromDriveFn = (oldName: string, raid: Data.MdRaid) => void; + +function useAddMdRaidFromDrive(): AddMdRaidFromDriveFn { + const config = useConfigModel(); + return (oldName: string, raid: Data.MdRaid) => { + putStorageModel(configModel.mdRaid.addFromDrive(config, oldName, raid)); + }; +} + +function useVolumeGroup(vgName: string): ConfigModel.VolumeGroup | null { + const config = useConfigModel(); + const volumeGroup = config?.volumeGroups?.find((v) => v.vgName === vgName); + return volumeGroup || null; +} + +type AddVolumeGroupFn = (data: Data.VolumeGroup, moveContent: boolean) => void; + +function useAddVolumeGroup(): AddVolumeGroupFn { + const config = useConfigModel(); + return (data: Data.VolumeGroup, moveContent: boolean) => { + putStorageModel(configModel.volumeGroup.add(config, data, moveContent)); + }; +} + +type EditVolumeGroupFn = (vgName: string, data: Data.VolumeGroup) => void; + +function useEditVolumeGroup(): EditVolumeGroupFn { + const config = useConfigModel(); + return (vgName: string, data: Data.VolumeGroup) => { + putStorageModel(configModel.volumeGroup.edit(config, vgName, data)); + }; +} + +type DeleteVolumeGroupFn = (vgName: string, moveToDrive: boolean) => void; + +function useDeleteVolumeGroup(): DeleteVolumeGroupFn { + const config = useConfigModel(); + return (vgName: string, moveToDrive: boolean) => { + putStorageModel( + moveToDrive + ? configModel.volumeGroup.convertToPartitionable(config, vgName) + : configModel.volumeGroup.remove(config, vgName), + ); + }; +} + +type AddVolumeGroupFromPartitionableFn = (driveName: string) => void; + +function useAddVolumeGroupFromPartitionable(): AddVolumeGroupFromPartitionableFn { + const config = useConfigModel(); + return (driveName: string) => { + putStorageModel(configModel.volumeGroup.addFromPartitionable(config, driveName)); + }; +} + +type AddLogicalVolumeFn = (vgName: string, data: Data.LogicalVolume) => void; + +function useAddLogicalVolume(): AddLogicalVolumeFn { + const config = useConfigModel(); + return (vgName: string, data: Data.LogicalVolume) => { + putStorageModel(configModel.logicalVolume.add(config, vgName, data)); + }; +} + +type EditLogicalVolumeFn = (vgName: string, mountPath: string, data: Data.LogicalVolume) => void; + +function useEditLogicalVolume(): EditLogicalVolumeFn { + const config = useConfigModel(); + return (vgName: string, mountPath: string, data: Data.LogicalVolume) => { + putStorageModel(configModel.logicalVolume.edit(config, vgName, mountPath, data)); + }; +} + +type DeleteLogicalVolumeFn = (vgName: string, mountPath: string) => void; + +function useDeleteLogicalVolume(): DeleteLogicalVolumeFn { + const config = useConfigModel(); + return (vgName: string, mountPath: string) => + putStorageModel(configModel.logicalVolume.remove(config, vgName, mountPath)); +} + +type AddPartitionFn = ( + collection: Partitionable.CollectionName, + index: number, + data: Data.Partition, +) => void; + +function useAddPartition(): AddPartitionFn { + const config = useConfigModel(); + return (collection: Partitionable.CollectionName, index: number, data: Data.Partition) => { + putStorageModel(configModel.partition.add(config, collection, index, data)); + }; +} + +type EditPartitionFn = ( + collection: Partitionable.CollectionName, + index: number, + mountPath: string, + data: Data.Partition, +) => void; + +function useEditPartition(): EditPartitionFn { + const config = useConfigModel(); + return ( + collection: Partitionable.CollectionName, + index: number, + mountPath: string, + data: Data.Partition, + ) => { + putStorageModel(configModel.partition.edit(config, collection, index, mountPath, data)); + }; +} + +type DeletePartitionFn = ( + collection: Partitionable.CollectionName, + index: number, + mountPath: string, +) => void; + +function useDeletePartition(): DeletePartitionFn { + const config = useConfigModel(); + return (collection: Partitionable.CollectionName, index: number, mountPath: string) => + putStorageModel(configModel.partition.remove(config, collection, index, mountPath)); +} + +type SetFilesystemFn = ( + collection: Partitionable.CollectionName, + index: number, + data: Data.Formattable, +) => void; + +type SetEncryptionFn = (encryption?: ConfigModel.Encryption) => void; + +function useSetEncryption(): SetEncryptionFn { + const config = useConfigModel(); + return (encryption?: ConfigModel.Encryption) => + putStorageModel(configModel.setEncryption(config, encryption)); +} + +function useSetFilesystem(): SetFilesystemFn { + const config = useConfigModel(); + return (collection: Partitionable.CollectionName, index: number, data: Data.Formattable) => { + putStorageModel(configModel.partitionable.setFilesystem(config, collection, index, data)); + }; +} + +type setSpacePolicyFn = ( + collection: Partitionable.CollectionName, + index: number, + data: Data.SpacePolicy, +) => void; + +function useSetSpacePolicy(): setSpacePolicyFn { + const model = useConfigModel(); + return (collection: Partitionable.CollectionName, index: number, data: Data.SpacePolicy) => { + putStorageModel(configModel.partitionable.setSpacePolicy(model, collection, index, data)); + }; +} + +export { + useConfigModel, + useSolvedConfigModel, + useMissingMountPaths, + useSetBootDevice, + useSetDefaultBootDevice, + useDisableBoot, + usePartitionable, + useDrive, + useAddDrive, + useDeleteDrive, + useAddDriveFromMdRaid, + useMdRaid, + useAddMdRaid, + useDeleteMdRaid, + useAddMdRaidFromDrive, + useVolumeGroup, + useAddVolumeGroup, + useEditVolumeGroup, + useDeleteVolumeGroup, + useAddVolumeGroupFromPartitionable, + useAddLogicalVolume, + useEditLogicalVolume, + useDeleteLogicalVolume, + useAddPartition, + useEditPartition, + useDeletePartition, + useSetEncryption, + useSetFilesystem, + useSetSpacePolicy, +}; diff --git a/web/src/hooks/model/system/storage.ts b/web/src/hooks/model/system/storage.ts index f0f3182f13..ac8882c6d1 100644 --- a/web/src/hooks/model/system/storage.ts +++ b/web/src/hooks/model/system/storage.ts @@ -191,7 +191,7 @@ function useVolumeTemplates(): Storage.Volume[] { const selectVolumeTemplate = (data: System | null, mountPath: string): Storage.Volume | null => { const volumes = data?.storage?.volumeTemplates || []; - return volumes.find((v) => v.mountPath === mountPath); + return volumes.find((v) => v.mountPath === mountPath) || volumes.find((v) => v.mountPath === ""); }; function useVolumeTemplate(mountPath: string): Storage.Volume | null { diff --git a/web/src/hooks/storage/boot.ts b/web/src/hooks/storage/boot.ts deleted file mode 100644 index 9b03bb1f1f..0000000000 --- a/web/src/hooks/storage/boot.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { setBootDevice, setDefaultBootDevice, disableBootConfig } from "~/storage/boot"; - -type SetBootDeviceFn = (deviceName: string) => void; - -function useSetBootDevice(): SetBootDeviceFn { - const config = useConfigModel(); - return (deviceName: string) => putStorageModel(setBootDevice(config, deviceName)); -} - -type SetDefaultBootDeviceFn = () => void; - -function useSetDefaultBootDevice(): SetDefaultBootDeviceFn { - const config = useConfigModel(); - return () => putStorageModel(setDefaultBootDevice(config)); -} - -type DisableBootConfigFn = () => void; - -function useDisableBootConfig(): DisableBootConfigFn { - const config = useConfigModel(); - return () => putStorageModel(disableBootConfig(config)); -} - -export { useSetBootDevice, useSetDefaultBootDevice, useDisableBootConfig }; diff --git a/web/src/hooks/storage/drive.ts b/web/src/hooks/storage/drive.ts deleted file mode 100644 index 780b990d74..0000000000 --- a/web/src/hooks/storage/drive.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { addDrive, deleteDrive, switchToDrive } from "~/storage/drive"; -import { useModel } from "~/hooks/storage/model"; -import type { Data } from "~/storage"; -import type { ConfigModel } from "~/model/storage/config-model"; - -function useDrive(name: string): ConfigModel.Drive | null { - const model = useModel(); - const drive = model?.drives?.find((d) => d.name === name); - return drive || null; -} - -type AddDriveFn = (data: Data.Drive) => void; - -function useAddDrive(): AddDriveFn { - const config = useConfigModel(); - return (data: Data.Drive) => { - putStorageModel(addDrive(config, data)); - }; -} - -type DeleteDriveFn = (name: string) => void; - -function useDeleteDrive(): DeleteDriveFn { - const config = useConfigModel(); - return (name: string) => { - putStorageModel(deleteDrive(config, name)); - }; -} - -type SwitchToDriveFn = (oldName: string, drive: Data.Drive) => void; - -function useSwitchToDrive(): SwitchToDriveFn { - const config = useConfigModel(); - return (oldName: string, drive: Data.Drive) => { - putStorageModel(switchToDrive(config, oldName, drive)); - }; -} - -export { useDrive, useAddDrive, useDeleteDrive, useSwitchToDrive }; -export type { AddDriveFn, DeleteDriveFn, SwitchToDriveFn }; diff --git a/web/src/hooks/storage/filesystem.ts b/web/src/hooks/storage/filesystem.ts deleted file mode 100644 index 78060a8ab7..0000000000 --- a/web/src/hooks/storage/filesystem.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { configureFilesystem } from "~/storage/filesystem"; -import type { Data } from "~/storage"; - -type AddFilesystemFn = (list: string, index: number, data: Data.Formattable) => void; - -function useAddFilesystem(): AddFilesystemFn { - const config = useConfigModel(); - return (list: string, index: number, data: Data.Formattable) => { - putStorageModel(configureFilesystem(config, list, index, data)); - }; -} - -type DeleteFilesystemFn = (list: string, index: number) => void; - -function useDeleteFilesystem(): DeleteFilesystemFn { - const config = useConfigModel(); - return (list: string, index: number) => { - putStorageModel(configureFilesystem(config, list, index, {})); - }; -} - -export { useAddFilesystem, useDeleteFilesystem }; -export type { AddFilesystemFn, DeleteFilesystemFn }; diff --git a/web/src/hooks/storage/logical-volume.ts b/web/src/hooks/storage/logical-volume.ts deleted file mode 100644 index ac9db8fb00..0000000000 --- a/web/src/hooks/storage/logical-volume.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { addLogicalVolume, editLogicalVolume, deleteLogicalVolume } from "~/storage/logical-volume"; -import type { Data } from "~/storage"; - -type AddLogicalVolumeFn = (vgName: string, data: Data.LogicalVolume) => void; - -function useAddLogicalVolume(): AddLogicalVolumeFn { - const config = useConfigModel(); - return (vgName: string, data: Data.LogicalVolume) => { - putStorageModel(addLogicalVolume(config, vgName, data)); - }; -} - -type EditLogicalVolumeFn = (vgName: string, mountPath: string, data: Data.LogicalVolume) => void; - -function useEditLogicalVolume(): EditLogicalVolumeFn { - const config = useConfigModel(); - return (vgName: string, mountPath: string, data: Data.LogicalVolume) => { - putStorageModel(editLogicalVolume(config, vgName, mountPath, data)); - }; -} - -type DeleteLogicalVolumeFn = (vgName: string, mountPath: string) => void; - -function useDeleteLogicalVolume(): DeleteLogicalVolumeFn { - const config = useConfigModel(); - return (vgName: string, mountPath: string) => - putStorageModel(deleteLogicalVolume(config, vgName, mountPath)); -} - -export { useAddLogicalVolume, useEditLogicalVolume, useDeleteLogicalVolume }; -export type { DeleteLogicalVolumeFn }; diff --git a/web/src/hooks/storage/md-raid.ts b/web/src/hooks/storage/md-raid.ts deleted file mode 100644 index 023bd933f7..0000000000 --- a/web/src/hooks/storage/md-raid.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { addReusedMdRaid, deleteMdRaid, switchToMdRaid } from "~/storage/md-raid"; -import type { Data } from "~/storage"; - -type AddReusedMdRaidFn = (data: Data.MdRaid) => void; - -function useAddReusedMdRaid(): AddReusedMdRaidFn { - const config = useConfigModel(); - return (data: Data.MdRaid) => { - putStorageModel(addReusedMdRaid(config, data)); - }; -} - -type DeleteMdRaidFn = (name: string) => void; - -function useDeleteMdRaid(): DeleteMdRaidFn { - const config = useConfigModel(); - return (name: string) => { - putStorageModel(deleteMdRaid(config, name)); - }; -} - -type SwitchToMdRaidFn = (oldName: string, raid: Data.MdRaid) => void; - -function useSwitchToMdRaid(): SwitchToMdRaidFn { - const config = useConfigModel(); - return (oldName: string, raid: Data.MdRaid) => { - putStorageModel(switchToMdRaid(config, oldName, raid)); - }; -} - -export { useAddReusedMdRaid, useDeleteMdRaid, useSwitchToMdRaid }; -export type { AddReusedMdRaidFn, DeleteMdRaidFn, SwitchToMdRaidFn }; diff --git a/web/src/hooks/storage/model.ts b/web/src/hooks/storage/model.ts deleted file mode 100644 index 97ea9137c9..0000000000 --- a/web/src/hooks/storage/model.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useCallback } from "react"; -import { useSuspenseQuery } from "@tanstack/react-query"; -import { configModelQuery } from "~/hooks/model/storage"; -import { useSystem } from "~/hooks/model/system/storage"; -import configModel from "~/model/storage/config-model"; -import type { ConfigModel } from "~/model/storage/config-model"; - -function useModel(): ConfigModel.Config | null { - const { data } = useSuspenseQuery({ - ...configModelQuery, - }); - return data; -} - -function useMissingMountPaths(): string[] { - const productMountPoints = useSystem()?.productMountPoints; - const { data } = useSuspenseQuery({ - ...configModelQuery, - select: useCallback( - (data: ConfigModel.Config | null): string[] => { - const currentMountPaths = data ? configModel.usedMountPaths(data) : []; - return (productMountPoints || []).filter((p) => !currentMountPaths.includes(p)); - }, - [productMountPoints], - ), - }); - return data; -} - -function useDevice( - collection: "drives" | "mdRaids", - index: number, -): ConfigModel.Drive | ConfigModel.MdRaid | null { - const { data } = useSuspenseQuery({ - ...configModelQuery, - select: useCallback( - (data: ConfigModel.Config | null): ConfigModel.Drive | ConfigModel.MdRaid | null => - data?.[collection]?.at(index) || null, - [collection, index], - ), - }); - return data; -} - -function useDrive(index: number): ConfigModel.Drive | null { - const { data } = useSuspenseQuery({ - ...configModelQuery, - select: useCallback( - (data: ConfigModel.Config | null): ConfigModel.Drive | null => - data?.drives?.at(index) || null, - [index], - ), - }); - return data; -} - -function useMdRaid(index: number): ConfigModel.MdRaid | null { - const { data } = useSuspenseQuery({ - ...configModelQuery, - select: useCallback( - (data: ConfigModel.Config | null): ConfigModel.MdRaid | null => - data?.mdRaids?.at(index) || null, - [index], - ), - }); - return data; -} - -export { useModel, useMissingMountPaths, useDevice, useDrive, useMdRaid }; diff --git a/web/src/hooks/storage/partition.ts b/web/src/hooks/storage/partition.ts deleted file mode 100644 index ef937cc806..0000000000 --- a/web/src/hooks/storage/partition.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { Data } from "~/storage"; -import { addPartition, editPartition, deletePartition } from "~/storage/partition"; - -type AddPartitionFn = ( - collection: "drives" | "mdRaids", - index: number | string, - data: Data.Partition, -) => void; - -function useAddPartition(): AddPartitionFn { - const config = useConfigModel(); - return (collection: "drives" | "mdRaids", index: number | string, data: Data.Partition) => { - putStorageModel(addPartition(config, collection, index, data)); - }; -} - -type EditPartitionFn = ( - collection: "drives" | "mdRaids", - index: number | string, - mountPath: string, - data: Data.Partition, -) => void; - -function useEditPartition(): EditPartitionFn { - const config = useConfigModel(); - return ( - collection: "drives" | "mdRaids", - index: number | string, - mountPath: string, - data: Data.Partition, - ) => { - putStorageModel(editPartition(config, collection, index, mountPath, data)); - }; -} - -type DeletePartitionFn = ( - collection: "drives" | "mdRaids", - index: number | string, - mountPath: string, -) => void; - -function useDeletePartition(): DeletePartitionFn { - const config = useConfigModel(); - return (collection: "drives" | "mdRaids", index: number | string, mountPath: string) => - putStorageModel(deletePartition(config, collection, index, mountPath)); -} - -export { useAddPartition, useEditPartition, useDeletePartition }; -export type { DeletePartitionFn }; diff --git a/web/src/hooks/storage/space-policy.ts b/web/src/hooks/storage/space-policy.ts deleted file mode 100644 index 0f48f500ad..0000000000 --- a/web/src/hooks/storage/space-policy.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { Data } from "~/storage"; -import { setSpacePolicy } from "~/storage/space-policy"; - -type setSpacePolicyFn = ( - collection: string, - index: number | string, - data: Data.SpacePolicy, -) => void; - -function useSetSpacePolicy(): setSpacePolicyFn { - const model = useConfigModel(); - return (collection: string, index: number | string, data: Data.SpacePolicy) => { - putStorageModel(setSpacePolicy(model, collection, index, data)); - }; -} - -export { useSetSpacePolicy }; diff --git a/web/src/hooks/storage/volume-group.ts b/web/src/hooks/storage/volume-group.ts deleted file mode 100644 index a308633ac1..0000000000 --- a/web/src/hooks/storage/volume-group.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { useConfigModel } from "~/hooks/model/storage"; -import { putStorageModel } from "~/api"; -import { - addVolumeGroup, - editVolumeGroup, - deleteVolumeGroup, - volumeGroupToPartitions, - deviceToVolumeGroup, -} from "~/storage/volume-group"; -import { useModel } from "~/hooks/storage/model"; -import type { Data } from "~/storage"; -import type { ConfigModel } from "~/model/storage/config-model"; - -function useVolumeGroup(vgName: string): ConfigModel.VolumeGroup | null { - const model = useModel(); - const volumeGroup = model?.volumeGroups?.find((v) => v.vgName === vgName); - return volumeGroup || null; -} - -type AddVolumeGroupFn = (data: Data.VolumeGroup, moveContent: boolean) => void; - -function useAddVolumeGroup(): AddVolumeGroupFn { - const config = useConfigModel(); - return (data: Data.VolumeGroup, moveContent: boolean) => { - putStorageModel(addVolumeGroup(config, data, moveContent)); - }; -} - -type EditVolumeGroupFn = (vgName: string, data: Data.VolumeGroup) => void; - -function useEditVolumeGroup(): EditVolumeGroupFn { - const config = useConfigModel(); - return (vgName: string, data: Data.VolumeGroup) => { - putStorageModel(editVolumeGroup(config, vgName, data)); - }; -} - -type DeleteVolumeGroupFn = (vgName: string, moveToDrive: boolean) => void; - -function useDeleteVolumeGroup(): DeleteVolumeGroupFn { - const config = useConfigModel(); - return (vgName: string, moveToDrive: boolean) => { - putStorageModel( - moveToDrive ? volumeGroupToPartitions(config, vgName) : deleteVolumeGroup(config, vgName), - ); - }; -} - -type ConvertToVolumeGroupFn = (driveName: string) => void; - -function useConvertToVolumeGroup(): ConvertToVolumeGroupFn { - const config = useConfigModel(); - return (driveName: string) => { - putStorageModel(deviceToVolumeGroup(config, driveName)); - }; -} - -export { - useVolumeGroup, - useAddVolumeGroup, - useEditVolumeGroup, - useDeleteVolumeGroup, - useConvertToVolumeGroup, -}; -export type { AddVolumeGroupFn, EditVolumeGroupFn, DeleteVolumeGroupFn, ConvertToVolumeGroupFn }; diff --git a/web/src/model/storage/config-model.ts b/web/src/model/storage/config-model.ts index 77c3b4417e..65b00b73cb 100644 --- a/web/src/model/storage/config-model.ts +++ b/web/src/model/storage/config-model.ts @@ -20,9 +20,20 @@ * find current contact information at www.suse.com. */ -import partitionableModel from "~/model/storage/partitionable-model"; -import volumeGroupModel from "~/model/storage/volume-group-model"; +import boot from "~/model/storage/config-model/boot"; +import partitionable from "~/model/storage/config-model/partitionable"; +import drive from "~/model/storage/config-model/drive"; +import mdRaid from "~/model/storage/config-model/md-raid"; +import partition from "~/model/storage/config-model/partition"; +import volumeGroup from "~/model/storage/config-model/volume-group"; +import logicalVolume from "~/model/storage/config-model/logical-volume"; import type * as ConfigModel from "~/openapi/storage/config-model"; +import type * as Partitionable from "~/model/storage/config-model/partitionable"; +import type * as Data from "~/model/storage/config-model/data"; + +function clone(config: ConfigModel.Config): ConfigModel.Config { + return JSON.parse(JSON.stringify(config)); +} function usedMountPaths(config: ConfigModel.Config): string[] { const drives = config.drives || []; @@ -30,53 +41,37 @@ function usedMountPaths(config: ConfigModel.Config): string[] { const volumeGroups = config.volumeGroups || []; return [ - ...drives.flatMap(partitionableModel.usedMountPaths), - ...mdRaids.flatMap(partitionableModel.usedMountPaths), - ...volumeGroups.flatMap(volumeGroupModel.usedMountPaths), + ...drives.flatMap(partitionable.usedMountPaths), + ...mdRaids.flatMap(partitionable.usedMountPaths), + ...volumeGroups.flatMap(volumeGroup.usedMountPaths), ]; } -function bootDevice(config: ConfigModel.Config): ConfigModel.Drive | ConfigModel.MdRaid | null { - const targets = [...config.drives, ...config.mdRaids]; - return targets.find((d) => d.name && d.name === config.boot?.device?.name) || null; -} - -function hasDefaultBoot(config: ConfigModel.Config): boolean { - return config.boot?.device?.default || false; -} - -function isBootDevice(config: ConfigModel.Config, deviceName: string): boolean { - return config.boot?.configure && config.boot.device?.name === deviceName; -} - -function isExplicitBootDevice(config: ConfigModel.Config, deviceName: string): boolean { - return isBootDevice(config, deviceName) && !hasDefaultBoot(config); -} - function isTargetDevice(config: ConfigModel.Config, deviceName: string): boolean { const targetDevices = (config.volumeGroups || []).flatMap((v) => v.targetDevices || []); return targetDevices.includes(deviceName); } -function isUsedDevice(config: ConfigModel.Config, deviceName: string): boolean { - const drives = config.drives || []; - const mdRaids = config.mdRaids || []; - const device = drives.concat(mdRaids).find((d) => d.name === deviceName); - - return ( - isExplicitBootDevice(config, deviceName) || - isTargetDevice(config, deviceName) || - partitionableModel.usedMountPaths(device).length > 0 - ); +function setEncryption( + config: ConfigModel.Config, + encryption?: ConfigModel.Encryption, +): ConfigModel.Config { + config = clone(config); + config.encryption = encryption; + return config; } export default { + clone, usedMountPaths, - bootDevice, - hasDefaultBoot, - isBootDevice, - isExplicitBootDevice, isTargetDevice, - isUsedDevice, + setEncryption, + boot, + partitionable, + drive, + mdRaid, + partition, + volumeGroup, + logicalVolume, }; -export type { ConfigModel }; +export type { ConfigModel, Data, Partitionable }; diff --git a/web/src/model/storage/config-model/boot.ts b/web/src/model/storage/config-model/boot.ts new file mode 100644 index 0000000000..86ffaa75e3 --- /dev/null +++ b/web/src/model/storage/config-model/boot.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Partitionable } from "~/model/storage/config-model"; + +function findDevice(config: ConfigModel.Config): Partitionable.Device | null { + return ( + configModel.partitionable + .all(config) + .find((d) => d.name && d.name === config.boot?.device?.name) || null + ); +} + +function isDefault(config: ConfigModel.Config): boolean { + return config.boot?.device?.default || false; +} + +function hasDevice(config: ConfigModel.Config, deviceName: string): boolean { + return config.boot?.configure && config.boot.device?.name === deviceName; +} + +function hasExplicitDevice(config: ConfigModel.Config, deviceName: string): boolean { + return hasDevice(config, deviceName) && !isDefault(config); +} + +function setBoot(config: ConfigModel.Config, boot: ConfigModel.Boot): ConfigModel.Config { + config = configModel.clone(config); + const device = findDevice(config); + config.boot = null; + + if (device && !configModel.partitionable.isUsed(config, device.name)) { + const location = configModel.partitionable.findLocation(config, device.name); + if (location) + config = configModel.partitionable.remove(config, location.collection, location.index); + } + + config.boot = boot; + return config; +} + +function setDevice(config: ConfigModel.Config, deviceName: string): ConfigModel.Config { + const boot = { + configure: true, + device: { + default: false, + name: deviceName, + }, + }; + + return setBoot(config, boot); +} + +function setDefault(config: ConfigModel.Config): ConfigModel.Config { + const boot = { + configure: true, + device: { + default: true, + }, + }; + + return setBoot(config, boot); +} + +function disable(config: ConfigModel.Config): ConfigModel.Config { + return setBoot(config, { configure: false }); +} + +export default { + findDevice, + isDefault, + hasDevice, + hasExplicitDevice, + setDevice, + setDefault, + disable, +}; diff --git a/web/src/storage/data.ts b/web/src/model/storage/config-model/data.ts similarity index 96% rename from web/src/storage/data.ts rename to web/src/model/storage/config-model/data.ts index c8cc047836..0893298ed7 100644 --- a/web/src/storage/data.ts +++ b/web/src/model/storage/config-model/data.ts @@ -23,8 +23,7 @@ /** * Data types. * - * Types that represent the data used for managing (add, edit) config devices. These types are - * typically used by forms and mutation hooks. + * Types that represent the data used for managing (add, edit) config model devices. */ import type { ConfigModel } from "~/model/storage/config-model"; diff --git a/web/src/storage/drive.ts b/web/src/model/storage/config-model/drive.ts similarity index 59% rename from web/src/storage/drive.ts rename to web/src/model/storage/config-model/drive.ts index 7e88de73c8..68d3b22dd7 100644 --- a/web/src/storage/drive.ts +++ b/web/src/model/storage/config-model/drive.ts @@ -20,32 +20,31 @@ * find current contact information at www.suse.com. */ -import { switchSearched } from "~/storage/search"; -import { copyApiModel } from "~/storage/api-model"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; -function addDrive(config: ConfigModel.Config, data: Data.Drive): ConfigModel.Config { - config = copyApiModel(config); - config.drives ||= []; - config.drives.push(data); - - return config; +function find(config: ConfigModel.Config, index: number): ConfigModel.Drive | null { + return config.drives?.[index] ?? null; } -function deleteDrive(config: ConfigModel.Config, name: string): ConfigModel.Config { - config = copyApiModel(config); - config.drives = config.drives.filter((d) => d.name !== name); +function add(config: ConfigModel.Config, data: Data.Drive): ConfigModel.Config { + config = configModel.clone(config); + config.drives ||= []; + config.drives.push(data); return config; } -function switchToDrive( +function addFromMdRaid( config: ConfigModel.Config, oldName: string, drive: Data.Drive, ): ConfigModel.Config { - return switchSearched(config, oldName, drive.name, "drives"); + return configModel.partitionable.convert(config, oldName, drive.name, "drives"); +} + +function remove(config: ConfigModel.Config, index: number): ConfigModel.Config { + return configModel.partitionable.remove(config, "drives", index); } -export { addDrive, deleteDrive, switchToDrive }; +export default { find, add, remove, addFromMdRaid }; diff --git a/web/src/storage/logical-volume.ts b/web/src/model/storage/config-model/logical-volume.ts similarity index 53% rename from web/src/storage/logical-volume.ts rename to web/src/model/storage/config-model/logical-volume.ts index 0c592c9b86..573f8a254e 100644 --- a/web/src/storage/logical-volume.ts +++ b/web/src/model/storage/config-model/logical-volume.ts @@ -20,30 +20,45 @@ * find current contact information at www.suse.com. */ -import { copyApiModel, buildLogicalVolume } from "~/storage/api-model"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; +import configModel from "~/model/storage/config-model"; +import { createFilesystem, createSize } from "~/model/storage/config-model/utils"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; -function findVolumeGroupIndex(config: ConfigModel.Config, vgName: string): number { - return (config.volumeGroups || []).findIndex((v) => v.vgName === vgName); +function findIndex(volumeGroup: ConfigModel.VolumeGroup, mountPath: string): number { + return (volumeGroup.logicalVolumes || []).findIndex((l) => l.mountPath === mountPath); } -function findLogicalVolumeIndex(volumeGroup: ConfigModel.VolumeGroup, mountPath: string): number { - return (volumeGroup.logicalVolumes || []).findIndex((l) => l.mountPath === mountPath); +function generateName(mountPath: string): string { + return mountPath === "/" ? "root" : mountPath.split("/").pop(); +} + +function create(data: Data.LogicalVolume): ConfigModel.LogicalVolume { + return { + ...data, + filesystem: data.filesystem ? createFilesystem(data.filesystem) : undefined, + size: data.size ? createSize(data.size) : undefined, + }; +} + +function createFromPartition(partition: ConfigModel.Partition): ConfigModel.LogicalVolume { + return { + ...partition, + lvName: partition.mountPath ? generateName(partition.mountPath) : undefined, + }; } -function addLogicalVolume( +function add( config: ConfigModel.Config, vgName: string, data: Data.LogicalVolume, ): ConfigModel.Config { - config = copyApiModel(config); + config = configModel.clone(config); - const vgIndex = findVolumeGroupIndex(config, vgName); + const vgIndex = configModel.volumeGroup.findIndex(config, vgName); if (vgIndex === -1) return config; const volumeGroup = config.volumeGroups[vgIndex]; - const logicalVolume = buildLogicalVolume(data); + const logicalVolume = create(data); volumeGroup.logicalVolumes ||= []; volumeGroup.logicalVolumes.push(logicalVolume); @@ -51,43 +66,39 @@ function addLogicalVolume( return config; } -function editLogicalVolume( +function edit( config: ConfigModel.Config, vgName: string, mountPath: string, data: Data.LogicalVolume, ): ConfigModel.Config { - config = copyApiModel(config); + config = configModel.clone(config); - const vgIndex = findVolumeGroupIndex(config, vgName); + const vgIndex = configModel.volumeGroup.findIndex(config, vgName); if (vgIndex === -1) return config; const volumeGroup = config.volumeGroups[vgIndex]; - const lvIndex = findLogicalVolumeIndex(volumeGroup, mountPath); + const lvIndex = findIndex(volumeGroup, mountPath); if (lvIndex === -1) return config; const oldLogicalVolume = volumeGroup.logicalVolumes[lvIndex]; - const newLogicalVolume = { ...oldLogicalVolume, ...buildLogicalVolume(data) }; + const newLogicalVolume = { ...oldLogicalVolume, ...create(data) }; volumeGroup.logicalVolumes.splice(lvIndex, 1, newLogicalVolume); return config; } -function deleteLogicalVolume( - config: ConfigModel.Config, - vgName: string, - mountPath: string, -): ConfigModel.Config { - config = copyApiModel(config); +function remove(config: ConfigModel.Config, vgName: string, mountPath: string): ConfigModel.Config { + config = configModel.clone(config); - const vgIndex = findVolumeGroupIndex(config, vgName); + const vgIndex = configModel.volumeGroup.findIndex(config, vgName); if (vgIndex === -1) return config; const volumeGroup = config.volumeGroups[vgIndex]; - const lvIndex = findLogicalVolumeIndex(volumeGroup, mountPath); + const lvIndex = findIndex(volumeGroup, mountPath); if (lvIndex === -1) return config; volumeGroup.logicalVolumes.splice(lvIndex, 1); @@ -95,4 +106,4 @@ function deleteLogicalVolume( return config; } -export { addLogicalVolume, editLogicalVolume, deleteLogicalVolume }; +export default { generateName, create, createFromPartition, add, edit, remove }; diff --git a/web/src/storage/md-raid.ts b/web/src/model/storage/config-model/md-raid.ts similarity index 59% rename from web/src/storage/md-raid.ts rename to web/src/model/storage/config-model/md-raid.ts index 9c69638799..d60a997d46 100644 --- a/web/src/storage/md-raid.ts +++ b/web/src/model/storage/config-model/md-raid.ts @@ -20,32 +20,31 @@ * find current contact information at www.suse.com. */ -import { switchSearched } from "~/storage/search"; -import { copyApiModel } from "~/storage/api-model"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; -function addReusedMdRaid(config: ConfigModel.Config, data: Data.MdRaid): ConfigModel.Config { - config = copyApiModel(config); - config.mdRaids ||= []; - config.mdRaids.push(data); - - return config; +function find(config: ConfigModel.Config, index: number): ConfigModel.MdRaid | null { + return config.mdRaids?.[index] ?? null; } -function deleteMdRaid(config: ConfigModel.Config, name: string): ConfigModel.Config { - config = copyApiModel(config); - config.mdRaids = config.mdRaids.filter((d) => d.name !== name); +function add(config: ConfigModel.Config, data: Data.MdRaid): ConfigModel.Config { + config = configModel.clone(config); + config.mdRaids ||= []; + config.mdRaids.push(data); return config; } -function switchToMdRaid( +function addFromDrive( config: ConfigModel.Config, oldName: string, raid: Data.MdRaid, ): ConfigModel.Config { - return switchSearched(config, oldName, raid.name, "mdRaids"); + return configModel.partitionable.convert(config, oldName, raid.name, "mdRaids"); +} + +function remove(config: ConfigModel.Config, index: number): ConfigModel.Config { + return configModel.partitionable.remove(config, "mdRaids", index); } -export { addReusedMdRaid, deleteMdRaid, switchToMdRaid }; +export default { find, add, addFromDrive, remove }; diff --git a/web/src/model/storage/config-model/partition.ts b/web/src/model/storage/config-model/partition.ts new file mode 100644 index 0000000000..d5a363a775 --- /dev/null +++ b/web/src/model/storage/config-model/partition.ts @@ -0,0 +1,153 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ +import { createFilesystem, createSize } from "~/model/storage/config-model/utils"; +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Data, Partitionable } from "~/model/storage/config-model"; + +function indexByName(device: Partitionable.Device, name: string): number { + return (device.partitions || []).findIndex((p) => p.name && p.name === name); +} + +function indexByPath(device: Partitionable.Device, path: string): number { + return (device.partitions || []).findIndex((p) => p.mountPath === path); +} + +function create(data: Data.Partition): ConfigModel.Partition { + return { + ...data, + filesystem: data.filesystem ? createFilesystem(data.filesystem) : undefined, + size: data.size ? createSize(data.size) : undefined, + // Using the ESP partition id for /boot/efi may not be strictly required, but it is + // a good practice. Let's force it here since it cannot be selected in the UI. + id: data.mountPath === "/boot/efi" ? "esp" : undefined, + }; +} + +function createFromLogicalVolume(lv: ConfigModel.LogicalVolume): ConfigModel.Partition { + return { + mountPath: lv.mountPath, + filesystem: lv.filesystem, + size: lv.size, + }; +} + +/** + * Adds a new partition. + * + * If a partition already exists in the model (e.g., as effect of using the custom policy), then + * the partition is replaced. + * */ +function add( + config: ConfigModel.Config, + collection: Partitionable.CollectionName, + index: number, + data: Data.Partition, +): ConfigModel.Config { + config = configModel.clone(config); + const device = configModel.partitionable.find(config, collection, index); + + if (device === undefined) return config; + + // Reset the spacePolicy to the default value if the device goes from unused to used + if (!configModel.partitionable.isUsed(config, device.name) && device.spacePolicy === "keep") + device.spacePolicy = null; + + const partition = create(data); + const partitionIndex = indexByName(device, partition.name); + + if (partitionIndex === -1) device.partitions.push(partition); + else device.partitions[partitionIndex] = partition; + + return config; +} + +function edit( + config: ConfigModel.Config, + collection: Partitionable.CollectionName, + index: number, + mountPath: string, + data: Data.Partition, +): ConfigModel.Config { + config = configModel.clone(config); + const device = configModel.partitionable.find(config, collection, index); + + if (device === undefined) return config; + + const partitionIndex = indexByPath(device, mountPath); + if (partitionIndex === -1) return config; + + const oldPartition = device.partitions[partitionIndex]; + const newPartition = { ...oldPartition, ...create(data) }; + device.partitions.splice(partitionIndex, 1, newPartition); + + return config; +} + +function remove( + config: ConfigModel.Config, + collection: Partitionable.CollectionName, + index: number, + mountPath: string, +): ConfigModel.Config { + config = configModel.clone(config); + const device = configModel.partitionable.find(config, collection, index); + + if (device === undefined) return config; + + const partitionIndex = indexByPath(device, mountPath); + device.partitions.splice(partitionIndex, 1); + + // Do not delete anything if the device is not really used + if (!configModel.partitionable.isUsed(config, device.name)) { + device.spacePolicy = "keep"; + } + + return config; +} + +function isNew(partition: ConfigModel.Partition): boolean { + return !partition.name; +} + +function isUsed(partition: ConfigModel.Partition): boolean { + return partition.filesystem !== undefined; +} + +function isReused(partition: ConfigModel.Partition): boolean { + return !isNew(partition) && isUsed(partition); +} + +function isUsedBySpacePolicy(partition: ConfigModel.Partition): boolean { + return partition.resizeIfNeeded || partition.delete || partition.deleteIfNeeded; +} + +export default { + create, + createFromLogicalVolume, + add, + edit, + remove, + isNew, + isUsed, + isReused, + isUsedBySpacePolicy, +}; diff --git a/web/src/model/storage/config-model/partitionable.ts b/web/src/model/storage/config-model/partitionable.ts new file mode 100644 index 0000000000..868e8a6132 --- /dev/null +++ b/web/src/model/storage/config-model/partitionable.ts @@ -0,0 +1,279 @@ +/* + * Copyright (c) [2025] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 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 General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import { fork, sift } from "radashi"; +import { createFilesystem } from "~/model/storage/config-model/utils"; +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; + +type Device = ConfigModel.Drive | ConfigModel.MdRaid; + +type CollectionName = "drives" | "mdRaids"; + +type Location = { collection: CollectionName; index: number }; + +function isCollectionName(collection: string): collection is CollectionName { + return collection === "drives" || collection === "mdRaids"; +} + +function all(config: ConfigModel.Config): Device[] { + const drives = config.drives || []; + const mdRaids = config.mdRaids || []; + return [...drives, ...mdRaids]; +} + +function find( + config: ConfigModel.Config, + collection: CollectionName, + index: number, +): Device | null { + return config[collection]?.at(index) || null; +} + +function findIndex(config: ConfigModel.Config, collection: CollectionName, name: string): number { + const devices = config[collection] || []; + return devices.findIndex((d) => d.name === name); +} + +function findLocation(config: ConfigModel.Config, name: string): Location | null { + const collections: CollectionName[] = ["drives", "mdRaids"]; + + for (const collection of collections) { + const index = findIndex(config, collection, name); + if (index !== -1) { + return { collection, index }; + } + } + + return null; +} + +function findPartition(device: Device, mountPath: string): ConfigModel.Partition | undefined { + return device.partitions.find((p) => p.mountPath === mountPath); +} + +function filterVolumeGroups(config: ConfigModel.Config, device: Device): ConfigModel.VolumeGroup[] { + const volumeGroups = config.volumeGroups || []; + return volumeGroups.filter((v) => + configModel.volumeGroup.filterTargetDevices(config, v).some((d) => d.name === device.name), + ); +} + +function filterConfiguredExistingPartitions(device: Device): ConfigModel.Partition[] { + if (device.spacePolicy === "custom") + return device.partitions.filter( + (p) => + !configModel.partition.isNew(p) && + (configModel.partition.isUsed(p) || configModel.partition.isUsedBySpacePolicy(p)), + ); + + return device.partitions.filter(configModel.partition.isReused); +} + +function usedMountPaths(device: Device): string[] { + const mountPaths = (device.partitions || []).map((p) => p.mountPath); + return sift([device.mountPath, ...mountPaths]); +} + +function isUsed(config: ConfigModel.Config, deviceName: string): boolean { + const device = all(config).find((d) => d.name === deviceName); + + return ( + configModel.boot.hasExplicitDevice(config, deviceName) || + configModel.isTargetDevice(config, deviceName) || + usedMountPaths(device).length > 0 + ); +} + +function isAddingPartitions(device: Device): boolean { + return device.partitions.some((p) => p.mountPath && configModel.partition.isNew(p)); +} + +function isReusingPartitions(device: Device): boolean { + return device.partitions.some(configModel.partition.isReused); +} + +function remove( + config: ConfigModel.Config, + collection: CollectionName, + index: number, +): ConfigModel.Config { + config = configModel.clone(config); + config[collection]?.splice(index, 1); + return config; +} + +function removeIfUnused(config: ConfigModel.Config, name: string): ConfigModel.Config { + if (isUsed(config, name)) return config; + + const location = findLocation(config, name); + if (!location) return config; + + return remove(config, location.collection, location.index); +} + +function convert( + config: ConfigModel.Config, + oldName: string, + name: string, + collection: CollectionName, +): ConfigModel.Config { + if (name === oldName) return config; + + config = configModel.clone(config); + + const location = configModel.partitionable.findLocation(config, oldName); + if (!location) return config; + + const device = configModel.partitionable.find(config, location.collection, location.index); + const targetIndex = configModel.partitionable.findIndex(config, collection, name); + const target = + targetIndex === -1 ? null : configModel.partitionable.find(config, collection, targetIndex); + + if (device.filesystem) { + if (target) { + target.mountPath = device.mountPath; + target.filesystem = device.filesystem; + target.spacePolicy = "keep"; + } else { + config[collection].push({ + name, + mountPath: device.mountPath, + filesystem: device.filesystem, + spacePolicy: "keep", + }); + } + + config[location.collection].splice(location.index, 1); + return config; + } + + const [newPartitions, existingPartitions] = fork(device.partitions, configModel.partition.isNew); + const reusedPartitions = existingPartitions.filter(configModel.partition.isReused); + const keepEntry = + configModel.boot.hasExplicitDevice(config, device.name) || reusedPartitions.length; + + if (keepEntry) { + device.partitions = existingPartitions; + } else { + config[location.collection].splice(location.index, 1); + } + + if (target) { + target.partitions ||= []; + target.partitions = [...target.partitions, ...newPartitions]; + } else { + config[collection].push({ + name, + partitions: newPartitions, + spacePolicy: device.spacePolicy === "custom" ? undefined : device.spacePolicy, + }); + } + + return config; +} + +function setActions(device: ConfigModel.Drive, actions: Data.SpacePolicyAction[]) { + device.partitions ||= []; + + // Reset resize/delete actions of all current partition configs. + device.partitions + .filter((p) => p.name !== undefined) + .forEach((partition) => { + partition.delete = false; + partition.deleteIfNeeded = false; + partition.resizeIfNeeded = false; + partition.size = undefined; + }); + + // Apply the given actions. + actions.forEach(({ deviceName, value }) => { + const isDelete = value === "delete"; + const isResizeIfNeeded = value === "resizeIfNeeded"; + const partition = device.partitions.find((p) => p.name === deviceName); + + if (partition) { + partition.delete = isDelete; + partition.resizeIfNeeded = isResizeIfNeeded; + } else { + device.partitions.push({ + name: deviceName, + delete: isDelete, + resizeIfNeeded: isResizeIfNeeded, + }); + } + }); +} + +function setSpacePolicy( + config: ConfigModel.Config, + collection: CollectionName, + index: number, + data: Data.SpacePolicy, +): ConfigModel.Config { + config = configModel.clone(config); + const device = find(config, collection, index); + + if (device === undefined) return config; + + device.spacePolicy = data.type; + if (data.type === "custom") setActions(device, data.actions || []); + + return config; +} + +function setFilesystem( + config: ConfigModel.Config, + collection: CollectionName, + index: number, + data: Data.Formattable, +): ConfigModel.Config { + config = configModel.clone(config); + + const device = find(config, collection, index); + if (!device) return config; + + device.mountPath = data.mountPath; + device.filesystem = data.filesystem ? createFilesystem(data.filesystem) : undefined; + return config; +} + +export default { + isCollectionName, + all, + find, + findIndex, + findLocation, + findPartition, + filterVolumeGroups, + filterConfiguredExistingPartitions, + usedMountPaths, + isUsed, + isAddingPartitions, + isReusingPartitions, + remove, + removeIfUnused, + convert, + setSpacePolicy, + setFilesystem, +}; +export type { Device, CollectionName, Location }; diff --git a/web/src/hooks/api.ts b/web/src/model/storage/config-model/utils.ts similarity index 69% rename from web/src/hooks/api.ts rename to web/src/model/storage/config-model/utils.ts index 3d6a10e2dc..d8a696f64f 100644 --- a/web/src/hooks/api.ts +++ b/web/src/model/storage/config-model/utils.ts @@ -20,10 +20,21 @@ * find current contact information at www.suse.com. */ -export * as config from "~/hooks/model/config"; -export * as issue from "~/hooks/model/issue"; -export * as proposal from "~/hooks/model/proposal"; -export * as question from "~/hooks/model/question"; -export * as status from "~/hooks/model/status"; -export * as storage from "~/hooks/model/storage"; -export * as system from "~/hooks/model/system"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; + +function createFilesystem(data: Data.Filesystem): ConfigModel.Filesystem { + return { + ...data, + default: false, + }; +} + +function createSize(data?: Data.Size): ConfigModel.Size { + return { + ...data, + default: false, + min: data.min || 0, + }; +} + +export { createFilesystem, createSize }; diff --git a/web/src/storage/volume-group.ts b/web/src/model/storage/config-model/volume-group.ts similarity index 53% rename from web/src/storage/volume-group.ts rename to web/src/model/storage/config-model/volume-group.ts index 0a2c016d39..44eb08a45c 100644 --- a/web/src/storage/volume-group.ts +++ b/web/src/model/storage/config-model/volume-group.ts @@ -20,17 +20,9 @@ * find current contact information at www.suse.com. */ -import { isUsed, deleteIfUnused } from "~/storage/search"; -import { - copyApiModel, - partitionables, - findDevice, - buildVolumeGroup, - buildLogicalVolumeFromPartition, - buildPartitionFromLogicalVolume, -} from "~/storage/api-model"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; +import { sift } from "radashi"; +import configModel from "~/model/storage/config-model"; +import type { ConfigModel, Data } from "~/model/storage/config-model"; function movePartitions( device: ConfigModel.Drive | ConfigModel.MdRaid, @@ -44,38 +36,61 @@ function movePartitions( const logicalVolumes = volumeGroup.logicalVolumes || []; volumeGroup.logicalVolumes = [ ...logicalVolumes, - ...newPartitions.map(buildLogicalVolumeFromPartition), + ...newPartitions.map(configModel.logicalVolume.createFromPartition), ]; } -function adjustSpacePolicy(config: ConfigModel.Config, list: string, index: number) { - const device = findDevice(config, list, index); - if (device.spacePolicy !== "keep") return; - if (isUsed(config, list, index)) return; +function adjustSpacePolicies(config: ConfigModel.Config, targets: string[]) { + const devices = configModel.partitionable.all(config); + devices + .filter((d) => targets.includes(d.name)) + .filter((d) => d.spacePolicy === "keep") + .filter((d) => !configModel.partitionable.isUsed(config, d.name)) + .forEach((d) => (d.spacePolicy = null)); +} - device.spacePolicy = null; +function create(data: Data.VolumeGroup): ConfigModel.VolumeGroup { + const defaultVolumeGroup = { vgName: "system", targetDevices: [] }; + return { ...defaultVolumeGroup, ...data }; } -function adjustSpacePolicies(config: ConfigModel.Config, targets: string[]) { - ["drives", "mdRaids"].forEach((list) => { - config[list].forEach((dev, idx) => { - if (targets.includes(dev.name)) adjustSpacePolicy(config, list, idx); - }); - }); +function usedMountPaths(volumeGroup: ConfigModel.VolumeGroup): string[] { + const mountPaths = (volumeGroup.logicalVolumes || []).map((l) => l.mountPath); + return sift(mountPaths); +} + +function findIndex(config: ConfigModel.Config, vgName: string): number { + return (config.volumeGroups || []).findIndex((v) => v.vgName === vgName); } -function addVolumeGroup( +function candidateTargetDevices( + config: ConfigModel.Config, +): (ConfigModel.Drive | ConfigModel.MdRaid)[] { + const drives = config.drives || []; + const mdRaids = config.mdRaids || []; + return [...drives, ...mdRaids]; +} + +function filterTargetDevices( + config: ConfigModel.Config, + volumeGroup: ConfigModel.VolumeGroup, +): (ConfigModel.Drive | ConfigModel.MdRaid)[] { + return candidateTargetDevices(config).filter((d) => volumeGroup.targetDevices.includes(d.name)); +} + +function add( config: ConfigModel.Config, data: Data.VolumeGroup, moveContent: boolean, ): ConfigModel.Config { - config = copyApiModel(config); + config = configModel.clone(config); adjustSpacePolicies(config, data.targetDevices); - const volumeGroup = buildVolumeGroup(data); + const volumeGroup = create(data); if (moveContent) { - partitionables(config) + configModel.partitionable + .all(config) .filter((d) => data.targetDevices.includes(d.name)) .forEach((d) => movePartitions(d, volumeGroup)); } @@ -86,96 +101,106 @@ function addVolumeGroup( return config; } -function newVgName(config: ConfigModel.Config): string { - const vgs = (config.volumeGroups || []).filter((vg) => vg.vgName.match(/^system\d*$/)); - - if (!vgs.length) return "system"; - - const numbers = vgs.map((vg) => parseInt(vg.vgName.substring(6)) || 0); - return `system${Math.max(...numbers) + 1}`; -} - -function deviceToVolumeGroup(config: ConfigModel.Config, devName: string): ConfigModel.Config { - config = copyApiModel(config); - - const device = partitionables(config).find((d) => d.name === devName); - if (!device) return config; - - const volumeGroup = buildVolumeGroup({ vgName: newVgName(config), targetDevices: [devName] }); - movePartitions(device, volumeGroup); - config.volumeGroups ||= []; - config.volumeGroups.push(volumeGroup); - - return config; -} - -function editVolumeGroup( +function edit( config: ConfigModel.Config, vgName: string, data: Data.VolumeGroup, ): ConfigModel.Config { - config = copyApiModel(config); + config = configModel.clone(config); const index = (config.volumeGroups || []).findIndex((v) => v.vgName === vgName); if (index === -1) return config; const oldVolumeGroup = config.volumeGroups[index]; - const newVolumeGroup = { ...oldVolumeGroup, ...buildVolumeGroup(data) }; + const newVolumeGroup = { ...oldVolumeGroup, ...create(data) }; adjustSpacePolicies(config, newVolumeGroup.targetDevices); config.volumeGroups.splice(index, 1, newVolumeGroup); (oldVolumeGroup.targetDevices || []).forEach((d) => { - config = deleteIfUnused(config, d); + config = configModel.partitionable.removeIfUnused(config, d); }); return config; } -function volumeGroupToPartitions(config: ConfigModel.Config, vgName: string): ConfigModel.Config { - config = copyApiModel(config); +function remove(config: ConfigModel.Config, vgName: string): ConfigModel.Config { + config = configModel.clone(config); const index = (config.volumeGroups || []).findIndex((v) => v.vgName === vgName); if (index === -1) return config; - const targetDevice = config.volumeGroups[index].targetDevices[0]; - if (!targetDevice) return config; + const targetDevices = config.volumeGroups[index].targetDevices || []; + + config.volumeGroups.splice(index, 1); + if (!targetDevices.length) return config; - const device = partitionables(config).find((d) => d.name === targetDevice); + let deletedConfig = configModel.clone(config); + targetDevices.forEach((d) => { + deletedConfig = configModel.partitionable.removeIfUnused(deletedConfig, d); + }); + + // Do not delete the underlying drives if that results in an empty configuration + return configModel.partitionable.all(deletedConfig).length ? deletedConfig : config; +} + +function generateName(config: ConfigModel.Config): string { + const vgs = (config.volumeGroups || []).filter((vg) => vg.vgName.match(/^system\d*$/)); + + if (!vgs.length) return "system"; + + const numbers = vgs.map((vg) => parseInt(vg.vgName.substring(6)) || 0); + return `system${Math.max(...numbers) + 1}`; +} + +function addFromPartitionable(config: ConfigModel.Config, devName: string): ConfigModel.Config { + config = configModel.clone(config); + + const device = configModel.partitionable.all(config).find((d) => d.name === devName); if (!device) return config; - const logicalVolumes = config.volumeGroups[index].logicalVolumes || []; - config.volumeGroups.splice(index, 1); - const partitions = device.partitions || []; - device.partitions = [...partitions, ...logicalVolumes.map(buildPartitionFromLogicalVolume)]; + const volumeGroup = create({ + vgName: generateName(config), + targetDevices: [devName], + }); + movePartitions(device, volumeGroup); + config.volumeGroups ||= []; + config.volumeGroups.push(volumeGroup); return config; } -function deleteVolumeGroup(config: ConfigModel.Config, vgName: string): ConfigModel.Config { - config = copyApiModel(config); +function convertToPartitionable(config: ConfigModel.Config, vgName: string): ConfigModel.Config { + config = configModel.clone(config); const index = (config.volumeGroups || []).findIndex((v) => v.vgName === vgName); if (index === -1) return config; - const targetDevices = config.volumeGroups[index].targetDevices || []; + const targetDevice = config.volumeGroups[index].targetDevices[0]; + if (!targetDevice) return config; - config.volumeGroups.splice(index, 1); - if (!targetDevices.length) return config; + const device = configModel.partitionable.all(config).find((d) => d.name === targetDevice); + if (!device) return config; - let deletedApiModel = copyApiModel(config); - targetDevices.forEach((d) => { - deletedApiModel = deleteIfUnused(deletedApiModel, d); - }); + const logicalVolumes = config.volumeGroups[index].logicalVolumes || []; + config.volumeGroups.splice(index, 1); + const partitions = device.partitions || []; + device.partitions = [ + ...partitions, + ...logicalVolumes.map(configModel.partition.createFromLogicalVolume), + ]; - // Do not delete the underlying drives if that results in an empty configuration - return partitionables(deletedApiModel).length ? deletedApiModel : config; + return config; } -export { - addVolumeGroup, - editVolumeGroup, - deleteVolumeGroup, - volumeGroupToPartitions, - deviceToVolumeGroup, +export default { + create, + usedMountPaths, + findIndex, + filterTargetDevices, + add, + edit, + remove, + addFromPartitionable, + convertToPartitionable, }; diff --git a/web/src/storage/device.ts b/web/src/model/storage/device.ts similarity index 100% rename from web/src/storage/device.ts rename to web/src/model/storage/device.ts diff --git a/web/src/storage/devices-manager.test.ts b/web/src/model/storage/devices-manager.test.ts similarity index 99% rename from web/src/storage/devices-manager.test.ts rename to web/src/model/storage/devices-manager.test.ts index 9046b64a47..d76de1bed4 100644 --- a/web/src/storage/devices-manager.test.ts +++ b/web/src/model/storage/devices-manager.test.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import DevicesManager from "~/storage/devices-manager"; +import DevicesManager from "~/model/storage/devices-manager"; import type { Storage as System } from "~/model/system"; import type { Storage as Proposal } from "~/model/proposal"; diff --git a/web/src/storage/devices-manager.ts b/web/src/model/storage/devices-manager.ts similarity index 98% rename from web/src/storage/devices-manager.ts rename to web/src/model/storage/devices-manager.ts index 618b795cf6..c77e0350e1 100644 --- a/web/src/storage/devices-manager.ts +++ b/web/src/model/storage/devices-manager.ts @@ -22,7 +22,7 @@ import { unique } from "radashi"; import { compact } from "~/utils"; -import { deviceSystems, isDrive, isMd, isVolumeGroup } from "~/storage/device"; +import { deviceSystems, isDrive, isMd, isVolumeGroup } from "~/model/storage/device"; import type { Storage as System } from "~/model/system"; import type { Storage as Proposal } from "~/model/proposal"; diff --git a/web/src/model/storage/partition-model.ts b/web/src/model/storage/partition-model.ts deleted file mode 100644 index 4e49d0a7f0..0000000000 --- a/web/src/model/storage/partition-model.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import type { ConfigModel } from "~/model/storage/config-model"; - -function isNew(partition: ConfigModel.Partition): boolean { - return !partition.name; -} - -function isUsed(partition: ConfigModel.Partition): boolean { - return partition.filesystem !== undefined; -} - -function isReused(partition: ConfigModel.Partition): boolean { - return !isNew(partition) && isUsed(partition); -} - -function isUsedBySpacePolicy(partition: ConfigModel.Partition): boolean { - return partition.resizeIfNeeded || partition.delete || partition.deleteIfNeeded; -} - -export default { isNew, isUsed, isReused, isUsedBySpacePolicy }; diff --git a/web/src/model/storage/partitionable-model.ts b/web/src/model/storage/partitionable-model.ts deleted file mode 100644 index b3bde52cc8..0000000000 --- a/web/src/model/storage/partitionable-model.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { sift } from "radashi"; -import partitionModel from "~/model/storage/partition-model"; -import volumeGroupModel from "~/model/storage/volume-group-model"; -import type { ConfigModel } from "~/model/storage/config-model"; - -type Partitionable = ConfigModel.Drive | ConfigModel.MdRaid; - -function usedMountPaths(device: Partitionable): string[] { - const mountPaths = (device.partitions || []).map((p) => p.mountPath); - return sift([device.mountPath, ...mountPaths]); -} - -function isAddingPartitions(device: Partitionable): boolean { - return device.partitions.some((p) => p.mountPath && partitionModel.isNew(p)); -} - -function isReusingPartitions(device: Partitionable): boolean { - return device.partitions.some(partitionModel.isReused); -} - -function findPartition( - device: Partitionable, - mountPath: string, -): ConfigModel.Partition | undefined { - return device.partitions.find((p) => p.mountPath === mountPath); -} - -function filterVolumeGroups( - device: Partitionable, - config: ConfigModel.Config, -): ConfigModel.VolumeGroup[] { - const volumeGroups = config.volumeGroups || []; - return volumeGroups.filter((v) => - volumeGroupModel.filterTargetDevices(v, config).some((d) => d.name === device.name), - ); -} - -function filterConfiguredExistingPartitions(device: Partitionable): ConfigModel.Partition[] { - if (device.spacePolicy === "custom") - return device.partitions.filter( - (p) => - !partitionModel.isNew(p) && - (partitionModel.isUsed(p) || partitionModel.isUsedBySpacePolicy(p)), - ); - - return device.partitions.filter(partitionModel.isReused); -} - -export default { - usedMountPaths, - isAddingPartitions, - isReusingPartitions, - findPartition, - filterVolumeGroups, - filterConfiguredExistingPartitions, -}; diff --git a/web/src/model/storage/volume-group-model.ts b/web/src/model/storage/volume-group-model.ts deleted file mode 100644 index f327886551..0000000000 --- a/web/src/model/storage/volume-group-model.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { sift } from "radashi"; -import type { ConfigModel } from "~/model/storage/config-model"; - -function usedMountPaths(volumeGroup: ConfigModel.VolumeGroup): string[] { - const mountPaths = (volumeGroup.logicalVolumes || []).map((l) => l.mountPath); - return sift(mountPaths); -} - -function candidateTargetDevices( - config: ConfigModel.Config, -): (ConfigModel.Drive | ConfigModel.MdRaid)[] { - const drives = config.drives || []; - const mdRaids = config.mdRaids || []; - return [...drives, ...mdRaids]; -} - -function filterTargetDevices( - volumeGroup: ConfigModel.VolumeGroup, - config: ConfigModel.Config, -): (ConfigModel.Drive | ConfigModel.MdRaid)[] { - return candidateTargetDevices(config).filter((d) => volumeGroup.targetDevices.includes(d.name)); -} - -export default { usedMountPaths, filterTargetDevices }; diff --git a/web/src/queries/storage/config-model.ts b/web/src/queries/storage/config-model.ts deleted file mode 100644 index d840bb5ac1..0000000000 --- a/web/src/queries/storage/config-model.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) [2024-2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -/** @deprecated These hooks will be replaced by new hooks at ~/hooks/storage/ */ - -import { useSuspenseQuery } from "@tanstack/react-query"; -import { putStorageModel, solveStorageModel } from "~/api"; -import { useConfigModel } from "~/hooks/model/storage"; -import { useVolumeTemplates } from "~/hooks/model/system/storage"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Storage } from "~/model/system"; - -function copyModel(model: ConfigModel.Config): ConfigModel.Config { - return JSON.parse(JSON.stringify(model)); -} - -function findDrive(model: ConfigModel.Config, driveName: string): ConfigModel.Drive | undefined { - const drives = model?.drives || []; - return drives.find((d) => d.name === driveName); -} - -function findPartition( - model: ConfigModel.Config, - driveName: string, - mountPath: string, -): ConfigModel.Partition | undefined { - const drive = findDrive(model, driveName); - if (drive === undefined) return undefined; - - const partitions = drive.partitions || []; - return partitions.find((p) => p.mountPath === mountPath); -} - -function isBoot(model: ConfigModel.Config, driveName: string): boolean { - return model.boot?.configure && driveName === model.boot?.device?.name; -} - -function isExplicitBoot(model: ConfigModel.Config, driveName: string): boolean { - return !model.boot?.device?.default && driveName === model.boot?.device?.name; -} - -function driveHasPv(model: ConfigModel.Config, name: string): boolean { - if (!name) return false; - - return model.volumeGroups.flatMap((g) => g.targetDevices).includes(name); -} - -function allMountPaths(drive: ConfigModel.Drive): string[] { - if (drive.mountPath) return [drive.mountPath]; - - return drive.partitions.map((p) => p.mountPath).filter((m) => m); -} - -function setEncryption( - originalModel: ConfigModel.Config, - method: ConfigModel.EncryptionMethod, - password: string, -): ConfigModel.Config { - const model = copyModel(originalModel); - model.encryption = { method, password }; - return model; -} - -function disableEncryption(originalModel: ConfigModel.Config): ConfigModel.Config { - const model = copyModel(originalModel); - model.encryption = null; - return model; -} - -function addDrive(originalModel: ConfigModel.Config, driveName: string): ConfigModel.Config { - if (findDrive(originalModel, driveName)) return; - - const model = copyModel(originalModel); - model.drives.push({ name: driveName }); - - return model; -} - -function usedMountPaths(model: ConfigModel.Config): string[] { - const drives = model.drives || []; - const volumeGroups = model.volumeGroups || []; - const logicalVolumes = volumeGroups.flatMap((v) => v.logicalVolumes || []); - - return [...drives, ...logicalVolumes].flatMap(allMountPaths); -} - -/** @depreacted Use useMissingMountPaths from ~/hooks/storage/product. */ -function unusedMountPaths(model: ConfigModel.Config, volumes: Storage.Volume[]): string[] { - const volPaths = volumes.filter((v) => v.mountPath.length).map((v) => v.mountPath); - const assigned = usedMountPaths(model); - return volPaths.filter((p) => !assigned.includes(p)); -} - -/** @deprecated Use useSolvedApiModel from ~/hooks/storage/api-model. */ -export function useSolvedConfigModel(model?: ConfigModel.Config): ConfigModel.Config | null { - const query = useSuspenseQuery({ - queryKey: ["storage", "solvedConfigModel", JSON.stringify(model)], - queryFn: () => (model ? solveStorageModel(model) : Promise.resolve(null)), - staleTime: Infinity, - }); - - return query.data; -} - -export type EncryptionHook = { - encryption?: ConfigModel.Encryption; - enable: (method: ConfigModel.EncryptionMethod, password: string) => void; - disable: () => void; -}; - -export function useEncryption(): EncryptionHook { - const model = useConfigModel(); - - return { - encryption: model?.encryption, - enable: (method: ConfigModel.EncryptionMethod, password: string) => - putStorageModel(setEncryption(model, method, password)), - disable: () => putStorageModel(disableEncryption(model)), - }; -} - -export type DriveHook = { - isBoot: boolean; - isExplicitBoot: boolean; - hasPv: boolean; - allMountPaths: string[]; - getPartition: (mountPath: string) => ConfigModel.Partition | undefined; -}; - -export function useDrive(name: string): DriveHook | null { - const model = useConfigModel(); - const drive = findDrive(model, name); - - if (drive === undefined) return null; - - return { - isBoot: isBoot(model, name), - isExplicitBoot: isExplicitBoot(model, name), - hasPv: driveHasPv(model, drive.name), - allMountPaths: allMountPaths(drive), - getPartition: (mountPath: string) => findPartition(model, name, mountPath), - }; -} - -export type ModelHook = { - model: ConfigModel.Config; - usedMountPaths: string[]; - unusedMountPaths: string[]; - addDrive: (driveName: string) => void; -}; - -/** - * Hook for operating on the collections of the model. - */ -export function useModel(): ModelHook { - const model = useConfigModel(); - const volumes = useVolumeTemplates(); - - return { - model, - addDrive: (driveName) => putStorageModel(addDrive(model, driveName)), - usedMountPaths: model ? usedMountPaths(model) : [], - unusedMountPaths: model ? unusedMountPaths(model, volumes) : [], - }; -} diff --git a/web/src/storage.ts b/web/src/storage.ts deleted file mode 100644 index ce2c5110d9..0000000000 --- a/web/src/storage.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) [2024-2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -export * as Data from "~/storage/data"; diff --git a/web/src/storage/api-model.ts b/web/src/storage/api-model.ts deleted file mode 100644 index fb33e91e2e..0000000000 --- a/web/src/storage/api-model.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; - -function copyApiModel(config: ConfigModel.Config): ConfigModel.Config { - return JSON.parse(JSON.stringify(config)); -} - -function findDevice(config: ConfigModel.Config, list: string, index: number | string) { - const collection = config[list] || []; - return collection.at(index); -} - -function findDeviceIndex(config: ConfigModel.Config, list: string, name: string) { - const collection = config[list] || []; - return collection.findIndex((d) => d.name === name); -} - -function partitionables(config: ConfigModel.Config): (ConfigModel.Drive | ConfigModel.MdRaid)[] { - return (config.drives || []).concat(config.mdRaids || []); -} - -function buildFilesystem(data?: Data.Filesystem): ConfigModel.Filesystem | undefined { - if (!data) return; - - return { - ...data, - default: false, - }; -} - -function buildSize(data?: Data.Size): ConfigModel.Size | undefined { - if (!data) return; - - return { - ...data, - default: false, - min: data.min || 0, - }; -} - -function buildVolumeGroup(data: Data.VolumeGroup): ConfigModel.VolumeGroup { - const defaultVolumeGroup = { vgName: "system", targetDevices: [] }; - return { ...defaultVolumeGroup, ...data }; -} - -function buildLogicalVolume(data: Data.LogicalVolume): ConfigModel.LogicalVolume { - return { - ...data, - filesystem: buildFilesystem(data.filesystem), - size: buildSize(data.size), - }; -} - -function buildPartition(data: Data.Partition): ConfigModel.Partition { - return { - ...data, - filesystem: buildFilesystem(data.filesystem), - size: buildSize(data.size), - // Using the ESP partition id for /boot/efi may not be strictly required, but it is - // a good practice. Let's force it here since it cannot be selected in the UI. - id: data.mountPath === "/boot/efi" ? "esp" : undefined, - }; -} - -function buildLogicalVolumeName(mountPath?: string): string | undefined { - if (!mountPath) return; - - return mountPath === "/" ? "root" : mountPath.split("/").pop(); -} - -function buildLogicalVolumeFromPartition( - partition: ConfigModel.Partition, -): ConfigModel.LogicalVolume { - return { - ...partition, - lvName: buildLogicalVolumeName(partition.mountPath), - }; -} - -function buildPartitionFromLogicalVolume(lv: ConfigModel.LogicalVolume): ConfigModel.Partition { - return { - mountPath: lv.mountPath, - filesystem: lv.filesystem, - size: lv.size, - }; -} - -export { - copyApiModel, - findDevice, - findDeviceIndex, - partitionables, - buildPartition, - buildVolumeGroup, - buildLogicalVolume, - buildLogicalVolumeName, - buildLogicalVolumeFromPartition, - buildPartitionFromLogicalVolume, -}; diff --git a/web/src/storage/boot.ts b/web/src/storage/boot.ts deleted file mode 100644 index 49b083f735..0000000000 --- a/web/src/storage/boot.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { copyApiModel } from "~/storage/api-model"; -import configModel from "~/model/storage/config-model"; -import partitionableModel from "~/model/storage/partitionable-model"; -import type { ConfigModel } from "~/model/storage/config-model"; - -function isUsed( - config: ConfigModel.Config, - device: ConfigModel.Drive | ConfigModel.MdRaid, -): boolean { - return ( - configModel.isTargetDevice(config, device.name) || - partitionableModel.usedMountPaths(device).length > 0 - ); -} - -function removeDevice( - config: ConfigModel.Config, - device: ConfigModel.Drive | ConfigModel.MdRaid, -): ConfigModel.Config { - config.drives = config.drives.filter((d) => d.name !== device.name); - config.mdRaids = config.mdRaids.filter((d) => d.name !== device.name); - return config; -} - -function setBoot(config: ConfigModel.Config, boot: ConfigModel.Boot) { - const device = configModel.bootDevice(config); - if (device && !isUsed(config, device)) removeDevice(config, device); - - config.boot = boot; - return config; -} - -function setBootDevice(config: ConfigModel.Config, deviceName: string): ConfigModel.Config { - config = copyApiModel(config); - - const boot = { - configure: true, - device: { - default: false, - name: deviceName, - }, - }; - - setBoot(config, boot); - return config; -} - -function setDefaultBootDevice(config: ConfigModel.Config): ConfigModel.Config { - config = copyApiModel(config); - - const boot = { - configure: true, - device: { - default: true, - }, - }; - - setBoot(config, boot); - return config; -} - -function disableBootConfig(config: ConfigModel.Config): ConfigModel.Config { - config = copyApiModel(config); - const boot = { configure: false }; - setBoot(config, boot); - return config; -} - -export { setBootDevice, setDefaultBootDevice, disableBootConfig }; diff --git a/web/src/storage/filesystem.ts b/web/src/storage/filesystem.ts deleted file mode 100644 index 17a9c27e9b..0000000000 --- a/web/src/storage/filesystem.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { copyApiModel } from "~/storage/api-model"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; - -function findDevice( - config: ConfigModel.Config, - list: string, - index: number | string, -): ConfigModel.Drive | ConfigModel.MdRaid | null { - return (config[list] || []).at(index) || null; -} - -function configureFilesystem( - config: ConfigModel.Config, - list: string, - index: number | string, - data: Data.Formattable, -): ConfigModel.Config { - config = copyApiModel(config); - - const device = findDevice(config, list, index); - if (!device) return config; - - device.mountPath = data.mountPath; - - if (data.filesystem) { - device.filesystem = { - default: false, - ...data.filesystem, - }; - } else { - device.filesystem = undefined; - } - - return config; -} - -export { configureFilesystem }; diff --git a/web/src/storage/partition.ts b/web/src/storage/partition.ts deleted file mode 100644 index ff491c11ee..0000000000 --- a/web/src/storage/partition.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { copyApiModel, findDevice, buildPartition } from "~/storage/api-model"; -import { isUsed } from "~/storage/search"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; - -type Partitionable = ConfigModel.Drive | ConfigModel.MdRaid; - -function indexByName(device: Partitionable, name: string): number { - return (device.partitions || []).findIndex((p) => p.name && p.name === name); -} - -function indexByPath(device: Partitionable, path: string): number { - return (device.partitions || []).findIndex((p) => p.mountPath === path); -} - -/** - * Adds a new partition. - * - * If a partition already exists in the model (e.g., as effect of using the custom policy), then - * the partition is replaced. - * */ -function addPartition( - model: ConfigModel.Config, - collection: "drives" | "mdRaids", - index: number | string, - data: Data.Partition, -): ConfigModel.Config { - model = copyApiModel(model); - const device = findDevice(model, collection, index); - - if (device === undefined) return model; - - // Reset the spacePolicy to the default value if the device goes from unused to used - if (!isUsed(model, collection, index) && device.spacePolicy === "keep") device.spacePolicy = null; - - const partition = buildPartition(data); - const partitionIndex = indexByName(device, partition.name); - - if (partitionIndex === -1) device.partitions.push(partition); - else device.partitions[partitionIndex] = partition; - - return model; -} - -function editPartition( - model: ConfigModel.Config, - collection: "drives" | "mdRaids", - index: number | string, - mountPath: string, - data: Data.Partition, -): ConfigModel.Config { - model = copyApiModel(model); - const device = findDevice(model, collection, index); - - if (device === undefined) return model; - - const partitionIndex = indexByPath(device, mountPath); - if (partitionIndex === -1) return model; - - const oldPartition = device.partitions[partitionIndex]; - const newPartition = { ...oldPartition, ...buildPartition(data) }; - device.partitions.splice(partitionIndex, 1, newPartition); - - return model; -} - -function deletePartition( - model: ConfigModel.Config, - collection: "drives" | "mdRaids", - index: number | string, - mountPath: string, -): ConfigModel.Config { - model = copyApiModel(model); - const device = findDevice(model, collection, index); - - if (device === undefined) return model; - - const partitionIndex = indexByPath(device, mountPath); - device.partitions.splice(partitionIndex, 1); - - // Do not delete anything if the device is not really used - if (!isUsed(model, collection, index)) { - device.spacePolicy = "keep"; - } - - return model; -} - -export { addPartition, editPartition, deletePartition }; diff --git a/web/src/storage/search.ts b/web/src/storage/search.ts deleted file mode 100644 index d1daf41d6c..0000000000 --- a/web/src/storage/search.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { copyApiModel, findDevice, findDeviceIndex } from "~/storage/api-model"; -import { fork } from "radashi"; -import configModel from "~/model/storage/config-model"; -import partitionModel from "~/model/storage/partition-model"; -import type { ConfigModel } from "~/model/storage/config-model"; - -function deviceLocation(config: ConfigModel.Config, name: string) { - let index; - for (const list of ["drives", "mdRaids"]) { - index = findDeviceIndex(config, list, name); - if (index !== -1) return { list, index }; - } - - return { list: undefined, index: -1 }; -} - -function buildModelDevice( - config: ConfigModel.Config, - list: string, - index: number | string, -): ConfigModel.Drive | ConfigModel.MdRaid | undefined { - return config[list].at(index); -} - -function isUsed(config: ConfigModel.Config, list: string, index: number | string): boolean { - const device = config[list].at(index); - if (!device) return false; - - return configModel.isUsedDevice(config, device.name); -} - -function deleteIfUnused(config: ConfigModel.Config, name: string): ConfigModel.Config { - config = copyApiModel(config); - - const { list, index } = deviceLocation(config, name); - if (!list) return config; - - if (isUsed(config, list, index)) return config; - - config[list].splice(index, 1); - return config; -} - -function switchSearched( - config: ConfigModel.Config, - oldName: string, - name: string, - list: "drives" | "mdRaids", -): ConfigModel.Config { - if (name === oldName) return config; - - config = copyApiModel(config); - - const { list: oldList, index } = deviceLocation(config, oldName); - if (!oldList) return config; - - const device = findDevice(config, oldList, index); - const deviceModel = buildModelDevice(config, oldList, index); - const targetIndex = findDeviceIndex(config, list, name); - const target = targetIndex === -1 ? null : findDevice(config, list, targetIndex); - - if (deviceModel.filesystem) { - if (target) { - target.mountPath = device.mountPath; - target.filesystem = device.filesystem; - target.spacePolicy = "keep"; - } else { - config[list].push({ - name, - mountPath: device.mountPath, - filesystem: device.filesystem, - spacePolicy: "keep", - }); - } - - config[oldList].splice(index, 1); - return config; - } - - const [newPartitions, existingPartitions] = fork(deviceModel.partitions, partitionModel.isNew); - const reusedPartitions = existingPartitions.filter(partitionModel.isReused); - const keepEntry = - configModel.isExplicitBootDevice(config, deviceModel.name) || reusedPartitions.length; - - if (keepEntry) { - device.partitions = existingPartitions; - } else { - config[oldList].splice(index, 1); - } - - if (target) { - target.partitions ||= []; - target.partitions = [...target.partitions, ...newPartitions]; - } else { - config[list].push({ - name, - partitions: newPartitions, - spacePolicy: device.spacePolicy === "custom" ? undefined : device.spacePolicy, - }); - } - - return config; -} - -export { deleteIfUnused, isUsed, switchSearched }; diff --git a/web/src/storage/space-policy.ts b/web/src/storage/space-policy.ts deleted file mode 100644 index b51ababf13..0000000000 --- a/web/src/storage/space-policy.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 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 General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { copyApiModel, findDevice } from "~/storage/api-model"; -import type { ConfigModel } from "~/model/storage/config-model"; -import type { Data } from "~/storage"; - -function setActions(device: ConfigModel.Drive, actions: Data.SpacePolicyAction[]) { - device.partitions ||= []; - - // Reset resize/delete actions of all current partition configs. - device.partitions - .filter((p) => p.name !== undefined) - .forEach((partition) => { - partition.delete = false; - partition.deleteIfNeeded = false; - partition.resizeIfNeeded = false; - partition.size = undefined; - }); - - // Apply the given actions. - actions.forEach(({ deviceName, value }) => { - const isDelete = value === "delete"; - const isResizeIfNeeded = value === "resizeIfNeeded"; - const partition = device.partitions.find((p) => p.name === deviceName); - - if (partition) { - partition.delete = isDelete; - partition.resizeIfNeeded = isResizeIfNeeded; - } else { - device.partitions.push({ - name: deviceName, - delete: isDelete, - resizeIfNeeded: isResizeIfNeeded, - }); - } - }); -} - -function setSpacePolicy( - model: ConfigModel.Config, - collection: string, - index: number | string, - data: Data.SpacePolicy, -): ConfigModel.Config { - model = copyApiModel(model); - const apiDevice = findDevice(model, collection, index); - - if (apiDevice === undefined) return model; - - apiDevice.spacePolicy = data.type; - if (data.type === "custom") setActions(apiDevice, data.actions || []); - - return model; -} - -export { setSpacePolicy };