Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5fc7c3a
Update openapi types
joseivanlopez Mar 27, 2026
1639ace
Rename conversion methods
joseivanlopez Mar 30, 2026
69e36ef
wip: allow reusing volume groups
joseivanlopez Mar 31, 2026
43e51ed
Allow configuring space policy for VGs
joseivanlopez Apr 7, 2026
5988bcc
Do not offer VGs as target for new Vgs
joseivanlopez Apr 8, 2026
5803c8e
Allow reusing LVs
joseivanlopez Apr 8, 2026
d527004
refactor(web): rewrite DeviceSelectorModal with tabbed device selection
dgdavid Apr 9, 2026
5e09372
fix(web): do not render Annotation when children is empty
dgdavid Apr 9, 2026
60adac4
refactor: web: adapt device menu callers to tabbed DeviceSelectorModal
dgdavid Apr 9, 2026
3877fc9
Recover menu option for creating new VG
joseivanlopez Apr 9, 2026
7d2a6bd
Do not offer already configured devices
joseivanlopez Apr 9, 2026
cffacb2
Fix sid of the physical volumes
joseivanlopez Apr 9, 2026
5f6ccf0
Show names of the MD members and PVs
joseivanlopez Apr 9, 2026
6ad31f0
refactor(web): derive initial tab from the effective initial device
dgdavid Apr 9, 2026
d3f70b8
feat(web): add current content column to MdRaidsTable
dgdavid Apr 9, 2026
5fa930b
fix(web): please linters
dgdavid Apr 9, 2026
362df04
Fix selector for reusing a VG
joseivanlopez Apr 10, 2026
b9e994d
Some small text changes
joseivanlopez Apr 10, 2026
c13ae33
fix(web): adapt tests to the volume group reuse changes
dgdavid Apr 10, 2026
43ab145
Fix current content of MD RAIDs
joseivanlopez Apr 10, 2026
a7e1e44
Simplify text
joseivanlopez Apr 10, 2026
3bd1dd2
Adapt text for adding LVs
joseivanlopez Apr 10, 2026
1c67184
Fix validation of LV name
joseivanlopez Apr 10, 2026
91c7303
Remove props
joseivanlopez Apr 10, 2026
b16391b
web: Adjust size of DeviceSelectorModal
ancorgs Apr 11, 2026
f8a3bd1
web: Adjust texts when changing the device of a storage definition
ancorgs Apr 13, 2026
6f61600
web: Adjust texts for adding new storage definitions
ancorgs Apr 13, 2026
0a29263
Remove test
joseivanlopez Apr 13, 2026
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# frozen_string_literal: true

# Copyright (c) [2025] SUSE LLC
# Copyright (c) [2025-2026] SUSE LLC
#
# All Rights Reserved.
#
Expand Down Expand Up @@ -53,7 +53,7 @@ def lvm_vg_size
#
# @return [Array<String>]
def lvm_vg_pvs
storage_device.lvm_pvs.map(&:sid)
storage_device.lvm_pvs.map(&:plain_blk_device).map(&:sid)
end
end
end
Expand Down
7 changes: 6 additions & 1 deletion web/src/components/core/Annotation.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2025] SUSE LLC
* Copyright (c) [2025-2026] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -48,4 +48,9 @@ describe("Annotation", () => {
const content = screen.getByText("Configured for installation only");
expect(content.tagName).toBe("STRONG");
});

it("renders nothing when children is empty", () => {
const { container } = plainRender(<Annotation>{undefined}</Annotation>);
expect(container).toBeEmptyDOMElement();
});
});
4 changes: 3 additions & 1 deletion web/src/components/core/Annotation.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2025] SUSE LLC
* Copyright (c) [2025-2026] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -45,6 +45,8 @@ type AnnotationProps = React.PropsWithChildren<{
* ```
*/
export default function Annotation({ icon = "emergency", children }: AnnotationProps) {
if (!children) return null;

return (
<Flex component="p" alignItems={{ default: "alignItemsCenter" }} gap={{ default: "gapXs" }}>
<Icon name={icon} /> <strong>{children}</strong>
Expand Down
17 changes: 17 additions & 0 deletions web/src/components/core/Popup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ describe("Popup.SecondaryAction", () => {
const button = screen.queryByRole("button", { name: "Do something" });
expect(button.classList.contains("pf-m-secondary")).toBe(true);
});

it("renders a 'link' button when asLink is set", async () => {
installerRender(<Popup.SecondaryAction asLink>Do something</Popup.SecondaryAction>);

const button = screen.queryByRole("button", { name: "Do something" });
expect(button.classList.contains("pf-m-link")).toBe(true);
});
});

describe("Popup.AncillaryAction", () => {
Expand Down Expand Up @@ -234,4 +241,14 @@ describe("Popup.Cancel", () => {
expect(button.classList.contains("pf-m-secondary")).toBe(true);
});
});

describe("when asLink is set", () => {
it("renders a 'link' button", async () => {
installerRender(<Popup.Cancel asLink />);

const button = screen.queryByRole("button", { name: "Cancel" });
expect(button).not.toBeNull();
expect(button.classList.contains("pf-m-link")).toBe(true);
});
});
});
6 changes: 3 additions & 3 deletions web/src/components/core/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { fork } from "radashi";
import { _, TranslatedString } from "~/i18n";

type ButtonWithoutVariantProps = Omit<ButtonProps, "variant">;
type PredefinedAction = React.PropsWithChildren<ButtonWithoutVariantProps>;
type PredefinedAction = React.PropsWithChildren<ButtonWithoutVariantProps & { asLink?: boolean }>;
export type PopupProps = {
/** The dialog title */
title?: ModalHeaderProps["title"];
Expand Down Expand Up @@ -122,8 +122,8 @@ const Confirm = ({ children = _("Confirm"), ...actionProps }: PredefinedAction)
* <Text>Dismiss</Text>
* </SecondaryAction>
*/
const SecondaryAction = ({ children, ...actionProps }: PredefinedAction) => (
<Action {...actionProps} variant="secondary">
const SecondaryAction = ({ children, asLink, ...actionProps }: PredefinedAction) => (
<Action {...actionProps} variant={asLink ? "link" : "secondary"}>
{children}
</Action>
);
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/layout/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import MoreVert from "@bolderIcons/more_vert.svg?component";
import NetworkWifi from "@icons/network_wifi.svg?component";
import NetworkWifi1Bar from "@icons/network_wifi_1_bar.svg?component";
import NetworkWifi3Bar from "@icons/network_wifi_3_bar.svg?component";
import NotificationsActive from "@icons/notifications_active.svg?component";
import Report from "@icons/report.svg?component";
import RestartAlt from "@icons/restart_alt.svg?component";
import SearchOff from "@icons/search_off.svg?component";
Expand Down Expand Up @@ -109,6 +110,7 @@ const icons = {
network_wifi: NetworkWifi,
network_wifi_1_bar: NetworkWifi1Bar,
network_wifi_3_bar: NetworkWifi3Bar,
notifications_ative: NotificationsActive,
report: Report,
restart_alt: RestartAlt,
search_off: SearchOff,
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/storage/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export default function ConfigEditor() {
<>
{/* FIXME add arial label */}
<DataList aria-label="" isCompact className="storage-structure">
{volumeGroups.map((vg, i) => {
return <VolumeGroupEditor key={`vg-${i}`} vg={vg} />;
{volumeGroups.map((_, i) => {
return <VolumeGroupEditor key={`vg-${i}`} index={i} />;
})}
{mdRaids.map((_, i) => (
<MdRaidEditor key={`md-${i}`} index={i} />
Expand Down
81 changes: 69 additions & 12 deletions web/src/components/storage/ConfigureDeviceMenu.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2025] SUSE LLC
* Copyright (c) [2025-2026] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -53,6 +53,22 @@ const vdb: Storage.Device = {
},
};

const md0: Storage.Device = {
sid: 61,
class: "mdRaid",
name: "/dev/md0",
description: "MD RAID 0",
md: { level: "raid1", devices: [59, 60] },
block: {
start: 0,
size: 2e12,
active: true,
encrypted: false,
systems: [],
shrinking: { supported: false },
},
};

const vdaDrive: ConfigModel.Drive = {
name: "/dev/vda",
spacePolicy: "delete",
Expand All @@ -68,22 +84,27 @@ const vdbDrive: ConfigModel.Drive = {
const mockAddDrive = jest.fn();
const mockAddReusedMdRaid = jest.fn();
const mockUseModel = jest.fn();
const mockUseAvailableDevices = jest.fn();

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

jest.mock("~/hooks/model/storage/config-model", () => ({
...jest.requireActual("~/hooks/model/storage/config-model"),
useConfigModel: () => mockUseModel(),
useAddDrive: () => mockAddDrive,
useAddMdRaid: () => mockAddReusedMdRaid,
useAddVolumeGroup: () => jest.fn(),
}));

describe("ConfigureDeviceMenu", () => {
beforeEach(() => {
mockUseModel.mockReturnValue({ drives: [], mdRaids: [] });
mockUseModel.mockReturnValue({ drives: [], mdRaids: [], volumeGroups: [] });
mockUseAvailableDevices.mockReturnValue([vda, vdb]);
});

it("renders an initially closed menu ", async () => {
Expand Down Expand Up @@ -113,18 +134,36 @@ describe("ConfigureDeviceMenu", () => {
const disksMenuItem = screen.getByRole("menuitem", { name: "Add device menu" });
await user.click(disksMenuItem);
const dialog = screen.getByRole("dialog", { name: /Select a disk/ });
const confirmButton = screen.getByRole("button", { name: "Confirm" });
const vdaItemRow = within(dialog).getByRole("row", { name: /\/dev\/vda/ });
const confirmButton = screen.getByRole("button", { name: /Add/ });
const vdaItemRow = within(dialog).getByRole("row", { name: /vda/ });
const vdaItemRadio = within(vdaItemRow).getByRole("radio");
await user.click(vdaItemRadio);
await user.click(confirmButton);
expect(mockAddDrive).toHaveBeenCalledWith({ name: "/dev/vda", spacePolicy: "keep" });
});

it("shows intro text in the device selector", async () => {
const { user } = installerRender(<ConfigureDeviceMenu />);
const toggler = screen.getByRole("button", { name: /More devices/ });
await user.click(toggler);
await user.click(screen.getByRole("menuitem", { name: "Add device menu" }));
within(screen.getByRole("dialog")).getByText("Start configuring a basic installation");
});

it("allows canceling the device selector without adding any device", async () => {
const { user } = installerRender(<ConfigureDeviceMenu />);
const toggler = screen.getByRole("button", { name: /More devices/ });
await user.click(toggler);
await user.click(screen.getByRole("menuitem", { name: "Add device menu" }));
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(screen.queryByRole("dialog")).toBeNull();
expect(mockAddDrive).not.toHaveBeenCalled();
});
});

describe("but some disks are already configured", () => {
beforeEach(() => {
mockUseModel.mockReturnValue({ drives: [vdaDrive], mdRaids: [] });
mockUseModel.mockReturnValue({ drives: [vdaDrive], mdRaids: [], volumeGroups: [] });
});

it("allows users to add a new drive to an unused disk", async () => {
Expand All @@ -134,11 +173,11 @@ describe("ConfigureDeviceMenu", () => {
const disksMenuItem = screen.getByRole("menuitem", { name: "Add device menu" });
await user.click(disksMenuItem);
const dialog = screen.getByRole("dialog", { name: /Select another disk/ });
const confirmButton = screen.getByRole("button", { name: "Confirm" });
expect(screen.queryByRole("row", { name: /vda$/ })).toBeNull();
const vdaItemRow = within(dialog).getByRole("row", { name: /\/dev\/vdb/ });
const vdaItemRadio = within(vdaItemRow).getByRole("radio");
await user.click(vdaItemRadio);
const confirmButton = screen.getByRole("button", { name: /Add/ });
expect(screen.queryByRole("row", { name: /vda/ })).toBeNull();
const vdbItemRow = within(dialog).getByRole("row", { name: /vdb/ });
const vdbItemRadio = within(vdbItemRow).getByRole("radio");
await user.click(vdbItemRadio);
await user.click(confirmButton);
expect(mockAddDrive).toHaveBeenCalledWith({ name: "/dev/vdb", spacePolicy: "keep" });
});
Expand All @@ -147,7 +186,7 @@ describe("ConfigureDeviceMenu", () => {

describe("when there are no more unused disks", () => {
beforeEach(() => {
mockUseModel.mockReturnValue({ drives: [vdaDrive, vdbDrive], mdRaids: [] });
mockUseModel.mockReturnValue({ drives: [vdaDrive, vdbDrive], mdRaids: [], volumeGroups: [] });
});

it("renders the disks menu as disabled with an informative label", async () => {
Expand All @@ -156,6 +195,24 @@ describe("ConfigureDeviceMenu", () => {
await user.click(toggler);
const disksMenuItem = screen.getByRole("menuitem", { name: "Add device menu" });
expect(disksMenuItem).toBeDisabled();
within(disksMenuItem).getByText("Already using all available disks");
});
});

describe("when there are MD RAID devices available", () => {
beforeEach(() => {
mockUseAvailableDevices.mockReturnValue([vda, md0]);
});

it("allows adding an MD RAID device", async () => {
const { user } = installerRender(<ConfigureDeviceMenu />);
const toggler = screen.getByRole("button", { name: /More devices/ });
await user.click(toggler);
await user.click(screen.getByRole("menuitem", { name: "Add device menu" }));
const dialog = screen.getByRole("dialog");
await user.click(within(dialog).getByRole("tab", { name: "RAID" }));
await user.click(screen.getByRole("button", { name: /Add/ }));
expect(mockAddReusedMdRaid).toHaveBeenCalledWith({ name: "/dev/md0", spacePolicy: "keep" });
});
});
});
Loading
Loading