diff --git a/web/package/cockpit-agama.changes b/web/package/cockpit-agama.changes
index 30ae533f8b..a28b65bab7 100644
--- a/web/package/cockpit-agama.changes
+++ b/web/package/cockpit-agama.changes
@@ -1,3 +1,9 @@
+-------------------------------------------------------------------
+Thu Feb 22 14:05:56 UTC 2024 - David Diaz
+
+- Break storage settings in multiple sections to improve the UX
+ (gh#openSUSE/agama#1045).
+
-------------------------------------------------------------------
Wed Feb 21 17:40:01 UTC 2024 - David Diaz
diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss
index a6d42c2e68..30e95e4aa4 100644
--- a/web/src/assets/styles/blocks.scss
+++ b/web/src/assets/styles/blocks.scss
@@ -18,24 +18,36 @@
margin-block-end: var(--spacer-medium);
}
- > h2 {
+ > header {
display: grid;
grid-area: header;
grid-template-columns: subgrid;
grid-column: bleed / content-end;
- svg {
- block-size: var(--section-icon-size);
- inline-size: var(--section-icon-size);
- grid-column: bleed / content;
+ h2 {
+ display: grid;
+ grid-template-columns: subgrid;
+ grid-column: bleed / content-end;
+
+ svg {
+ block-size: var(--section-icon-size);
+ inline-size: var(--section-icon-size);
+ grid-column: bleed / content;
+ }
+
+ :not(svg) {
+ grid-column: content
+ }
}
- :not(svg) {
- grid-column: content
+ p {
+ grid-column: content;
+ color: var(--color-gray-dimmest);
+ margin-block-end: var(--spacer-smaller);
}
}
- > :not(h2) {
+ > :not(header) {
grid-area: content;
grid-column: content;
}
diff --git a/web/src/assets/styles/layout.scss b/web/src/assets/styles/layout.scss
index 6e879528f1..d8a7034ad5 100644
--- a/web/src/assets/styles/layout.scss
+++ b/web/src/assets/styles/layout.scss
@@ -20,7 +20,7 @@
padding: var(--spacer-small);
}
- header {
+ > header {
@extend .bottom-shadow;
grid-area: header;
display: flex;
diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss
index 90fa1bb699..9127b55a5b 100644
--- a/web/src/assets/styles/patternfly-overrides.scss
+++ b/web/src/assets/styles/patternfly-overrides.scss
@@ -127,7 +127,6 @@ table td > .pf-v5-c-empty-state {
--stack-gutter: 0;
--pf-v5-c-toolbar--PaddingTop: 0;
--pf-v5-c-toolbar--PaddingBottom: 0;
- border-block-end: 1px solid var(--color-gray-light);
}
.pf-v5-c-toolbar__content {
diff --git a/web/src/assets/styles/variables.scss b/web/src/assets/styles/variables.scss
index fb96b3058f..b25bfcbad3 100644
--- a/web/src/assets/styles/variables.scss
+++ b/web/src/assets/styles/variables.scss
@@ -52,6 +52,7 @@
--color-gray-dark: #efefef; // Fog
--color-gray-darker: #999;
--color-gray-dimmed: #888;
+ --color-gray-dimmest: #666;
--color-link: #0c322c;
--color-link-hover: #30ba78;
diff --git a/web/src/components/core/Section.jsx b/web/src/components/core/Section.jsx
index 0a6b755740..c1954b7d4e 100644
--- a/web/src/components/core/Section.jsx
+++ b/web/src/components/core/Section.jsx
@@ -24,7 +24,7 @@
import React from "react";
import { Link } from "react-router-dom";
import { Icon } from '~/components/layout';
-import { ValidationErrors } from "~/components/core";
+import { If, ValidationErrors } from "~/components/core";
/**
* Renders children into an HTML section
@@ -48,6 +48,7 @@ import { ValidationErrors } from "~/components/core";
* @typedef { Object } SectionProps
* @property {string} [icon] - Name of the section icon. Not rendered if title is not provided.
* @property {string} [title] - The section title. If not given, aria-label must be provided.
+ * @property {string|React.ReactElement} [description] - A section description. Use only if really needed.
* @property {string} [name] - The section name. Used to build the header id.
* @property {string} [path] - Path where the section links to.
* when user clicks on the title, used for opening a dialog.
@@ -63,6 +64,7 @@ import { ValidationErrors } from "~/components/core";
export default function Section({
icon,
title,
+ description,
name,
path,
loading,
@@ -84,9 +86,13 @@ export default function Section({
const iconName = loading ? "loading" : icon;
const headerIcon = iconName ? : null;
const headerText = !path?.trim() ? title : {title};
+ const renderDescription = React.isValidElement(description) || description?.length > 0;
return (
-
+
} />
+
);
};
diff --git a/web/src/components/core/Section.test.jsx b/web/src/components/core/Section.test.jsx
index d7d5db2a81..f132a637a6 100644
--- a/web/src/components/core/Section.test.jsx
+++ b/web/src/components/core/Section.test.jsx
@@ -36,7 +36,13 @@ describe("Section", () => {
describe("when title is given", () => {
it("renders the section header", () => {
plainRender();
- screen.getByRole("heading", { name: "Settings" });
+ screen.getByRole("banner");
+ });
+
+ it("renders given title as section heading", () => {
+ plainRender();
+ const header = screen.getByRole("banner");
+ within(header).getByRole("heading", { name: "Settings" });
});
it("renders an icon if valid icon name is given", () => {
@@ -58,15 +64,29 @@ describe("Section", () => {
const icon = container.querySelector("svg");
expect(icon).toBeNull();
});
+
+ it("renders given description as part of the header", () => {
+ plainRender(
+
+ );
+ const header = screen.getByRole("banner");
+ within(header).getByText(/Short explanation/);
+ });
});
describe("when title is not given", () => {
it("does not render the section header", async () => {
- plainRender();
- const header = await screen.queryByRole("heading");
+ plainRender();
+ const header = await screen.queryByRole("banner");
expect(header).not.toBeInTheDocument();
});
+ it("does not render a section heading", async () => {
+ plainRender();
+ const heading = await screen.queryByRole("heading");
+ expect(heading).not.toBeInTheDocument();
+ });
+
it("does not render the section icon", () => {
const { container } = plainRender();
const icon = container.querySelector("svg");
diff --git a/web/src/components/storage/ProposalActionsSection.jsx b/web/src/components/storage/ProposalActionsSection.jsx
index e6a272b8e5..8d35563afd 100644
--- a/web/src/components/storage/ProposalActionsSection.jsx
+++ b/web/src/components/storage/ProposalActionsSection.jsx
@@ -25,7 +25,6 @@ import {
ListItem,
ExpandableSection,
Skeleton,
- Text
} from "@patternfly/react-core";
import { sprintf } from "sprintf-js";
@@ -74,9 +73,6 @@ const ProposalActions = ({ actions = [] }) => {
return (
<>
-
- {_("Actions to create the file systems and to ensure the system boots.")}
-
{subvolActions.length > 0 && (
+
}
diff --git a/web/src/components/storage/ProposalActionsSection.test.jsx b/web/src/components/storage/ProposalActionsSection.test.jsx
index 46d9bf4964..9864391d0d 100644
--- a/web/src/components/storage/ProposalActionsSection.test.jsx
+++ b/web/src/components/storage/ProposalActionsSection.test.jsx
@@ -65,8 +65,9 @@ it("renders skeleton while loading", () => {
it("renders nothing when there is no actions", () => {
plainRender();
- expect(screen.queryByText(/Actions to create/)).toBeNull();
+ expect(screen.queryAllByText(/Delete/)).toEqual([]);
expect(screen.queryAllByText(/Create/)).toEqual([]);
+ expect(screen.queryAllByText(/Show/)).toEqual([]);
});
describe("when there are actions", () => {
diff --git a/web/src/components/storage/ProposalDeviceSection.jsx b/web/src/components/storage/ProposalDeviceSection.jsx
new file mode 100644
index 0000000000..df37eaf304
--- /dev/null
+++ b/web/src/components/storage/ProposalDeviceSection.jsx
@@ -0,0 +1,441 @@
+/*
+ * Copyright (c) [2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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, { useState } from "react";
+import {
+ Button,
+ Form,
+ Skeleton,
+ Switch,
+ ToggleGroup, ToggleGroupItem,
+ Tooltip
+} from "@patternfly/react-core";
+
+import { _ } from "~/i18n";
+import { Icon } from "~/components/layout";
+import { If, Section, Popup } from "~/components/core";
+import { DeviceList, DeviceSelector } from "~/components/storage";
+import { deviceLabel } from '~/components/storage/utils';
+import { noop } from "~/utils";
+
+/**
+ * @typedef {import ("~/client/storage").ProposalManager.ProposalSettings} ProposalSettings
+ * @typedef {import ("~/client/storage").DevicesManager.StorageDevice} StorageDevice
+ */
+
+/**
+ * Form for selecting the installation device.
+ * @component
+ *
+ * @param {object} props
+ * @param {string} props.id - Form ID.
+ * @param {StorageDevice} [props.current] - Currently selected device, if any.
+ * @param {StorageDevice[]} [props.devices=[]] - Available devices for the selection.
+ * @param {onSubmitFn} [props.onSubmit=noop] - On submit callback.
+ *
+ * @callback onSubmitFn
+ * @param {string} device - Name of the selected device.
+ */
+const InstallationDeviceForm = ({
+ id,
+ current,
+ devices = [],
+ onSubmit = noop
+}) => {
+ const [device, setDevice] = useState(current || devices[0]);
+
+ const changeSelected = (deviceId) => {
+ setDevice(devices.find(d => d.sid === deviceId));
+ };
+
+ const submitForm = (e) => {
+ e.preventDefault();
+ if (device !== undefined) onSubmit(device);
+ };
+
+ return (
+
+ );
+};
+
+/**
+ * Allows to select the installation device.
+ * @component
+ *
+ * @callback onChangeFn
+ * @param {string} device - Name of the selected device.
+ *
+ * @param {object} props
+ * @param {string} [props.current] - Device name, if any.
+ * @param {StorageDevice[]} [props.devices=[]] - Available devices for the selection.
+ * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading.
+ * @param {onChangeFn} [props.onChange=noop] - On change callback.
+ */
+const InstallationDeviceField = ({
+ current,
+ devices = [],
+ isLoading = false,
+ onChange = noop
+}) => {
+ const [device, setDevice] = useState(devices.find(d => d.name === current));
+ const [isFormOpen, setIsFormOpen] = useState(false);
+
+ const openForm = () => setIsFormOpen(true);
+
+ const closeForm = () => setIsFormOpen(false);
+
+ const acceptForm = (selectedDevice) => {
+ closeForm();
+ setDevice(selectedDevice);
+ onChange(selectedDevice);
+ };
+
+ /**
+ * Renders a button that allows changing selected device
+ *
+ * NOTE: if a device is already selected, its name and size will be used for
+ * the button text. Otherwise, a "No device selected" text will be shown.
+ *
+ * @param {object} props
+ * @param {StorageDevice|undefined} [props.current] - Currently selected device, if any.
+ */
+ const DeviceContent = ({ device }) => {
+ return (
+
+ );
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ const description = _("Select the device for installing the system.");
+
+ return (
+ <>
+
+ {_("Installation device")}
+
+
+
+
+ }
+ />
+
+
+ {_("Accept")}
+
+
+
+
+ >
+ );
+};
+
+/**
+ * Form for configuring the system volume group.
+ * @component
+ *
+ * @param {object} props
+ * @param {string} props.id - Form ID.
+ * @param {ProposalSettings} props.settings - Settings used for calculating a proposal.
+ * @param {StorageDevice[]} [props.devices=[]] - Available storage devices.
+ * @param {onSubmitFn} [props.onSubmit=noop] - On submit callback.
+ * @param {onValidateFn} [props.onValidate=noop] - On validate callback.
+ *
+ * @callback onSubmitFn
+ * @param {string[]} devices - Name of the selected devices.
+ *
+ * @callback onValidateFn
+ * @param {boolean} valid
+ */
+const LVMSettingsForm = ({
+ id,
+ settings,
+ devices = [],
+ onSubmit: onSubmitProp = noop,
+ onValidate = noop
+}) => {
+ const [vgDevices, setVgDevices] = useState(settings.systemVGDevices);
+ const [isBootDeviceSelected, setIsBootDeviceSelected] = useState(settings.systemVGDevices.length === 0);
+ const [editedDevices, setEditedDevices] = useState(false);
+
+ const selectBootDevice = () => {
+ setIsBootDeviceSelected(true);
+ onValidate(true);
+ };
+
+ const selectCustomDevices = () => {
+ setIsBootDeviceSelected(false);
+ const { bootDevice } = settings;
+ const customDevices = (vgDevices.length === 0 && !editedDevices) ? [bootDevice] : vgDevices;
+ setVgDevices(customDevices);
+ onValidate(customDevices.length > 0);
+ };
+
+ const onChangeDevices = (selection) => {
+ const selectedDevices = devices.filter(d => selection.includes(d.sid)).map(d => d.name);
+ setVgDevices(selectedDevices);
+ setEditedDevices(true);
+ onValidate(devices.length > 0);
+ };
+
+ const onSubmit = (e) => {
+ e.preventDefault();
+ const customDevices = isBootDeviceSelected ? [] : vgDevices;
+ onSubmitProp(customDevices);
+ };
+
+ const BootDevice = () => {
+ const bootDevice = devices.find(d => d.name === settings.bootDevice);
+
+ // FIXME: In this case, should be a "readOnly" selector.
+ return ;
+ };
+
+ return (
+
+ );
+};
+
+/**
+ * Allows to select LVM and configure the system volume group.
+ * @component
+ *
+ * @param {object} props
+ * @param {ProposalSettings} props.settings - Settings used for calculating a proposal.
+ * @param {StorageDevice[]} [props.devices=[]] - Available storage devices.
+ * @param {boolean} [props.isChecked=false] - Whether LVM is selected.
+ * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading.
+ * @param {onChangeFn} [props.onChange=noop] - On change callback.
+ *
+ * @callback onChangeFn
+ * @param {boolean} lvm
+ */
+const LVMField = ({
+ settings,
+ devices = [],
+ isChecked: isCheckedProp = false,
+ isLoading = false,
+ onChange: onChangeProp = noop
+}) => {
+ const [isChecked, setIsChecked] = useState(isCheckedProp);
+ const [isFormOpen, setIsFormOpen] = useState(false);
+ const [isFormValid, setIsFormValid] = useState(true);
+
+ const onChange = (_, value) => {
+ setIsChecked(value);
+ onChangeProp({ lvm: value, vgDevices: [] });
+ };
+
+ const openForm = () => setIsFormOpen(true);
+
+ const closeForm = () => setIsFormOpen(false);
+
+ const onValidateForm = (valid) => setIsFormValid(valid);
+
+ const onSubmitForm = (vgDevices) => {
+ closeForm();
+ onChangeProp({ vgDevices });
+ };
+
+ const description = _("Configuration of the system volume group. All the file systems will be \
+created in a logical volume of the system volume group.");
+
+ const LVMSettingsButton = () => {
+ return (
+
+
+
+ );
+ };
+
+ if (isLoading) return ;
+
+ return (
+
+
+
} />
+
+
+
+
+ {_("Accept")}
+
+
+
+
+
+ );
+};
+
+/**
+ * Section for editing the selected device
+ * @component
+ *
+ * @callback onChangeFn
+ * @param {object} settings
+ *
+ * @param {object} props
+ * @param {ProposalSettings} props.settings
+ * @param {StorageDevice[]} [props.availableDevices=[]]
+ * @param {boolean} [isLoading=false]
+ * @param {onChangeFn} [props.onChange=noop]
+ */
+export default function ProposalDeviceSection({
+ settings,
+ availableDevices = [],
+ isLoading = false,
+ onChange = noop
+}) {
+ // FIXME: we should work with devices objects ASAP
+ const { bootDevice } = settings;
+
+ const changeBootDevice = (device) => {
+ if (device.name !== bootDevice) {
+ onChange({ bootDevice: device.name });
+ }
+ };
+
+ const changeLVM = ({ lvm, vgDevices }) => {
+ const settings = {};
+ if (lvm !== undefined) settings.lvm = lvm;
+ if (vgDevices !== undefined) settings.systemVGDevices = vgDevices;
+
+ onChange(settings);
+ };
+
+ const Description = () => (
+ LVM \
+Volume Group for installation.")
+ }}
+ />
+ );
+
+ return (
+ }
+ >
+
+
+
+ );
+}
diff --git a/web/src/components/storage/ProposalDeviceSection.test.jsx b/web/src/components/storage/ProposalDeviceSection.test.jsx
new file mode 100644
index 0000000000..73cd460835
--- /dev/null
+++ b/web/src/components/storage/ProposalDeviceSection.test.jsx
@@ -0,0 +1,396 @@
+/*
+ * Copyright (c) [2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { screen, within } from "@testing-library/react";
+import { plainRender } from "~/test-utils";
+import { ProposalDeviceSection } from "~/components/storage";
+
+const sda = {
+ sid: "59",
+ isDrive: true,
+ type: "disk",
+ vendor: "Micron",
+ model: "Micron 1100 SATA",
+ driver: ["ahci", "mmcblk"],
+ bus: "IDE",
+ busId: "",
+ transport: "usb",
+ dellBOSS: false,
+ sdCard: true,
+ active: true,
+ name: "/dev/sda",
+ size: 1024,
+ recoverableSize: 0,
+ systems : [],
+ udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"],
+ udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"],
+};
+
+const sdb = {
+ sid: "62",
+ isDrive: true,
+ type: "disk",
+ vendor: "Samsung",
+ model: "Samsung Evo 8 Pro",
+ driver: ["ahci"],
+ bus: "IDE",
+ busId: "",
+ transport: "",
+ dellBOSS: false,
+ sdCard: false,
+ active: true,
+ name: "/dev/sdb",
+ size: 2048,
+ recoverableSize: 0,
+ systems : [],
+ udevIds: [],
+ udevPaths: ["pci-0000:00-19"]
+};
+
+const vda = {
+ sid: "59",
+ type: "disk",
+ vendor: "Micron",
+ model: "Micron 1100 SATA",
+ driver: ["ahci", "mmcblk"],
+ bus: "IDE",
+ transport: "usb",
+ dellBOSS: false,
+ sdCard: true,
+ active: true,
+ name: "/dev/vda",
+ size: 1024,
+ systems: ["Windows", "openSUSE Leap 15.2"],
+ udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"],
+ udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"],
+ partitionTable: { type: "gpt", partitions: [] }
+};
+
+const md0 = {
+ sid: "62",
+ type: "md",
+ level: "raid0",
+ uuid: "12345:abcde",
+ members: ["/dev/vdb"],
+ active: true,
+ name: "/dev/md0",
+ size: 2048,
+ systems: [],
+ udevIds: [],
+ udevPaths: []
+};
+
+const md1 = {
+ sid: "63",
+ type: "md",
+ level: "raid0",
+ uuid: "12345:abcde",
+ members: ["/dev/vdc"],
+ active: true,
+ name: "/dev/md1",
+ size: 4096,
+ systems: [],
+ udevIds: [],
+ udevPaths: []
+};
+
+const props = {
+ settings: {
+ bootDevice: "/dev/sda",
+ },
+ availableDevices: [sda, sdb],
+ isLoading: false,
+ onChange: jest.fn()
+};
+
+describe("ProposalDeviceSection", () => {
+ describe("Installation device field", () => {
+ describe("when set as loading", () => {
+ beforeEach(() => {
+ props.isLoading = true;
+ });
+
+ describe("and selected device is not defined yet", () => {
+ beforeEach(() => {
+ props.settings = { bootDevice: undefined };
+ });
+
+ it("renders a loading hint", () => {
+ plainRender();
+ screen.getByText("Waiting for information about selected device");
+ });
+ });
+ });
+ describe("when installation device is not selected yet", () => {
+ beforeEach(() => {
+ props.settings = { bootDevice: "" };
+ });
+
+ it("uses a 'No device selected yet' text for the selection button", async () => {
+ const { user } = plainRender();
+ const button = screen.getByRole("button", { name: "No device selected yet" });
+
+ await user.click(button);
+
+ screen.getByRole("dialog", { name: "Installation device" });
+ });
+ });
+
+ describe("when installation device is selected", () => {
+ beforeEach(() => {
+ props.settings = { bootDevice: "/dev/sda" };
+ });
+
+ it("uses its name as part of the text for the selection button", async () => {
+ const { user } = plainRender();
+ const button = screen.getByRole("button", { name: /\/dev\/sda/ });
+
+ await user.click(button);
+
+ screen.getByRole("dialog", { name: "Installation device" });
+ });
+ });
+
+ it("allows changing the selected device", async () => {
+ const { user } = plainRender();
+ const button = screen.getByRole("button", { name: "/dev/sda, 1 KiB" });
+
+ await user.click(button);
+
+ const selector = await screen.findByRole("dialog", { name: "Installation device" });
+ const sdbOption = within(selector).getByRole("radio", { name: /sdb/ });
+ const accept = within(selector).getByRole("button", { name: "Accept" });
+
+ await user.click(sdbOption);
+ await user.click(accept);
+
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ expect(props.onChange).toHaveBeenCalledWith({ bootDevice: sdb.name });
+ });
+
+ it("allows canceling a device selection", async () => {
+ const { user } = plainRender();
+ const button = screen.getByRole("button", { name: "/dev/sda, 1 KiB" });
+
+ await user.click(button);
+
+ const selector = await screen.findByRole("dialog", { name: "Installation device" });
+ const sdbOption = within(selector).getByRole("radio", { name: /sdb/ });
+ const cancel = within(selector).getByRole("button", { name: "Cancel" });
+
+ await user.click(sdbOption);
+ await user.click(cancel);
+
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ expect(props.onChange).not.toHaveBeenCalled();
+ });
+
+ it("does not trigger the onChange callback when selection actually did not change", async () => {
+ const { user } = plainRender();
+ const button = screen.getByRole("button", { name: "/dev/sda, 1 KiB" });
+
+ await user.click(button);
+
+ const selector = await screen.findByRole("dialog", { name: "Installation device" });
+ const sdaOption = within(selector).getByRole("radio", { name: /sda/ });
+ const sdbOption = within(selector).getByRole("radio", { name: /sdb/ });
+ const accept = within(selector).getByRole("button", { name: "Accept" });
+
+ // User selects a different device
+ await user.click(sdbOption);
+ // but then goes back to the selected device
+ await user.click(sdaOption);
+ // and clicks on Accept button
+ await user.click(accept);
+
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ // There is no reason for triggering the onChange callback
+ expect(props.onChange).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("LVM field", () => {
+ describe("if LVM setting is not set yet", () => {
+ beforeEach(() => {
+ props.settings = {};
+ });
+
+ it("does not render the LVM switch", () => {
+ plainRender();
+
+ expect(screen.queryByLabelText(/Use logical volume/)).toBeNull();
+ });
+ });
+
+ describe("if LVM setting is set", () => {
+ beforeEach(() => {
+ props.settings = { lvm: false };
+ });
+
+ it("renders the LVM switch", () => {
+ plainRender();
+
+ screen.getByRole("checkbox", { name: /Use logical volume/ });
+ });
+ });
+
+ describe("if LVM is set to true", () => {
+ beforeEach(() => {
+ props.availableDevices = [vda, md0, md1];
+ props.settings = { bootDevice: "/dev/vda", lvm: true, systemVGDevices: [] };
+ props.onChange = jest.fn();
+ });
+
+ it("renders the LVM switch as selected", () => {
+ plainRender();
+
+ const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
+ expect(checkbox).toBeChecked();
+ });
+
+ it("renders a button for changing the LVM settings", () => {
+ plainRender();
+
+ screen.getByRole("button", { name: /LVM settings/ });
+ });
+
+ it("changes the selection on click", async () => {
+ const { user } = plainRender();
+
+ const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
+ await user.click(checkbox);
+
+ expect(checkbox).not.toBeChecked();
+ expect(props.onChange).toHaveBeenCalled();
+ });
+
+ describe("and user clicks on LVM settings", () => {
+ it("opens the LVM settings dialog", async () => {
+ const { user } = plainRender();
+ const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
+
+ await user.click(settingsButton);
+
+ const popup = await screen.findByRole("dialog");
+ within(popup).getByText("System Volume Group");
+ });
+
+ it("allows selecting either installation device or custom devices", async () => {
+ const { user } = plainRender();
+ const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
+
+ await user.click(settingsButton);
+
+ const popup = await screen.findByRole("dialog");
+ screen.getByText("System Volume Group");
+
+ within(popup).getByRole("button", { name: "Installation device" });
+ within(popup).getByRole("button", { name: "Custom devices" });
+ });
+
+ it("allows to set the installation device as system volume group", async () => {
+ const { user } = plainRender();
+ const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
+
+ await user.click(settingsButton);
+
+ const popup = await screen.findByRole("dialog");
+ screen.getByText("System Volume Group");
+
+ const bootDeviceButton = within(popup).getByRole("button", { name: "Installation device" });
+ const customDevicesButton = within(popup).getByRole("button", { name: "Custom devices" });
+ const acceptButton = within(popup).getByRole("button", { name: "Accept" });
+
+ await user.click(customDevicesButton);
+ await user.click(bootDeviceButton);
+ await user.click(acceptButton);
+
+ expect(props.onChange).toHaveBeenCalledWith(
+ expect.objectContaining({ systemVGDevices: [] })
+ );
+ });
+
+ it("allows customize the system volume group", async () => {
+ const { user } = plainRender();
+ const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
+
+ await user.click(settingsButton);
+
+ const popup = await screen.findByRole("dialog");
+ screen.getByText("System Volume Group");
+
+ const customDevicesButton = within(popup).getByRole("button", { name: "Custom devices" });
+ const acceptButton = within(popup).getByRole("button", { name: "Accept" });
+
+ await user.click(customDevicesButton);
+
+ const vdaOption = within(popup).getByRole("row", { name: /vda/ });
+ const md0Option = within(popup).getByRole("row", { name: /md0/ });
+ const md1Option = within(popup).getByRole("row", { name: /md1/ });
+
+ // unselect the boot devices
+ await user.click(vdaOption);
+
+ await user.click(md0Option);
+ await user.click(md1Option);
+
+ await user.click(acceptButton);
+
+ expect(props.onChange).toHaveBeenCalledWith(
+ expect.objectContaining({ systemVGDevices: ["/dev/md0", "/dev/md1"] })
+ );
+ });
+ });
+ });
+
+ describe("if LVM is set to false", () => {
+ beforeEach(() => {
+ props.settings = { lvm: false };
+ props.onChange = jest.fn();
+ });
+
+ it("renders the LVM switch as not selected", () => {
+ plainRender();
+
+ const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
+ expect(checkbox).not.toBeChecked();
+ });
+
+ it("does not render a button for changing the LVM settings", () => {
+ plainRender();
+
+ const button = screen.queryByRole("button", { name: /LVM settings/ });
+ expect(button).toBeNull();
+ });
+
+ it("changes the selection on click", async () => {
+ const { user } = plainRender();
+
+ const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
+ await user.click(checkbox);
+
+ expect(checkbox).toBeChecked();
+ expect(props.onChange).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/web/src/components/storage/ProposalFileSystemsSection.jsx b/web/src/components/storage/ProposalFileSystemsSection.jsx
new file mode 100644
index 0000000000..0b0e5f632d
--- /dev/null
+++ b/web/src/components/storage/ProposalFileSystemsSection.jsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) [2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { _ } from "~/i18n";
+import { Section } from "~/components/core";
+import { ProposalVolumes } from "~/components/storage";
+import { noop } from "~/utils";
+
+/**
+ * @typedef {import ("~/client/storage").ProposalManager.ProposalSettings} ProposalSettings
+ * @typedef {import ("~/client/storage").ProposalManager.Volume} Volume
+ */
+
+/**
+ * Section for editing the proposal file systems
+ * @component
+ *
+ * @callback onChangeFn
+ * @param {object} settings
+ *
+ * @param {object} props
+ * @param {ProposalSettings} props.settings
+ * @param {Volume[]} [props.volumeTemplates=[]]
+ * @param {boolean} [props.isLoading=false]
+ * @param {onChangeFn} [props.onChange=noop]
+ *
+ */
+export default function ProposalFileSystemsSection({
+ settings,
+ volumeTemplates = [],
+ isLoading = false,
+ onChange = noop
+}) {
+ const { volumes = [] } = settings;
+
+ const changeVolumes = (volumes) => {
+ onChange({ volumes });
+ };
+
+ // Templates for already existing mount points are filtered out
+ const usefulTemplates = () => {
+ const mountPaths = volumes.map(v => v.mountPath);
+ return volumeTemplates.filter(t => (
+ t.mountPath.length > 0 && !mountPaths.includes(t.mountPath)
+ ));
+ };
+
+ const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0;
+
+ return (
+
+ );
+}
diff --git a/web/src/components/storage/ProposalFileSystemsSection.test.jsx b/web/src/components/storage/ProposalFileSystemsSection.test.jsx
new file mode 100644
index 0000000000..0b1493a5ff
--- /dev/null
+++ b/web/src/components/storage/ProposalFileSystemsSection.test.jsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) [2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * 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 { screen, within } from "@testing-library/react";
+import { plainRender } from "~/test-utils";
+import { ProposalFileSystemsSection } from "~/components/storage";
+
+const props = {
+ settings: {},
+ isLoading: false,
+ onChange: jest.fn()
+};
+
+describe("ProposalFileSystemsSection", () => {
+ it("renders a section holding file systems related stuff", () => {
+ plainRender();
+ screen.getByRole("region", { name: "File systems" });
+ screen.getByRole("grid", { name: /mount points/ });
+ });
+
+ it("requests a volume change when onChange callback is triggered", async () => {
+ const { user } = plainRender();
+ const button = screen.getByRole("button", { name: "Actions" });
+
+ await user.click(button);
+
+ const menu = screen.getByRole("menu");
+ const reset = within(menu).getByRole("menuitem", { name: /Reset/ });
+
+ await user.click(reset);
+
+ expect(props.onChange).toHaveBeenCalledWith(
+ { volumes: expect.any(Array) }
+ );
+ });
+});
diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx
index e354ccfd93..51ab17e7bf 100644
--- a/web/src/components/storage/ProposalPage.jsx
+++ b/web/src/components/storage/ProposalPage.jsx
@@ -30,6 +30,8 @@ import {
ProposalPageMenu,
ProposalSettingsSection,
ProposalSpacePolicySection,
+ ProposalDeviceSection,
+ ProposalFileSystemsSection
} from "~/components/storage";
import { IDLE } from "~/client/status";
@@ -197,25 +199,27 @@ export default function ProposalPage() {
};
const PageContent = () => {
- // Templates for already existing mount points are filtered out
- const usefulTemplates = () => {
- const volumes = state.settings.volumes || [];
- const mountPaths = volumes.map(v => v.mountPath);
- return state.volumeTemplates.filter(t => (
- t.mountPath.length > 0 && !mountPaths.includes(t.mountPath)
- ));
- };
-
return (
<>
+
+
{
- const [device, setDevice] = useState(current || devices[0]);
-
- const changeSelected = (deviceId) => {
- setDevice(devices.find(d => d.sid === deviceId));
- };
-
- const submitForm = (e) => {
- e.preventDefault();
- if (device !== undefined) onSubmit(device);
- };
-
- return (
-
- );
-};
-
-/**
- * Allows to select the installation device.
- * @component
- *
- * @param {object} props
- * @param {string} [props.current] - Device name, if any.
- * @param {StorageDevice[]} [props.devices=[]] - Available devices for the selection.
- * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading.
- * @param {onChangeFn} [props.onChange=noop] - On change callback.
- *
- * @callback onChangeFn
- * @param {string} device - Name of the selected device.
- */
-const InstallationDeviceField = ({
- current,
- devices = [],
- isLoading = false,
- onChange = noop
-}) => {
- const [device, setDevice] = useState(devices.find(d => d.name === current));
- const [isFormOpen, setIsFormOpen] = useState(false);
-
- const openForm = () => setIsFormOpen(true);
-
- const closeForm = () => setIsFormOpen(false);
-
- const acceptForm = (selectedDevice) => {
- closeForm();
- setDevice(selectedDevice);
- onChange(selectedDevice);
- };
-
- /**
- * Renders a button that allows changing selected device
- *
- * NOTE: if a device is already selected, its name and size will be used for
- * the button text. Otherwise, a "No device selected" text will be shown.
- *
- * @param {object} props
- * @param {StorageDevice|undefined} [props.current] - Currently selected device, if any.
- */
- const DeviceContent = ({ device }) => {
- return (
-
- );
- };
-
- if (isLoading) {
- return ;
- }
-
- const description = _("Select the device for installing the system.");
-
- return (
- <>
-
- {_("Installation device")}
-
-
-
-
- }
- />
-
-
- {_("Accept")}
-
-
-
-
- >
- );
-};
-
-/**
- * Form for configuring the system volume group.
- * @component
- *
- * @param {object} props
- * @param {string} props.id - Form ID.
- * @param {ProposalSettings} props.settings - Settings used for calculating a proposal.
- * @param {StorageDevice[]} [props.devices=[]] - Available storage devices.
- * @param {onSubmitFn} [props.onSubmit=noop] - On submit callback.
- * @param {onValidateFn} [props.onValidate=noop] - On validate callback.
- *
- * @callback onSubmitFn
- * @param {string[]} devices - Name of the selected devices.
- *
- * @callback onValidateFn
- * @param {boolean} valid
- */
-const LVMSettingsForm = ({
- id,
- settings,
- devices = [],
- onSubmit: onSubmitProp = noop,
- onValidate = noop
-}) => {
- const [vgDevices, setVgDevices] = useState(settings.systemVGDevices);
- const [isBootDeviceSelected, setIsBootDeviceSelected] = useState(settings.systemVGDevices.length === 0);
- const [editedDevices, setEditedDevices] = useState(false);
-
- const selectBootDevice = () => {
- setIsBootDeviceSelected(true);
- onValidate(true);
- };
-
- const selectCustomDevices = () => {
- setIsBootDeviceSelected(false);
- const { bootDevice } = settings;
- const customDevices = (vgDevices.length === 0 && !editedDevices) ? [bootDevice] : vgDevices;
- setVgDevices(customDevices);
- onValidate(customDevices.length > 0);
- };
-
- const onChangeDevices = (selection) => {
- const selectedDevices = devices.filter(d => selection.includes(d.sid)).map(d => d.name);
- setVgDevices(selectedDevices);
- setEditedDevices(true);
- onValidate(devices.length > 0);
- };
-
- const onSubmit = (e) => {
- e.preventDefault();
- const customDevices = isBootDeviceSelected ? [] : vgDevices;
- onSubmitProp(customDevices);
- };
-
- const BootDevice = () => {
- const bootDevice = devices.find(d => d.name === settings.bootDevice);
-
- // FIXME: In this case, should be a "readOnly" selector.
- return ;
- };
-
- return (
-
- );
-};
-
-/**
- * Allows to select LVM and configure the system volume group.
- * @component
- *
- * @param {object} props
- * @param {ProposalSettings} props.settings - Settings used for calculating a proposal.
- * @param {StorageDevice[]} [props.devices=[]] - Available storage devices.
- * @param {boolean} [props.isChecked=false] - Whether LVM is selected.
- * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading.
- * @param {onChangeFn} [props.onChange=noop] - On change callback.
- *
- * @callback onChangeFn
- * @param {boolean} lvm
- */
-const LVMField = ({
- settings,
- devices = [],
- isChecked: isCheckedProp = false,
- isLoading = false,
- onChange: onChangeProp = noop
-}) => {
- const [isChecked, setIsChecked] = useState(isCheckedProp);
- const [isFormOpen, setIsFormOpen] = useState(false);
- const [isFormValid, setIsFormValid] = useState(true);
-
- const onChange = (_, value) => {
- setIsChecked(value);
- onChangeProp({ lvm: value, vgDevices: [] });
- };
-
- const openForm = () => setIsFormOpen(true);
-
- const closeForm = () => setIsFormOpen(false);
-
- const onValidateForm = (valid) => setIsFormValid(valid);
-
- const onSubmitForm = (vgDevices) => {
- closeForm();
- onChangeProp({ vgDevices });
- };
-
- const description = _("Configuration of the system volume group. All the file systems will be \
-created in a logical volume of the system volume group.");
-
- const LVMSettingsButton = () => {
- return (
-
-
-
- );
- };
-
- if (isLoading) return ;
-
- return (
-
-
-
} />
-
-
-
-
- {_("Accept")}
-
-
-
-
-
- );
-};
-
/**
* Form for configuring the encryption password.
* @component
@@ -558,10 +222,7 @@ const EncryptionField = ({
*
* @param {object} props
* @param {ProposalSettings} props.settings
- * @param {StorageDevice[]} [props.availableDevices=[]]
- * @param {Volume[]} [props.volumeTemplates=[]]
* @param {String[]} [props.encryptionMethods=[]]
- * @param {boolean} [isLoading=false]
* @param {onChangeFn} [props.onChange=noop]
*
* @callback onChangeFn
@@ -569,52 +230,18 @@ const EncryptionField = ({
*/
export default function ProposalSettingsSection({
settings,
- availableDevices = [],
- volumeTemplates = [],
encryptionMethods = [],
- isLoading = false,
onChange = noop
}) {
- // FIXME: we should work with devices objects ASAP
- const changeBootDevice = (device) => {
- onChange({ bootDevice: device.name });
- };
-
- const changeLVM = ({ lvm, vgDevices }) => {
- const settings = {};
- if (lvm !== undefined) settings.lvm = lvm;
- if (vgDevices !== undefined) settings.systemVGDevices = vgDevices;
-
- onChange(settings);
- };
-
const changeEncryption = ({ password, method }) => {
onChange({ encryptionPassword: password, encryptionMethod: method });
};
- const changeVolumes = (volumes) => {
- onChange({ volumes });
- };
-
- const { bootDevice } = settings;
const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0;
return (
<>
-
-
-
+
>
);
diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx
index a3687f6047..50de9dac00 100644
--- a/web/src/components/storage/ProposalSettingsSection.test.jsx
+++ b/web/src/components/storage/ProposalSettingsSection.test.jsx
@@ -35,333 +35,10 @@ jest.mock("@patternfly/react-core", () => {
let props;
-const vda = {
- sid: "59",
- type: "disk",
- vendor: "Micron",
- model: "Micron 1100 SATA",
- driver: ["ahci", "mmcblk"],
- bus: "IDE",
- transport: "usb",
- dellBOSS: false,
- sdCard: true,
- active: true,
- name: "/dev/vda",
- size: 1024,
- systems: ["Windows", "openSUSE Leap 15.2"],
- udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"],
- udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"],
- partitionTable: { type: "gpt", partitions: [] }
-};
-
-const md0 = {
- sid: "62",
- type: "md",
- level: "raid0",
- uuid: "12345:abcde",
- members: ["/dev/vdb"],
- active: true,
- name: "/dev/md0",
- size: 2048,
- systems: [],
- udevIds: [],
- udevPaths: []
-};
-
-const md1 = {
- sid: "63",
- type: "md",
- level: "raid0",
- uuid: "12345:abcde",
- members: ["/dev/vdc"],
- active: true,
- name: "/dev/md1",
- size: 4096,
- systems: [],
- udevIds: [],
- udevPaths: []
-};
-
beforeEach(() => {
props = {};
});
-describe("Installation device field", () => {
- describe("if it is loading", () => {
- beforeEach(() => {
- props.isLoading = true;
- });
-
- describe("and there is no selected device yet", () => {
- beforeEach(() => {
- props.settings = { bootDevice: "" };
- });
-
- it("renders a message indicating that the device is not selected", () => {
- plainRender();
-
- screen.getByText(/Installation device/);
- screen.getByText(/No device selected/);
- });
- });
-
- // FIXME: skipping this test example by now.
- // It will be addressed when reworking the Device section
- // as part of https://github.com/openSUSE/agama/pull/1031
- describe.skip("and there is a selected device", () => {
- beforeEach(() => {
- props.settings = { bootDevice: "/dev/vda" };
- });
-
- it("renders the selected device", () => {
- plainRender();
-
- screen.getByText(/Installation device/);
- screen.getByText("/dev/vda");
- });
- });
- });
-
- describe("if there is no selected device yet", () => {
- beforeEach(() => {
- props.settings = { bootDevice: "" };
- });
-
- it("renders a message indicating that the device is not selected", () => {
- plainRender();
-
- screen.getByText(/Installation device/);
- screen.getByText(/No device selected/);
- });
- });
-
- // FIXME: skipping this test example by now.
- // It will be addressed when reworking the Device section
- // as part of https://github.com/openSUSE/agama/pull/1031
- describe.skip("if there is a selected device", () => {
- beforeEach(() => {
- props.settings = { bootDevice: "/dev/vda" };
- });
-
- it("renders the selected device", () => {
- plainRender();
-
- screen.getByText(/Installation device/);
- screen.getByText("/dev/vda");
- });
- });
-
- it("allows selecting a device when clicking on the device name", async () => {
- props = {
- availableDevices: [vda],
- settings: { bootDevice: "/dev/vda" },
- onChange: jest.fn()
- };
-
- const { user } = plainRender();
-
- const button = screen.getByRole("button", { name: "/dev/vda, 1 KiB" });
- await user.click(button);
-
- const popup = await screen.findByRole("dialog");
- within(popup).getByText("Installation device");
-
- const accept = within(popup).getByRole("button", { name: "Accept" });
- await user.click(accept);
-
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
- expect(props.onChange).toHaveBeenCalled();
- });
-
- it("allows canceling the selection of the device", async () => {
- props = {
- availableDevices: [vda],
- settings: { bootDevice: "/dev/vda" },
- onChange: jest.fn()
- };
-
- const { user } = plainRender();
-
- const button = screen.getByRole("button", { name: "/dev/vda, 1 KiB" });
- await user.click(button);
-
- const popup = await screen.findByRole("dialog");
- within(popup).getByText("Installation device");
-
- const cancel = within(popup).getByRole("button", { name: "Cancel" });
- await user.click(cancel);
-
- expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
- expect(props.onChange).not.toHaveBeenCalled();
- });
-});
-
-describe("LVM field", () => {
- describe("if LVM setting is not set yet", () => {
- beforeEach(() => {
- props.settings = {};
- });
-
- it("does not render the LVM switch", () => {
- plainRender();
-
- expect(screen.queryByLabelText(/Use logical volume/)).toBeNull();
- });
- });
-
- describe("if LVM setting is set", () => {
- beforeEach(() => {
- props.settings = { lvm: false };
- });
-
- it("renders the LVM switch", () => {
- plainRender();
-
- screen.getByRole("checkbox", { name: /Use logical volume/ });
- });
- });
-
- describe("if LVM is set to true", () => {
- beforeEach(() => {
- props.availableDevices = [vda, md0, md1];
- props.settings = { bootDevice: "/dev/vda", lvm: true, systemVGDevices: [] };
- props.onChange = jest.fn();
- });
-
- it("renders the LVM switch as selected", () => {
- plainRender();
-
- const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
- expect(checkbox).toBeChecked();
- });
-
- it("renders a button for changing the LVM settings", () => {
- plainRender();
-
- screen.getByRole("button", { name: /LVM settings/ });
- });
-
- it("changes the selection on click", async () => {
- const { user } = plainRender();
-
- const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
- await user.click(checkbox);
-
- expect(checkbox).not.toBeChecked();
- expect(props.onChange).toHaveBeenCalled();
- });
-
- describe("and user clicks on LVM settings", () => {
- it("opens the LVM settings dialog", async () => {
- const { user } = plainRender();
- const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
-
- await user.click(settingsButton);
-
- const popup = await screen.findByRole("dialog");
- within(popup).getByText("System Volume Group");
- });
-
- it("allows selecting either installation device or custom devices", async () => {
- const { user } = plainRender();
- const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
-
- await user.click(settingsButton);
-
- const popup = await screen.findByRole("dialog");
- screen.getByText("System Volume Group");
-
- within(popup).getByRole("button", { name: "Installation device" });
- within(popup).getByRole("button", { name: "Custom devices" });
- });
-
- it("allows to set the installation device as system volume group", async () => {
- const { user } = plainRender();
- const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
-
- await user.click(settingsButton);
-
- const popup = await screen.findByRole("dialog");
- screen.getByText("System Volume Group");
-
- const bootDeviceButton = within(popup).getByRole("button", { name: "Installation device" });
- const customDevicesButton = within(popup).getByRole("button", { name: "Custom devices" });
- const acceptButton = within(popup).getByRole("button", { name: "Accept" });
-
- await user.click(customDevicesButton);
- await user.click(bootDeviceButton);
- await user.click(acceptButton);
-
- expect(props.onChange).toHaveBeenCalledWith(
- expect.objectContaining({ systemVGDevices: [] })
- );
- });
-
- it("allows customize the system volume group", async () => {
- const { user } = plainRender();
- const settingsButton = screen.getByRole("button", { name: /LVM settings/ });
-
- await user.click(settingsButton);
-
- const popup = await screen.findByRole("dialog");
- screen.getByText("System Volume Group");
-
- const customDevicesButton = within(popup).getByRole("button", { name: "Custom devices" });
- const acceptButton = within(popup).getByRole("button", { name: "Accept" });
-
- await user.click(customDevicesButton);
-
- const vdaOption = within(popup).getByRole("row", { name: /vda/ });
- const md0Option = within(popup).getByRole("row", { name: /md0/ });
- const md1Option = within(popup).getByRole("row", { name: /md1/ });
-
- // unselect the boot devices
- await user.click(vdaOption);
-
- await user.click(md0Option);
- await user.click(md1Option);
-
- await user.click(acceptButton);
-
- expect(props.onChange).toHaveBeenCalledWith(
- expect.objectContaining({ systemVGDevices: ["/dev/md0", "/dev/md1"] })
- );
- });
- });
- });
-
- describe("if LVM is set to false", () => {
- beforeEach(() => {
- props.settings = { lvm: false };
- props.onChange = jest.fn();
- });
-
- it("renders the LVM switch as not selected", () => {
- plainRender();
-
- const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
- expect(checkbox).not.toBeChecked();
- });
-
- it("does not render a button for changing the LVM settings", () => {
- plainRender();
-
- const button = screen.queryByRole("button", { name: /LVM settings/ });
- expect(button).toBeNull();
- });
-
- it("changes the selection on click", async () => {
- const { user } = plainRender();
-
- const checkbox = screen.getByRole("checkbox", { name: /Use logical volume/ });
- await user.click(checkbox);
-
- expect(checkbox).toBeChecked();
- expect(props.onChange).toHaveBeenCalled();
- });
- });
-});
-
describe("Encryption field", () => {
describe("if encryption password setting is not set yet", () => {
beforeEach(() => {
diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx
index 9c2d65fc45..c08135c0e5 100644
--- a/web/src/components/storage/ProposalSpacePolicySection.jsx
+++ b/web/src/components/storage/ProposalSpacePolicySection.jsx
@@ -447,7 +447,13 @@ export default function ProposalSpacePolicySection({
const currentPolicy = SPACE_POLICIES.find(p => p.id === settings.spacePolicy) || SPACE_POLICIES[0];
return (
-
+
}
diff --git a/web/src/components/storage/ProposalVolumes.jsx b/web/src/components/storage/ProposalVolumes.jsx
index d9f16334c0..8b6398b43a 100644
--- a/web/src/components/storage/ProposalVolumes.jsx
+++ b/web/src/components/storage/ProposalVolumes.jsx
@@ -404,9 +404,6 @@ export default function ProposalVolumes({
<>
-
- {_("File systems to create in your system")}
-