Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Wed Jul 16 14:46:32 UTC 2025 - Ancor Gonzalez Sosa <[email protected]>

- Allow to use a whole disk or MD RAID without a partition table
(gh#agama-project/agama#2559).

-------------------------------------------------------------------
Mon Jul 14 16:02:55 UTC 2025 - José Iván López González <[email protected]>

Expand Down
8 changes: 8 additions & 0 deletions web/src/components/storage/BootSelection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ describe("BootSelection", () => {
isDefault: true,
getDevice: () => null,
},
drives: [],
mdRaids: [],
});
});

Expand Down Expand Up @@ -183,6 +185,8 @@ describe("BootSelection", () => {
isDefault: false,
getDevice: () => sda,
},
drives: [],
mdRaids: [],
});
});

Expand All @@ -203,6 +207,8 @@ describe("BootSelection", () => {
isDefault: false,
getDevice: () => null,
},
drives: [],
mdRaids: [],
});
});

Expand All @@ -224,6 +230,8 @@ describe("BootSelection", () => {
isDefault: false,
getDevice: () => sda,
},
drives: [],
mdRaids: [],
});
});

Expand Down
10 changes: 9 additions & 1 deletion web/src/components/storage/BootSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ import {
useDisableBootConfig,
} from "~/hooks/storage/boot";

const filteredCandidates = (candidates, model): StorageDevice[] => {
return candidates.filter((candidate) => {
const collection = candidate.isDrive ? model.drives : model.mdRaids;
const device = collection.find((d) => d.name === candidate.name);
return !device || !device.filesystem;
});
};

// FIXME: improve classNames
// FIXME: improve and rename to BootSelectionDialog

Expand All @@ -62,8 +70,8 @@ export default function BootSelectionDialog() {
const [state, setState] = useState<BootSelectionState>({ load: false });
const navigate = useNavigate();
const devices = useDevices("system");
const candidateDevices = useCandidateDevices();
const model = useModel({ suspense: true });
const candidateDevices = filteredCandidates(useCandidateDevices(), model);
const setBootDevice = useSetBootDevice();
const setDefaultBootDevice = useSetDefaultBootDevice();
const disableBootConfig = useDisableBootConfig();
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/storage/ConfigureDeviceMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const mockUseModel = jest.fn();

jest.mock("~/hooks/storage/system", () => ({
...jest.requireActual("~/hooks/storage/system"),
useCandidateDevices: () => [vda, vdb],
useAvailableDevices: () => [vda, vdb],
}));

jest.mock("~/hooks/storage/model", () => ({
Expand Down
73 changes: 49 additions & 24 deletions web/src/components/storage/ConfigureDeviceMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import MenuButton, { MenuButtonItem } from "~/components/core/MenuButton";
import { Divider, MenuItemProps } from "@patternfly/react-core";
import { useCandidateDevices } from "~/hooks/storage/system";
import { useAvailableDevices } from "~/hooks/storage/system";
import { useModel } from "~/hooks/storage/model";
import { useAddDrive } from "~/hooks/storage/drive";
import { useAddReusedMdRaid } from "~/hooks/storage/md-raid";
Expand All @@ -35,36 +35,59 @@ import { StorageDevice } from "~/types/storage";
import DeviceSelectorModal from "./DeviceSelectorModal";

type AddDeviceMenuItemProps = {
/** Whether some of the available devices is an MD RAID */
withRaids: boolean;
/** Available devices to be chosen */
devices: StorageDevice[];
/** The total amount of drives and RAIDs already configured */
usedCount: number;
} & MenuItemProps;

const AddDeviceTitle = ({ usedCount }) =>
usedCount
? _("Select another disk to define partitions")
: _("Select a disk to define partitions");
const AddDeviceTitle = ({ withRaids, usedCount }) => {
if (withRaids) {
if (usedCount === 0) return _("Select a device to define partitions or to mount");
return _("Select another device to define partitions or to mount");
}

const AddDeviceDescription = ({ usedCount, isDisabled = false }) => {
if (isDisabled) return _("Already using all available disks");
if (usedCount === 0) return _("Select a disk to define partitions or to mount");
return _("Select another disk to define partitions or to mount");
};

const AddDeviceDescription = ({ withRaids, usedCount, isDisabled = false }) => {
if (isDisabled) {
if (withRaids) return _("Already using all available devices");
return _("Already using all available disks");
}

return usedCount
? sprintf(
if (usedCount) {
if (withRaids)
return sprintf(
n_(
"Extend the installation beyond the currently selected disk",
"Extend the installation beyond the current %d disks",
"Extend the installation beyond the currently selected device",
"Extend the installation beyond the current %d devices",
usedCount,
),
usedCount,
)
: _("Start configuring a basic installation");
);

return sprintf(
n_(
"Extend the installation beyond the currently selected disk",
"Extend the installation beyond the current %d disks",
usedCount,
),
usedCount,
);
}

return _("Start configuring a basic installation");
};

/**
* Internal component holding the logic for rendering the disks drilldown menu
*/
const AddDeviceMenuItem = ({
withRaids,
usedCount,
devices,
onClick,
Expand All @@ -75,10 +98,16 @@ const AddDeviceMenuItem = ({
<MenuButtonItem
aria-label={_("Add device menu")}
isDisabled={isDisabled}
description={<AddDeviceDescription usedCount={usedCount} isDisabled={isDisabled} />}
description={
<AddDeviceDescription
withRaids={withRaids}
usedCount={usedCount}
isDisabled={isDisabled}
/>
}
onClick={onClick}
>
<AddDeviceTitle usedCount={usedCount} />
<AddDeviceTitle withRaids={withRaids} usedCount={usedCount} />
</MenuButtonItem>
</>
);
Expand All @@ -87,12 +116,6 @@ const AddDeviceMenuItem = ({
/**
* Menu that provides options for users to configure storage drives
*
* It uses a drilled-down menu approach for disks, making the available options less
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch :)

* overwhelming by presenting them in a more organized manner.
*
* TODO: Refactor and test the component after extracting a basic DrillDown menu to
* share the internal logic with other potential menus that could benefit from a similar
* approach.
*/
export default function ConfigureDeviceMenu(): React.ReactNode {
const [deviceSelectorOpen, setDeviceSelectorOpen] = useState(false);
Expand All @@ -104,11 +127,12 @@ export default function ConfigureDeviceMenu(): React.ReactNode {
const model = useModel({ suspense: true });
const addDrive = useAddDrive();
const addReusedMdRaid = useAddReusedMdRaid();
const allDevices = useCandidateDevices();
const allDevices = useAvailableDevices();

const usedDevicesNames = model.drives.concat(model.mdRaids).map((d) => d.name);
const usedDevicesCount = usedDevicesNames.length;
const devices = allDevices.filter((d) => !usedDevicesNames.includes(d.name));
const withRaids = !!allDevices.filter((d) => !d.isDrive).length;

const addDevice = (device: StorageDevice) => {
const hook = device.isDrive ? addDrive : addReusedMdRaid;
Expand All @@ -130,6 +154,7 @@ export default function ConfigureDeviceMenu(): React.ReactNode {
key="select-disk-option"
usedCount={usedDevicesCount}
devices={devices}
withRaids={withRaids}
onClick={openDeviceSelector}
/>,
<Divider key="divider-option" />,
Expand All @@ -147,8 +172,8 @@ export default function ConfigureDeviceMenu(): React.ReactNode {
{deviceSelectorOpen && (
<DeviceSelectorModal
devices={devices}
title={<AddDeviceTitle usedCount={usedDevicesCount} />}
description={<AddDeviceDescription usedCount={usedDevicesCount} />}
title={<AddDeviceTitle withRaids={withRaids} usedCount={usedDevicesCount} />}
description={<AddDeviceDescription withRaids={withRaids} usedCount={usedDevicesCount} />}
onCancel={closeDeviceSelector}
onConfirm={([device]) => {
addDevice(device);
Expand Down
45 changes: 45 additions & 0 deletions web/src/components/storage/DeviceEditorContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 React from "react";
import UnusedMenu from "~/components/storage/UnusedMenu";
import FilesystemMenu from "~/components/storage/FilesystemMenu";
import PartitionsMenu from "~/components/storage/PartitionsMenu";
import SpacePolicyMenu from "~/components/storage/SpacePolicyMenu";
import { model, StorageDevice } from "~/types/storage";

type DeviceEditorContentProps = { deviceModel: model.Drive | model.MdRaid; device: StorageDevice };

export default function DeviceEditorContent({
deviceModel,
device,
}: DeviceEditorContentProps): React.ReactNode {
if (!deviceModel.isUsed) return <UnusedMenu deviceModel={deviceModel} />;

return (
<>
{deviceModel.filesystem && <FilesystemMenu deviceModel={deviceModel} />}
{!deviceModel.filesystem && <PartitionsMenu device={deviceModel} />}
{!deviceModel.filesystem && <SpacePolicyMenu modelDevice={deviceModel} device={device} />}
</>
);
}
10 changes: 2 additions & 8 deletions web/src/components/storage/DriveEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
import React from "react";
import ConfigEditorItem from "~/components/storage/ConfigEditorItem";
import DriveHeader from "~/components/storage/DriveHeader";
import PartitionsMenu from "~/components/storage/PartitionsMenu";
import SpacePolicyMenu from "~/components/storage/SpacePolicyMenu";
import DeviceEditorContent from "~/components/storage/DeviceEditorContent";
import SearchedDeviceMenu from "~/components/storage/SearchedDeviceMenu";
import { Drive } from "~/types/storage/model";
import { model, StorageDevice } from "~/types/storage";
Expand Down Expand Up @@ -55,12 +54,7 @@ export default function DriveEditor({ drive, driveDevice }: DriveEditorProps) {
return (
<ConfigEditorItem
header={<DriveHeader drive={drive} device={driveDevice} />}
content={
<>
<PartitionsMenu device={drive} />
<SpacePolicyMenu modelDevice={drive} device={driveDevice} />
</>
}
content={<DeviceEditorContent deviceModel={drive} device={driveDevice} />}
actions={<DriveDeviceMenu drive={drive} selected={driveDevice} />}
/>
);
Expand Down
7 changes: 7 additions & 0 deletions web/src/components/storage/DriveHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ import { _ } from "~/i18n";
export type DriveHeaderProps = { drive: model.Drive; device: StorageDevice };

const text = (drive: model.Drive): string => {
if (drive.filesystem) {
// TRANSLATORS: %s will be replaced by a disk name and its size - "sda (20 GiB)"
if (drive.filesystem.reuse) return _("Mount disk %s");
// TRANSLATORS: %s will be replaced by a disk name and its size - "sda (20 GiB)"
return _("Format disk %s");
}

const { isBoot, isTargetDevice: hasPv } = drive;
const isRoot = !!drive.getPartition("/");
const hasFs = !!drive.getMountPaths().length;
Expand Down
Loading