diff --git a/service/lib/agama/storage/config_checkers/alias.rb b/service/lib/agama/storage/config_checkers/alias.rb index ae78b09ce5..a6d9b9ce18 100644 --- a/service/lib/agama/storage/config_checkers/alias.rb +++ b/service/lib/agama/storage/config_checkers/alias.rb @@ -88,7 +88,7 @@ def formatted_issue target_users = storage_config.target_users(config.alias) any_user = (users + target_users).any? - return unless users.any? || target_users.any? + return unless any_user error( format( diff --git a/web/src/components/storage/ConfigEditor.tsx b/web/src/components/storage/ConfigEditor.tsx index db29a48ef0..bf028e8aaf 100644 --- a/web/src/components/storage/ConfigEditor.tsx +++ b/web/src/components/storage/ConfigEditor.tsx @@ -26,7 +26,6 @@ import Text from "~/components/core/Text"; import DriveEditor from "~/components/storage/DriveEditor"; import VolumeGroupEditor from "~/components/storage/VolumeGroupEditor"; import MdRaidEditor from "~/components/storage/MdRaidEditor"; -import { useDevices } from "~/hooks/api/system/storage"; import { useReset } from "~/hooks/api/config/storage"; import ConfigureDeviceMenu from "./ConfigureDeviceMenu"; import { useModel } from "~/hooks/storage/model"; @@ -57,24 +56,8 @@ const NoDevicesConfiguredAlert = () => { ); }; -/** - * @fixme Adapt components (DriveEditor, MdRaidEditor, etc) to receive a list name and an index - * instead of a device object. Each component will retrieve the device from the model if needed. - * - * That will allow to: - * * Simplify the model types (list and listIndex properties are not needed). - * * All the components (DriveEditor, PartitionPage, etc) work in a similar way. They receive a - * list and an index and each component retrieves the device from the model if needed. - * * The components always have all the needed info for generating an url. - * * The partitions and logical volumes can also be referenced by an index, so it opens the door - * to have partitions and lvs without a mount path. - * - * These changes will be done once creating partitions without a mount path is needed (e.g., for - * manually creating physical volumes). - */ export default function ConfigEditor() { const model = useModel(); - const devices = useDevices(); const drives = model.drives; const mdRaids = model.mdRaids; const volumeGroups = model.volumeGroups; @@ -89,22 +72,12 @@ export default function ConfigEditor() { {volumeGroups.map((vg, i) => { return ; })} - {mdRaids.map((raid, i) => { - const device = devices.find((d) => d.name === raid.name); - - return ; - })} - {drives.map((drive, i) => { - const device = devices.find((d) => d.name === drive.name); - - /** - * @fixme Make DriveEditor to work when the device is not found (e.g., after disabling - * a iSCSI device). - */ - if (device === undefined) return null; - - return ; - })} + {mdRaids.map((_, i) => ( + + ))} + {drives.map((_, i) => ( + + ))} diff --git a/web/src/components/storage/DeviceEditorContent.tsx b/web/src/components/storage/DeviceEditorContent.tsx index 3da4fe0d73..978670e656 100644 --- a/web/src/components/storage/DeviceEditorContent.tsx +++ b/web/src/components/storage/DeviceEditorContent.tsx @@ -25,22 +25,25 @@ 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 type { model } from "~/storage"; -import type { storage } from "~/api/system"; +import { useDevice } from "~/hooks/storage/model"; -type DeviceEditorContentProps = { deviceModel: model.Drive | model.MdRaid; device: storage.Device }; +type DeviceEditorContentProps = { + collection: "drives" | "mdRaids"; + index: number; +}; export default function DeviceEditorContent({ - deviceModel, - device, + collection, + index, }: DeviceEditorContentProps): React.ReactNode { - if (!deviceModel.isUsed) return ; + const deviceModel = useDevice(collection, index); + if (!deviceModel.isUsed) return ; return ( <> - {deviceModel.filesystem && } - {!deviceModel.filesystem && } - {!deviceModel.filesystem && } + {deviceModel.filesystem && } + {!deviceModel.filesystem && } + {!deviceModel.filesystem && } ); } diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index 73d7bb2d20..594ba8ad69 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -30,17 +30,14 @@ 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 { useDevice } from "~/hooks/api/system/storage"; import type { model } from "~/storage"; -import type { storage } from "~/api/system"; - -type DriveDeviceMenuProps = { - drive: model.Drive; - selected: storage.Device; -}; +import type { storage as system } from "~/api/system"; type DriveDeviceMenuToggleProps = CustomToggleProps & { drive: model.Drive | model.MdRaid; - device: storage.Device; + device: system.Device; }; const DriveDeviceMenuToggle = forwardRef( @@ -71,6 +68,11 @@ const DriveDeviceMenuToggle = forwardRef( }, ); +type DriveDeviceMenuProps = { + drive: model.Drive; + selected: system.Device; +}; + /** * Internal component that renders generic actions available for a Drive device. */ @@ -88,16 +90,25 @@ const DriveDeviceMenu = ({ drive, selected }: DriveDeviceMenuProps) => { ); }; -export type DriveEditorProps = { drive: model.Drive; driveDevice: storage.Device }; +export type DriveEditorProps = { index: number }; /** * Component responsible for displaying detailed information and available actions * related to a specific Drive device within the storage ConfigEditor. */ -export default function DriveEditor({ drive, driveDevice }: DriveEditorProps) { +export default function DriveEditor({ index }: DriveEditorProps) { + const driveModel = useDrive(index); + const drive = useDevice(driveModel.name); + + /** + * @fixme Make DriveEditor to work when the device is not found (e.g., after disabling + * a iSCSI device). + */ + if (drive === undefined) return null; + return ( - }> - + }> + ); } diff --git a/web/src/components/storage/FilesystemMenu.tsx b/web/src/components/storage/FilesystemMenu.tsx index 29fa8d8f2f..1cd1d7da31 100644 --- a/web/src/components/storage/FilesystemMenu.tsx +++ b/web/src/components/storage/FilesystemMenu.tsx @@ -28,12 +28,11 @@ import MenuButton, { CustomToggleProps } from "~/components/core/MenuButton"; import { STORAGE as PATHS } from "~/routes/paths"; import { model } from "~/storage"; import { filesystemType, formattedPath } from "~/components/storage/utils"; +import { useDevice } from "~/hooks/storage/model"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -type FilesystemMenuProps = { deviceModel: model.Drive | model.MdRaid }; - -function deviceDescription(deviceModel: FilesystemMenuProps["deviceModel"]): string { +function deviceDescription(deviceModel: model.Drive | model.MdRaid): string { const fs = filesystemType(deviceModel.filesystem); const mountPath = deviceModel.mountPath; const reuse = deviceModel.filesystem.reuse; @@ -82,10 +81,18 @@ const FilesystemMenuToggle = forwardRef( }, ); -export default function FilesystemMenu({ deviceModel }: FilesystemMenuProps): React.ReactNode { +type FilesystemMenuProps = { + collection: "drives" | "mdRaids"; + index: number; +}; + +export default function FilesystemMenu({ + collection, + index, +}: FilesystemMenuProps): React.ReactNode { const navigate = useNavigate(); - const { list, listIndex } = deviceModel; - const editFilesystemPath = generatePath(PATHS.formatDevice, { list, listIndex }); + const deviceModel = useDevice(collection, index); + const editFilesystemPath = generatePath(PATHS.formatDevice, { collection, index }); return ( d.name === deviceModel.name); +function useDeviceFromParams(): system.Device { + const deviceModel = useDeviceModelFromParams(); + return useDevice(deviceModel.name); } function useCurrentFilesystem(): string | null { - const device = useDevice(); + const device = useDeviceFromParams(); return device?.filesystem?.type || null; } @@ -161,14 +165,14 @@ function useDefaultFilesystem(mountPoint: string): string { } function useInitialFormValue(): FormValue | null { - const deviceModel = useDeviceModel(); + const deviceModel = useDeviceModelFromParams(); return React.useMemo(() => (deviceModel ? toFormValue(deviceModel) : null), [deviceModel]); } /** Unused predefined mount points. Includes the currently used mount point when editing. */ function useUnusedMountPoints(): string[] { const unusedMountPaths = useMissingMountPaths(); - const deviceModel = useDeviceModel(); + const deviceModel = useDeviceModelFromParams(); return compact([deviceModel?.mountPath, ...unusedMountPaths]); } @@ -204,7 +208,7 @@ function useUsableFilesystems(mountPoint: string): string[] { function useMountPointError(value: FormValue): Error | undefined { const model = useModel(); const mountPoints = model?.getMountPaths() || []; - const deviceModel = useDeviceModel(); + const deviceModel = useDeviceModelFromParams(); const mountPoint = value.mountPoint; if (mountPoint === NO_VALUE) { @@ -291,7 +295,7 @@ type FilesystemOptionsProps = { }; function FilesystemOptions({ mountPoint }: FilesystemOptionsProps): React.ReactNode { - const device = useDevice(); + const device = useDeviceFromParams(); const volume = useVolumeTemplate(mountPoint); const defaultFilesystem = useDefaultFilesystem(mountPoint); const usableFilesystems = useUsableFilesystems(mountPoint); @@ -402,7 +406,8 @@ export default function FormattableDevicePage() { const value = { mountPoint, filesystem, filesystemLabel }; const { errors, getVisibleError } = useErrors(value); - const device = useDeviceModel(); + const { collection, index } = useParams(); + const device = useDeviceModelFromParams(); const unusedMountPoints = useUnusedMountPoints(); const addFilesystem = useAddFilesystem(); @@ -436,8 +441,7 @@ export default function FormattableDevicePage() { const onSubmit = () => { const data = toData(value); - const { list, listIndex } = device; - addFilesystem(list, listIndex, data); + addFilesystem(collection, Number(index), data); navigate(PATHS.root); }; diff --git a/web/src/components/storage/MdRaidEditor.tsx b/web/src/components/storage/MdRaidEditor.tsx index 51d33b27be..e6e0c20416 100644 --- a/web/src/components/storage/MdRaidEditor.tsx +++ b/web/src/components/storage/MdRaidEditor.tsx @@ -30,6 +30,8 @@ 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 { useDevice } from "~/hooks/api/system/storage"; import type { model } from "~/storage"; import type { storage } from "~/api/system"; @@ -88,16 +90,18 @@ const MdRaidDeviceMenu = ({ raid, selected }: MdRaidDeviceMenuProps): React.Reac ); }; -type MdRaidEditorProps = { raid: model.MdRaid; raidDevice: storage.Device }; +type MdRaidEditorProps = { index: number }; /** * Component responsible for displaying detailed information and available * actions related to a specific MdRaid device within the storage ConfigEditor. */ -export default function MdRaidEditor({ raid, raidDevice }: MdRaidEditorProps) { +export default function MdRaidEditor({ index }: MdRaidEditorProps) { + const raidModel = useMdRaid(index); + const raid = useDevice(raidModel.name); return ( - }> - + }> + ); } diff --git a/web/src/components/storage/PartitionPage.tsx b/web/src/components/storage/PartitionPage.tsx index 53e490d3e1..5f27f4ef69 100644 --- a/web/src/components/storage/PartitionPage.tsx +++ b/web/src/components/storage/PartitionPage.tsx @@ -51,12 +51,18 @@ import SizeModeSelect, { SizeMode, SizeRange } from "~/components/storage/SizeMo import AlertOutOfSync from "~/components/core/AlertOutOfSync"; import ResourceNotFound from "~/components/core/ResourceNotFound"; import { useAddPartition, useEditPartition } from "~/hooks/storage/partition"; -import { useModel, useMissingMountPaths } from "~/hooks/storage/model"; +import { + useModel, + useMissingMountPaths, + useDrive as useDriveModel, + useMdRaid as useMdRaidModel, +} from "~/hooks/storage/model"; import { addPartition as addPartitionHelper, editPartition as editPartitionHelper, } from "~/storage/partition"; -import { useDevices, useVolumeTemplate } from "~/hooks/api/system/storage"; +import { useVolumeTemplate, useDevice } from "~/hooks/api/system/storage"; + import { useSolvedConfigModel } from "~/queries/storage/config-model"; import { useStorageModel } from "~/hooks/api/storage"; import { findDevice } from "~/storage/api-model"; @@ -189,20 +195,19 @@ function toFormValue(partitionConfig: model.Partition): FormValue { }; } -function useModelDevice() { - const { list, listIndex } = useParams(); - const model = useModel(); - return model[list].at(listIndex); +function useDeviceModelFromParams() { + const { collection, index } = useParams(); + const deviceModel = collection === "drives" ? useDriveModel : useMdRaidModel; + return deviceModel(Number(index)); } -function useDevice(): system.Device { - const modelDevice = useModelDevice(); - const devices = useDevices(); - return devices.find((d) => d.name === modelDevice.name); +function useDeviceFromParams(): system.Device { + const deviceModel = useDeviceModelFromParams(); + return useDevice(deviceModel.name); } function usePartition(target: string): system.Device | null { - const device = useDevice(); + const device = useDeviceFromParams(); if (target === NEW_PARTITION) return null; @@ -223,7 +228,7 @@ function useDefaultFilesystem(mountPoint: string): string { function useInitialPartitionConfig(): model.Partition | null { const { partitionId: mountPath } = useParams(); - const device = useModelDevice(); + const device = useDeviceModelFromParams(); return mountPath && device ? device.getPartition(mountPath) : null; } @@ -248,10 +253,10 @@ function useUnusedMountPoints(): string[] { /** Unused partitions. Includes the currently used partition when editing (if any). */ function useUnusedPartitions(): system.Device[] { - const device = useDevice(); + const device = useDeviceFromParams(); const allPartitions = device.partitions || []; const initialPartitionConfig = useInitialPartitionConfig(); - const configuredPartitionConfigs = useModelDevice() + const configuredPartitionConfigs = useDeviceModelFromParams() .getConfiguredExistingPartitions() .filter((p) => p.name !== initialPartitionConfig?.name) .map((p) => p.name); @@ -387,7 +392,8 @@ function useErrors(value: FormValue): ErrorsHandler { } function useSolvedModel(value: FormValue): model.Config | null { - const device = useModelDevice(); + const { collection, index } = useParams(); + const device = useDeviceModelFromParams(); const model = useStorageModel(); const { errors } = useErrors(value); const initialPartitionConfig = useInitialPartitionConfig(); @@ -395,19 +401,21 @@ function useSolvedModel(value: FormValue): model.Config | null { partitionConfig.size = undefined; if (partitionConfig.filesystem) partitionConfig.filesystem.label = undefined; + const modelCollection = collection === "drives" ? "drives" : "mdRaids"; + let sparseModel: model.Config | undefined; if (device && !errors.length && value.target === NEW_PARTITION && value.filesystem !== NO_VALUE) { if (initialPartitionConfig) { sparseModel = editPartitionHelper( model, - device.list, - device.listIndex, + modelCollection, + index, initialPartitionConfig.mountPath, partitionConfig, ); } else { - sparseModel = addPartitionHelper(model, device.list, device.listIndex, partitionConfig); + sparseModel = addPartitionHelper(model, modelCollection, index, partitionConfig); } } @@ -417,10 +425,10 @@ function useSolvedModel(value: FormValue): model.Config | null { function useSolvedPartitionConfig(value: FormValue): model.Partition | undefined { const model = useSolvedModel(value); - const { list, listIndex } = useModelDevice(); + const { collection, index } = useParams(); if (!model) return; - const container = findDevice(model, list, listIndex); + const container = findDevice(model, collection, index); return container?.partitions?.find((p) => p.mountPath === value.mountPoint); } @@ -490,7 +498,7 @@ type TargetOptionLabelProps = { }; function TargetOptionLabel({ value }: TargetOptionLabelProps): React.ReactNode { - const device = useDevice(); + const device = useDeviceFromParams(); const partition = usePartition(value); if (value === NEW_PARTITION) { @@ -691,6 +699,7 @@ function AutoSizeInfo({ value }: AutoSizeInfoProps): React.ReactNode { * deprecated hooks from ~/queries/storage/config-model. */ const PartitionPageForm = () => { + const { collection, index } = useParams(); const navigate = useNavigate(); const location = useLocation(); const headingId = useId(); @@ -710,7 +719,7 @@ const PartitionPageForm = () => { const value = { mountPoint, target, filesystem, filesystemLabel, sizeOption, minSize, maxSize }; const { errors, getVisibleError } = useErrors(value); - const device = useModelDevice(); + const device = useDeviceModelFromParams(); const unusedMountPoints = useUnusedMountPoints(); @@ -792,10 +801,11 @@ const PartitionPageForm = () => { const onSubmit = () => { const partitionConfig = toPartitionConfig(value); - const { list, listIndex } = device; + const modelCollection = collection === "drives" ? "drives" : "mdRaids"; - if (initialValue) editPartition(list, listIndex, initialValue.mountPoint, partitionConfig); - else addPartition(list, listIndex, partitionConfig); + if (initialValue) + editPartition(modelCollection, index, initialValue.mountPoint, partitionConfig); + else addPartition(modelCollection, index, partitionConfig); navigate({ pathname: PATHS.root, search: location.search }); }; @@ -916,7 +926,7 @@ const PartitionPageForm = () => { }; export default function PartitionPage() { - const device = useModelDevice(); + const device = useDeviceModelFromParams(); return isUndefined(device) ? ( diff --git a/web/src/components/storage/PartitionsSection.tsx b/web/src/components/storage/PartitionsSection.tsx index 9f6ff16a80..0214ca5745 100644 --- a/web/src/components/storage/PartitionsSection.tsx +++ b/web/src/components/storage/PartitionsSection.tsx @@ -40,6 +40,7 @@ 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 * as driveUtils from "~/components/storage/utils/drive"; import { generateEncodedPath } from "~/utils"; import * as partitionUtils from "~/components/storage/utils/partition"; @@ -52,12 +53,18 @@ import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacin import { toggle } from "radashi"; import type { model } from "~/storage"; -const PartitionMenuItem = ({ device, mountPath }) => { +type PartitionMenuItemProps = { + device: model.Drive | model.MdRaid; + mountPath: string; + collection: "drives" | "mdRaids"; + index: number; +}; + +const PartitionMenuItem = ({ device, mountPath, collection, index }: PartitionMenuItemProps) => { const partition = device.getPartition(mountPath); - const { list, listIndex } = device; const editPath = generateEncodedPath(PATHS.editPartition, { - list, - listIndex, + collection, + index, partitionId: mountPath, }); const deletePartition = useDeletePartition(); @@ -66,7 +73,7 @@ const PartitionMenuItem = ({ device, mountPath }) => { deletePartition(list, listIndex, mountPath)} + deleteFn={() => deletePartition(collection, index, mountPath)} /> ); }; @@ -120,12 +127,16 @@ const optionalPartitionsTexts = (device) => { return texts; }; -const PartitionRow = ({ partition, device }) => { - // const partition = device.getPartition(mountPath); - const { list, listIndex } = device; +type PartitionRowProps = { + partition: model.Partition; + collection: "drives" | "mdRaids"; + index: number; +}; + +const PartitionRow = ({ partition, collection, index }: PartitionRowProps) => { const editPath = generateEncodedPath(PATHS.editPartition, { - list, - listIndex, + collection, + index, partitionId: partition.mountPath, }); const deletePartition = useDeletePartition(); @@ -174,7 +185,7 @@ const PartitionRow = ({ partition, device }) => { deletePartition(list, listIndex, partition.mountPath)} + onClick={() => deletePartition(collection, index, partition.mountPath)} isDanger > {_("Delete")} @@ -202,23 +213,24 @@ const PartitionsSectionHeader = ({ device }) => { }; type PartitionsSectionProps = { - device: model.Drive | model.MdRaid; + collection: "drives" | "mdRaids"; + index: number; }; -export default function PartitionsSection({ device }: PartitionsSectionProps) { +export default function PartitionsSection({ collection, index }: PartitionsSectionProps) { const { uiState, setUiState } = useStorageUiState(); const toggleId = useId(); const contentId = useId(); - const { list, listIndex } = device; - const index = `${list[0]}${listIndex}`; + const device = useDevice(collection, index); + const uiIndex = `${collection[0]}${index}`; const expanded = uiState.get("expanded")?.split(","); - const isExpanded = expanded?.includes(index); - const newPartitionPath = generateEncodedPath(PATHS.addPartition, { list, listIndex }); + const isExpanded = expanded?.includes(uiIndex); + const newPartitionPath = generateEncodedPath(PATHS.addPartition, { collection, index }); const hasPartitions = device.partitions.some((p: model.Partition) => p.mountPath); const onToggle = () => { setUiState((state) => { - const nextExpanded = toggle(expanded, index); + const nextExpanded = toggle(expanded, uiIndex); state.set("expanded", nextExpanded.join(",")); return state; }); @@ -241,7 +253,15 @@ export default function PartitionsSection({ device }: PartitionsSectionProps) { device.partitions .filter((p: model.Partition) => p.mountPath) .map((p: model.Partition) => { - return ; + return ( + + ); }), ); } @@ -271,7 +291,14 @@ export default function PartitionsSection({ device }: PartitionsSectionProps) { {device.partitions .filter((p: model.Partition) => p.mountPath) .map((p: model.Partition) => { - return ; + return ( + + ); })} diff --git a/web/src/components/storage/ProposalResultSection.tsx b/web/src/components/storage/ProposalResultSection.tsx index 95e1b68801..4bdd0e7167 100644 --- a/web/src/components/storage/ProposalResultSection.tsx +++ b/web/src/components/storage/ProposalResultSection.tsx @@ -28,8 +28,11 @@ import DevicesManager from "~/storage/devices-manager"; import ProposalResultTable from "~/components/storage/ProposalResultTable"; import { ProposalActionsDialog } from "~/components/storage"; import { _, n_, formatList } from "~/i18n"; -import { useDevices as useSystemDevices } from "~/hooks/api/system/storage"; -import { useDevices as useProposalDevices, useActions } from "~/hooks/api/proposal/storage"; +import { useFlattenDevices as useSystemFlattenDevices } from "~/hooks/api/system/storage"; +import { + useFlattenDevices as useProposalFlattenDevices, + useActions, +} from "~/hooks/api/proposal/storage"; import { sprintf } from "sprintf-js"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { useStorageUiState } from "~/context/storage-ui-state"; @@ -108,8 +111,8 @@ export type ProposalResultSectionProps = { export default function ProposalResultSection({ isLoading = false }: ProposalResultSectionProps) { const { uiState, setUiState } = useStorageUiState(); - const system = useSystemDevices(); - const staging = useProposalDevices(); + const system = useSystemFlattenDevices(); + const staging = useProposalFlattenDevices(); const actions = useActions(); const devicesManager = new DevicesManager(system, staging, actions); const handleTabClick = ( diff --git a/web/src/components/storage/SpacePolicyMenu.tsx b/web/src/components/storage/SpacePolicyMenu.tsx index 332422cd68..6647bab7c7 100644 --- a/web/src/components/storage/SpacePolicyMenu.tsx +++ b/web/src/components/storage/SpacePolicyMenu.tsx @@ -32,7 +32,8 @@ import { STORAGE as PATHS } from "~/routes/paths"; import * as driveUtils from "~/components/storage/utils/drive"; import { generateEncodedPath } from "~/utils"; import { isEmpty } from "radashi"; -import type { storage as system } from "~/api/system"; +import { useDevice as useDeviceModel } from "~/hooks/storage/model"; +import { useDevice } from "~/hooks/api/system/storage"; import type { model as apiModel } from "~/api/storage"; import type { model } from "~/storage"; @@ -72,27 +73,28 @@ const SpacePolicyMenuToggle = forwardRef(({ drive, ...props }: SpacePolicyMenuTo }); type SpacePolicyMenuProps = { - modelDevice: model.Drive | model.MdRaid; - device: system.Device; + collection: "drives" | "mdRaids"; + index: number; }; -export default function SpacePolicyMenu({ modelDevice, device }: SpacePolicyMenuProps) { +export default function SpacePolicyMenu({ collection, index }: SpacePolicyMenuProps) { const navigate = useNavigate(); const setSpacePolicy = useSetSpacePolicy(); - const { list, listIndex } = modelDevice; + const deviceModel = useDeviceModel(collection, index); + const device = useDevice(deviceModel.name); const existingPartitions = device.partitions?.length; if (isEmpty(existingPartitions)) return; const onSpacePolicyChange = (spacePolicy: apiModel.SpacePolicy) => { if (spacePolicy === "custom") { - return navigate(generateEncodedPath(PATHS.editSpacePolicy, { list, listIndex })); + return navigate(generateEncodedPath(PATHS.editSpacePolicy, { collection, index })); } else { - setSpacePolicy(list, listIndex, { type: spacePolicy }); + setSpacePolicy(collection, index, { type: spacePolicy }); } }; - const currentPolicy = driveUtils.spacePolicyEntry(modelDevice); + const currentPolicy = driveUtils.spacePolicyEntry(deviceModel); return ( ))} - customToggle={} + customToggle={} /> ); } diff --git a/web/src/components/storage/SpacePolicySelection.tsx b/web/src/components/storage/SpacePolicySelection.tsx index 341e639c16..ddcd5ea5ff 100644 --- a/web/src/components/storage/SpacePolicySelection.tsx +++ b/web/src/components/storage/SpacePolicySelection.tsx @@ -28,7 +28,7 @@ import SpaceActionsTable, { SpacePolicyAction } from "~/components/storage/Space import { deviceChildren } from "~/components/storage/utils"; import { _ } from "~/i18n"; import { useDevices } from "~/hooks/api/system/storage"; -import { useModel } from "~/hooks/storage/model"; +import { useDrive as useDriveModel, useMdRaid as useMdRaidModel } from "~/hooks/storage/model"; import { useSetSpacePolicy } from "~/hooks/storage/space-policy"; import { toDevice } from "./device-utils"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; @@ -43,17 +43,22 @@ const partitionAction = (partition: model.Partition) => { return undefined; }; +function useDeviceModelFromParams(): model.Drive | model.MdRaid | null { + const { collection, index } = useParams(); + const deviceModel = collection === "drives" ? useDriveModel : useMdRaidModel; + return deviceModel(Number(index)); +} + /** * Renders a page that allows the user to select the space policy and actions. */ export default function SpacePolicySelection() { - const { list, listIndex } = useParams(); - const model = useModel(); - const deviceModel = model[list][listIndex]; + const deviceModel = useDeviceModelFromParams(); const devices = useDevices(); const device = devices.find((d) => d.name === deviceModel.name); const children = deviceChildren(device); const setSpacePolicy = useSetSpacePolicy(); + const { collection, index } = useParams(); const partitionDeviceAction = (device: Device) => { const partition = deviceModel.partitions?.find((p) => p.name === device.name); @@ -89,7 +94,7 @@ export default function SpacePolicySelection() { const onSubmit = (e) => { e.preventDefault(); - setSpacePolicy(list, listIndex, { type: "custom", actions }); + setSpacePolicy(collection, index, { type: "custom", actions }); navigate(".."); }; diff --git a/web/src/components/storage/UnusedMenu.tsx b/web/src/components/storage/UnusedMenu.tsx index b6896a7d54..8b70a33683 100644 --- a/web/src/components/storage/UnusedMenu.tsx +++ b/web/src/components/storage/UnusedMenu.tsx @@ -26,12 +26,9 @@ import Icon from "~/components/layout/Icon"; import { useNavigate } from "react-router"; import MenuButton, { CustomToggleProps } from "~/components/core/MenuButton"; import { STORAGE as PATHS } from "~/routes/paths"; -import { model } from "~/storage"; import { generateEncodedPath } from "~/utils"; import { _ } from "~/i18n"; -type UnusedMenuProps = { deviceModel: model.Drive | model.MdRaid }; - const UnusedMenuToggle = forwardRef(({ ...props }: CustomToggleProps, ref) => { const description = _("Not configured yet"); @@ -52,13 +49,19 @@ const UnusedMenuToggle = forwardRef(({ ...props }: CustomToggleProps, ref) => { ); }); -export default function UnusedMenu({ deviceModel }: UnusedMenuProps): React.ReactNode { +type UnusedMenuProps = { + collection: "drives" | "mdRaids"; + index: number; +}; + +export default function UnusedMenu({ collection, index }: UnusedMenuProps): React.ReactNode { const navigate = useNavigate(); - const { list, listIndex } = deviceModel; - const newPartitionPath = generateEncodedPath(PATHS.addPartition, { list, listIndex }); - const formatDevicePath = generateEncodedPath(PATHS.formatDevice, { list, listIndex }); + const newPartitionPath = generateEncodedPath(PATHS.addPartition, { collection, index }); + const formatDevicePath = generateEncodedPath(PATHS.formatDevice, { collection, index }); const filesystemLabel = - list === "drives" ? _("Use the disk without partitions") : _("Use the RAID without partitions"); + collection === "drives" + ? _("Use the disk without partitions") + : _("Use the RAID without partitions"); return ( data?.storage; @@ -44,6 +45,17 @@ function useDevices(): storage.Device[] { return data; } +const selectFlattenDevices = (data: Proposal | null): storage.Device[] => + data?.storage ? flatDevices(data.storage) : []; + +function useFlattenDevices(): storage.Device[] { + const { data } = useSuspenseQuery({ + ...proposalQuery, + select: selectFlattenDevices, + }); + return data; +} + const selectActions = (data: Proposal | null): storage.Action[] => data?.storage?.actions || []; function useActions(): storage.Action[] { @@ -54,4 +66,4 @@ function useActions(): storage.Action[] { return data; } -export { useProposal, useDevices, useActions }; +export { useProposal, useDevices, useFlattenDevices, useActions }; diff --git a/web/src/hooks/api/system/storage.ts b/web/src/hooks/api/system/storage.ts index bc51084894..0451d8f834 100644 --- a/web/src/hooks/api/system/storage.ts +++ b/web/src/hooks/api/system/storage.ts @@ -23,7 +23,7 @@ import { useCallback } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; import { systemQuery } from "~/hooks/api/system"; -import { findDevices } from "~/storage/system"; +import { flatDevices, findDevices, findDeviceByName } from "~/storage/system"; import type { System, storage } from "~/api/system"; import type { EncryptionMethod } from "~/api/system/storage"; @@ -155,6 +155,30 @@ function useDevices(): storage.Device[] { return data; } +const selectFlattenDevices = (data: System | null): storage.Device[] => + data?.storage ? flatDevices(data.storage) : []; + +function useFlattenDevices(): storage.Device[] { + const { data } = useSuspenseQuery({ + ...systemQuery, + select: selectFlattenDevices, + }); + return data; +} + +function useDevice(name: string): storage.Device | null { + const { data } = useSuspenseQuery({ + ...systemQuery, + select: useCallback( + (data: System | null): storage.Device | null => { + return data?.storage ? findDeviceByName(data.storage, name) : null; + }, + [name], + ), + }); + return data; +} + const selectVolumeTemplates = (data: System | null): storage.Volume[] => data?.storage?.volumeTemplates || []; @@ -199,6 +223,8 @@ export { useAvailableDevices, useCandidateDevices, useDevices, + useFlattenDevices, + useDevice, useVolumeTemplates, useVolumeTemplate, useIssues, diff --git a/web/src/hooks/storage/model.ts b/web/src/hooks/storage/model.ts index 37564e2089..7cf435b4f2 100644 --- a/web/src/hooks/storage/model.ts +++ b/web/src/hooks/storage/model.ts @@ -55,4 +55,41 @@ function useMissingMountPaths(): string[] { return data; } -export { useModel, useMissingMountPaths }; +function useDevice( + collection: "drives" | "mdRaids", + index: number, +): model.Drive | model.MdRaid | null { + const { data } = useSuspenseQuery({ + ...storageModelQuery, + select: useCallback( + (data: apiModel.Config): model.Drive | model.MdRaid | null => + build(data)?.[collection]?.at(index) || null, + [collection, index], + ), + }); + return data; +} + +function useDrive(index: number): model.Drive | null { + const { data } = useSuspenseQuery({ + ...storageModelQuery, + select: useCallback( + (data: apiModel.Config): model.Drive | null => build(data)?.drives?.at(index) || null, + [index], + ), + }); + return data; +} + +function useMdRaid(index: number): model.MdRaid | null { + const { data } = useSuspenseQuery({ + ...storageModelQuery, + select: useCallback( + (data: apiModel.Config): model.MdRaid | null => build(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 index 407914ab95..272ab0b7d7 100644 --- a/web/src/hooks/storage/partition.ts +++ b/web/src/hooks/storage/partition.ts @@ -26,21 +26,21 @@ import { data } from "~/storage"; import { addPartition, editPartition, deletePartition } from "~/storage/partition"; type AddPartitionFn = ( - list: "drives" | "mdRaids", - listIndex: number | string, + collection: "drives" | "mdRaids", + index: number | string, data: data.Partition, ) => void; function useAddPartition(): AddPartitionFn { const apiModel = useStorageModel(); - return (list: "drives" | "mdRaids", listIndex: number | string, data: data.Partition) => { - putStorageModel(addPartition(apiModel, list, listIndex, data)); + return (collection: "drives" | "mdRaids", index: number | string, data: data.Partition) => { + putStorageModel(addPartition(apiModel, collection, index, data)); }; } type EditPartitionFn = ( - list: "drives" | "mdRaids", - listIndex: number | string, + collection: "drives" | "mdRaids", + index: number | string, mountPath: string, data: data.Partition, ) => void; @@ -48,25 +48,25 @@ type EditPartitionFn = ( function useEditPartition(): EditPartitionFn { const apiModel = useStorageModel(); return ( - list: "drives" | "mdRaids", - listIndex: number | string, + collection: "drives" | "mdRaids", + index: number | string, mountPath: string, data: data.Partition, ) => { - putStorageModel(editPartition(apiModel, list, listIndex, mountPath, data)); + putStorageModel(editPartition(apiModel, collection, index, mountPath, data)); }; } type DeletePartitionFn = ( - list: "drives" | "mdRaids", - listIndex: number | string, + collection: "drives" | "mdRaids", + index: number | string, mountPath: string, ) => void; function useDeletePartition(): DeletePartitionFn { const apiModel = useStorageModel(); - return (list: "drives" | "mdRaids", listIndex: number | string, mountPath: string) => - putStorageModel(deletePartition(apiModel, list, listIndex, mountPath)); + return (collection: "drives" | "mdRaids", index: number | string, mountPath: string) => + putStorageModel(deletePartition(apiModel, collection, index, mountPath)); } export { useAddPartition, useEditPartition, useDeletePartition }; diff --git a/web/src/hooks/storage/space-policy.ts b/web/src/hooks/storage/space-policy.ts index aa132b013b..d30741512c 100644 --- a/web/src/hooks/storage/space-policy.ts +++ b/web/src/hooks/storage/space-policy.ts @@ -25,12 +25,16 @@ import { putStorageModel } from "~/api"; import { data } from "~/storage"; import { setSpacePolicy } from "~/storage/space-policy"; -type setSpacePolicyFn = (list: string, listIndex: number | string, data: data.SpacePolicy) => void; +type setSpacePolicyFn = ( + collection: string, + index: number | string, + data: data.SpacePolicy, +) => void; function useSetSpacePolicy(): setSpacePolicyFn { - const apiModel = useStorageModel(); - return (list: string, listIndex: number | string, data: data.SpacePolicy) => { - putStorageModel(setSpacePolicy(apiModel, list, listIndex, data)); + const model = useStorageModel(); + return (collection: string, index: number | string, data: data.SpacePolicy) => { + putStorageModel(setSpacePolicy(model, collection, index, data)); }; } diff --git a/web/src/routes/paths.ts b/web/src/routes/paths.ts index 8c2357f8b3..96f764f142 100644 --- a/web/src/routes/paths.ts +++ b/web/src/routes/paths.ts @@ -78,10 +78,10 @@ const STORAGE = { progress: "/storage/progress", editBootDevice: "/storage/boot-device/edit", editEncryption: "/storage/encryption/edit", - editSpacePolicy: "/storage/:list/:listIndex/space-policy/edit", - formatDevice: "/storage/:list/:listIndex/format", - addPartition: "/storage/:list/:listIndex/partitions/add", - editPartition: "/storage/:list/:listIndex/partitions/:partitionId/edit", + editSpacePolicy: "/storage/:collection/:index/space-policy/edit", + formatDevice: "/storage/:collection/:index/format", + addPartition: "/storage/:collection/:index/partitions/add", + editPartition: "/storage/:collection/:index/partitions/:partitionId/edit", selectDevice: "/storage/devices/select", volumeGroup: { add: "/storage/volume-groups/add", diff --git a/web/src/storage/devices-manager.ts b/web/src/storage/devices-manager.ts index 6f9f141e24..5c0fa42fb2 100644 --- a/web/src/storage/devices-manager.ts +++ b/web/src/storage/devices-manager.ts @@ -64,14 +64,14 @@ export default class DevicesManager { * Whether the given device exists in system. */ existInSystem(device: system.Device): boolean { - return this.system.find((d) => d.sid === device.sid) !== undefined; + return this.systemDevice(device.sid) !== undefined; } /** * Whether the given device exists in staging. */ existInStaging(device: proposal.Device): boolean { - return this.staging.find((d) => d.sid === device.sid) !== undefined; + return this.stagingDevice(device.sid) !== undefined; } /** diff --git a/web/src/storage/model.ts b/web/src/storage/model.ts index dfc168c7ab..3846f4a1b1 100644 --- a/web/src/storage/model.ts +++ b/web/src/storage/model.ts @@ -41,14 +41,7 @@ interface Boot extends Omit { getDevice: () => Drive | MdRaid | null; } -/** - * @fixme Remove list and listIndex from types once the components are adapted to receive a list - * and an index instead of a device object. See ConfigEditor component. - */ - interface Drive extends Omit { - list: string; - listIndex: number; isExplicitBoot: boolean; isUsed: boolean; isAddingPartitions: boolean; @@ -63,8 +56,6 @@ interface Drive extends Omit { } interface MdRaid extends Omit { - list: string; - listIndex: number; isExplicitBoot: boolean; isUsed: boolean; isAddingPartitions: boolean; @@ -86,8 +77,6 @@ interface Partition extends apiModel.Partition { } interface VolumeGroup extends Omit { - list: string; - listIndex: number; logicalVolumes: LogicalVolume[]; getTargetDevices: () => Drive[]; getMountPaths: () => string[]; @@ -213,34 +202,16 @@ function partitionableProperties( }; } -function buildDrive( - apiDrive: apiModel.Drive, - listIndex: number, - apiModel: apiModel.Config, - model: Model, -): Drive { - const list = "drives"; - +function buildDrive(apiDrive: apiModel.Drive, apiModel: apiModel.Config, model: Model): Drive { return { ...apiDrive, - list, - listIndex, ...partitionableProperties(apiDrive, apiModel, model), }; } -function buildMdRaid( - apiMdRaid: apiModel.MdRaid, - listIndex: number, - apiModel: apiModel.Config, - model: Model, -): MdRaid { - const list = "mdRaids"; - +function buildMdRaid(apiMdRaid: apiModel.MdRaid, apiModel: apiModel.Config, model: Model): MdRaid { return { ...apiMdRaid, - list, - listIndex, ...partitionableProperties(apiMdRaid, apiModel, model), }; } @@ -249,13 +220,7 @@ function buildLogicalVolume(logicalVolumeData: apiModel.LogicalVolume): LogicalV return { ...logicalVolumeData }; } -function buildVolumeGroup( - apiVolumeGroup: apiModel.VolumeGroup, - listIndex: number, - model: Model, -): VolumeGroup { - const list = "volumeGroups"; - +function buildVolumeGroup(apiVolumeGroup: apiModel.VolumeGroup, model: Model): VolumeGroup { const getMountPaths = (): string[] => { return (apiVolumeGroup.logicalVolumes || []).map((l) => l.mountPath).filter((p) => p); }; @@ -271,8 +236,6 @@ function buildVolumeGroup( return { ...apiVolumeGroup, logicalVolumes: buildLogicalVolumes(), - list, - listIndex, getMountPaths, getTargetDevices, }; @@ -294,15 +257,15 @@ function buildModel(apiModel: apiModel.Config): Model { }; const buildDrives = (): Drive[] => { - return (apiModel.drives || []).map((d, i) => buildDrive(d, i, apiModel, model)); + return (apiModel.drives || []).map((d) => buildDrive(d, apiModel, model)); }; const buildMdRaids = (): MdRaid[] => { - return (apiModel.mdRaids || []).map((r, i) => buildMdRaid(r, i, apiModel, model)); + return (apiModel.mdRaids || []).map((r) => buildMdRaid(r, apiModel, model)); }; const buildVolumeGroups = (): VolumeGroup[] => { - return (apiModel.volumeGroups || []).map((v, i) => buildVolumeGroup(v, i, model)); + return (apiModel.volumeGroups || []).map((v) => buildVolumeGroup(v, model)); }; const withMountPaths = (): (Drive | MdRaid | VolumeGroup)[] => { diff --git a/web/src/storage/partition.ts b/web/src/storage/partition.ts index bfe337cfcb..020fe240dc 100644 --- a/web/src/storage/partition.ts +++ b/web/src/storage/partition.ts @@ -43,65 +43,65 @@ function indexByPath(device: Partitionable, path: string): number { * */ function addPartition( model: model.Config, - list: "drives" | "mdRaids", - listIndex: number | string, + collection: "drives" | "mdRaids", + index: number | string, data: data.Partition, ): model.Config { model = copyApiModel(model); - const device = findDevice(model, list, listIndex); + 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, list, listIndex) && device.spacePolicy === "keep") device.spacePolicy = null; + if (!isUsed(model, collection, index) && device.spacePolicy === "keep") device.spacePolicy = null; const partition = buildPartition(data); - const index = indexByName(device, partition.name); + const partitionIndex = indexByName(device, partition.name); - if (index === -1) device.partitions.push(partition); - else device.partitions[index] = partition; + if (partitionIndex === -1) device.partitions.push(partition); + else device.partitions[partitionIndex] = partition; return model; } function editPartition( model: model.Config, - list: "drives" | "mdRaids", - listIndex: number | string, + collection: "drives" | "mdRaids", + index: number | string, mountPath: string, data: data.Partition, ): model.Config { model = copyApiModel(model); - const device = findDevice(model, list, listIndex); + const device = findDevice(model, collection, index); if (device === undefined) return model; - const index = indexByPath(device, mountPath); - if (index === -1) return model; + const partitionIndex = indexByPath(device, mountPath); + if (partitionIndex === -1) return model; - const oldPartition = device.partitions[index]; + const oldPartition = device.partitions[partitionIndex]; const newPartition = { ...oldPartition, ...buildPartition(data) }; - device.partitions.splice(index, 1, newPartition); + device.partitions.splice(partitionIndex, 1, newPartition); return model; } function deletePartition( model: model.Config, - list: "drives" | "mdRaids", - listIndex: number | string, + collection: "drives" | "mdRaids", + index: number | string, mountPath: string, ): model.Config { model = copyApiModel(model); - const device = findDevice(model, list, listIndex); + const device = findDevice(model, collection, index); if (device === undefined) return model; - const index = indexByPath(device, mountPath); - device.partitions.splice(index, 1); + const partitionIndex = indexByPath(device, mountPath); + device.partitions.splice(partitionIndex, 1); // Do not delete anything if the device is not really used - if (!isUsed(model, list, listIndex)) { + if (!isUsed(model, collection, index)) { device.spacePolicy = "keep"; } diff --git a/web/src/storage/proposal.ts b/web/src/storage/proposal.ts new file mode 100644 index 0000000000..d80ce09d80 --- /dev/null +++ b/web/src/storage/proposal.ts @@ -0,0 +1,32 @@ +/* + * 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 { Proposal, Device } from "~/api/proposal/storage"; + +function flatDevices(proposal: Proposal): Device[] { + const partitions = proposal.devices?.flatMap((d) => d.partitions); + const logicalVolumes = proposal.devices?.flatMap((d) => d.logicalVolumes); + return sift([proposal.devices, partitions, logicalVolumes].flat()); +} + +export { flatDevices }; diff --git a/web/src/storage/space-policy.ts b/web/src/storage/space-policy.ts index 2b3c61d74b..ab0af2b091 100644 --- a/web/src/storage/space-policy.ts +++ b/web/src/storage/space-policy.ts @@ -57,20 +57,20 @@ function setActions(device: model.Drive, actions: data.SpacePolicyAction[]) { } function setSpacePolicy( - apiModel: model.Config, - list: string, - listIndex: number | string, + model: model.Config, + collection: string, + index: number | string, data: data.SpacePolicy, ): model.Config { - apiModel = copyApiModel(apiModel); - const apiDevice = findDevice(apiModel, list, listIndex); + model = copyApiModel(model); + const apiDevice = findDevice(model, collection, index); - if (apiDevice === undefined) return apiModel; + if (apiDevice === undefined) return model; apiDevice.spacePolicy = data.type; if (data.type === "custom") setActions(apiDevice, data.actions || []); - return apiModel; + return model; } export { setSpacePolicy }; diff --git a/web/src/storage/system.ts b/web/src/storage/system.ts index 6bc8253ac8..1f3be1ac95 100644 --- a/web/src/storage/system.ts +++ b/web/src/storage/system.ts @@ -20,10 +20,17 @@ * find current contact information at www.suse.com. */ +import { sift } from "radashi"; import type { System, Device } from "~/api/system/storage"; +function flatDevices(system: System): Device[] { + const partitions = system.devices?.flatMap((d) => d.partitions); + const logicalVolumes = system.devices?.flatMap((d) => d.logicalVolumes); + return sift([system.devices, partitions, logicalVolumes].flat()); +} + function findDevice(system: System, sid: number): Device | undefined { - const device = system.devices.find((d) => d.sid === sid); + const device = flatDevices(system).find((d) => d.sid === sid); if (device === undefined) console.warn("Device not found:", sid); return device; @@ -33,4 +40,8 @@ function findDevices(system: System, sids: number[]): Device[] { return sids.map((sid) => findDevice(system, sid)).filter((d) => d); } -export { findDevice, findDevices }; +function findDeviceByName(system: System, name: string): Device | null { + return flatDevices(system).find((d) => d.name === name) || null; +} + +export { flatDevices, findDevice, findDevices, findDeviceByName };