From b1d01248453ecb9925c6ed3cc4abf8456de26b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 2 May 2025 13:46:55 +0100 Subject: [PATCH 1/6] feat(web): more detailed warning when proposal fails Provide users with more specific hints about where the problem could be, including cases involving LVM. Note: tests are still pending. --- .../storage/ProposalFailedInfo.test.tsx | 73 +++++++++ .../components/storage/ProposalFailedInfo.tsx | 143 ++++++++++++++---- 2 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 web/src/components/storage/ProposalFailedInfo.test.tsx diff --git a/web/src/components/storage/ProposalFailedInfo.test.tsx b/web/src/components/storage/ProposalFailedInfo.test.tsx new file mode 100644 index 0000000000..b45cb276c1 --- /dev/null +++ b/web/src/components/storage/ProposalFailedInfo.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 { installerRender } from "~/test-utils"; +import ProposalFailedInfo from "./ProposalFailedInfo"; +import { LogicalVolume } from "~/types/storage/data"; + +const mockApiModel = jest.fn(); + +jest.mock("~/hooks/storage/api-model", () => ({ + ...jest.requireActual("~/hooks/storage/api-model"), + useApiModel: () => mockApiModel, +})); + +// eslint-disable-next-line +const fakeLogicalVolume: LogicalVolume = { + // @ts-expect-error: The #name property is used to distinguish new "devices" + // in the API model, but it is not yet exposed for logical volumes since they + // are currently not reusable. This directive exists to ensure developers + // don't overlook updating the ProposalFailedInfo component in the future, + // when logical volumes become reusable and the #name property is exposed. See + // the FIXME in the ProposalFailedInfo component for more context. + name: "Reusable LV", + lvName: "helpful", +}; + +describe("ProposalFailedInfo", () => { + it("renders nothing if there are no config errors", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + + it("renders nothing if there are no storage errors", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + + describe("when there are neither, new partitions nor new logical volumes", () => { + it.todo("renders a generic warning"); + }); + + describe("when there are only new partitions", () => { + it.todo("renders an specific warning refering to partitions"); + }); + + describe("when there are only new logical volumes", () => { + it.todo("renders specific warning refering to volumes"); + }); + + describe("when there are both, new partitions and new logical volumes", () => { + it.todo("renders more generic warning refering to file systems"); + }); +}); diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index 41b8c1e37e..01a2a66439 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -22,43 +22,32 @@ import React from "react"; import { Alert, Content } from "@patternfly/react-core"; -import { _, n_, formatList } from "~/i18n"; -import { useIssues, useConfigErrors } from "~/queries/issues"; -import { useConfigModel } from "~/queries/storage/config-model"; import { IssueSeverity } from "~/types/issues"; +import { useApiModel } from "~/hooks/storage/api-model"; +import { useIssues, useConfigErrors } from "~/queries/issues"; import * as partitionUtils from "~/components/storage/utils/partition"; +import { _, n_, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; -function Description({ partitions, booting }) { - const newPartitions = partitions.filter((p) => !p.name); - - if (!newPartitions.length) { - return ( - - {_( - "It is not possible to install the system with the current configuration. Adjust the settings below.", - )} - - ); - } +const UnallocatablePartitions = ({ partitions, hasBoot }) => { + const mountPaths = partitions.map((p) => partitionUtils.pathWithSize(p)); - const mountPaths = newPartitions.map((p) => partitionUtils.pathWithSize(p)); - const msg1 = booting + return hasBoot ? sprintf( - // TRANSLATORS: %s is a list of formatted mount points with a partition size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' - // (or a single mount point in the singular case). + // TRANSLATORS: %s is a list of formatted mount points with size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a + // single mount point in the singular case). n_( - "It is not possible to allocate the requested partitions for booting and for %s.", + "It is not possible to allocate the requested partition for booting and for %s.", "It is not possible to allocate the requested partitions for booting, %s.", mountPaths.length, ), formatList(mountPaths), ) : sprintf( - // TRANSLATORS: %s is a list of formatted mount points with a partition size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' - // (or a single mount point in the singular case). + // TRANSLATORS: %s is a list of formatted mount points with size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a + // single mount point in the singular case). n_( "It is not possible to allocate the requested partition for %s.", "It is not possible to allocate the requested partitions for %s.", @@ -66,16 +55,112 @@ function Description({ partitions, booting }) { ), formatList(mountPaths), ); +}; + +const UnallocatableVolumes = ({ logicalVolumes, hasBoot }) => { + const mountPaths = logicalVolumes.map((lv) => partitionUtils.pathWithSize(lv)); + + return hasBoot + ? sprintf( + // TRANSLATORS: %s is a list of formatted mount points with size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a + // single mount point in the singular case). + n_( + "It is not possible to allocate the requested partition for booting and volume for %s.", + "It is not possible to allocate the requested partition for booting and volumes %s.", + mountPaths.length, + ), + formatList(mountPaths), + ) + : sprintf( + // TRANSLATORS: %s is a list of formatted mount points with size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a + // single mount point in the singular case). + n_( + "It is not possible to allocate the requested volume for %s.", + "It is not possible to allocate the requested volumes for %s.", + mountPaths.length, + ), + formatList(mountPaths), + ); +}; + +const UnallocatableFileSystems = ({ devices, hasBoot }) => { + const mountPaths = devices.map((d) => partitionUtils.pathWithSize(d)); + + return hasBoot + ? sprintf( + // TRANSLATORS: %s is a list of formatted mount points with size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a + // single mount point in the singular case). + n_( + "It is not possible to allocate the requested partition for booting and file system for %s.", + "It is not possible to allocate the requested partition for booting and file systems %s.", + mountPaths.length, + ), + formatList(mountPaths), + ) + : sprintf( + // TRANSLATORS: %s is a list of formatted mount points with size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a + // single mount point in the singular case). + n_( + "It is not possible to allocate the requested file system for %s.", + "It is not possible to allocate the requested file systems for %s.", + mountPaths.length, + ), + formatList(mountPaths), + ); +}; + +const Description = () => { + const model = useApiModel({ suspense: true }); + const partitions = model.drives.flatMap((d) => d.partitions || []); + const logicalVolumes = model.volumeGroups.flatMap((vg) => vg.logicalVolumes || []); + + const newPartitions = partitions.filter((p) => !p.name); + // FIXME: Currently, it's not possible to reuse a logical volume, so all + // volumes are treated as new. This code cannot be made future-proof due to an + // internal decision not to expose unused properties, even though "#name" is + // used to infer whether a "device" is new or not. + // const newLogicalVolumes = logicalVolumes.filter((lv) => !lv.name); + + const hasBoot = !!model.boot?.configure; + const hasPartitions = newPartitions.length !== 0; + const hasVolumes = logicalVolumes.length !== 0; + + if (!hasPartitions && !hasVolumes) { + return ( + + {_( + "It is not possible to install the system with the current configuration. Adjust the settings below.", + )} + + ); + } return ( <> - {msg1} + + {hasPartitions && !hasVolumes && ( + + )} + {!hasPartitions && hasVolumes && ( + + )} + {hasPartitions && hasVolumes && ( + + )} + {_("Adjust the settings below to make the new system fit into the available space.")} ); -} +}; /** * Information about a failed storage proposal @@ -84,17 +169,13 @@ function Description({ partitions, booting }) { export default function ProposalFailedInfo() { const configErrors = useConfigErrors("storage"); const errors = useIssues("storage").filter((s) => s.severity === IssueSeverity.Error); - const model = useConfigModel({ suspense: true }); if (configErrors.length) return; if (!errors.length) return; - const modelPartitions = model.drives.flatMap((d) => d.partitions || []); - const booting = !!model.boot?.configure; - return ( - + ); } From 1322da58cdc59ae591c463107071316d3ddc84e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 2 May 2025 15:43:30 +0100 Subject: [PATCH 2/6] feat(web): simplify proposal failed alert message Use a single, concise explanation to describe failures in allocating space for mount points or file systems. This reduces code complexity and avoids overly technical language for what is essentially the same underlying issue. --- .../components/storage/ProposalFailedInfo.tsx | 120 +++--------------- 1 file changed, 19 insertions(+), 101 deletions(-) diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index 01a2a66439..9b019c705e 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -26,110 +26,28 @@ import { IssueSeverity } from "~/types/issues"; import { useApiModel } from "~/hooks/storage/api-model"; import { useIssues, useConfigErrors } from "~/queries/issues"; import * as partitionUtils from "~/components/storage/utils/partition"; -import { _, n_, formatList } from "~/i18n"; +import { _, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; -const UnallocatablePartitions = ({ partitions, hasBoot }) => { - const mountPaths = partitions.map((p) => partitionUtils.pathWithSize(p)); - - return hasBoot - ? sprintf( - // TRANSLATORS: %s is a list of formatted mount points with size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a - // single mount point in the singular case). - n_( - "It is not possible to allocate the requested partition for booting and for %s.", - "It is not possible to allocate the requested partitions for booting, %s.", - mountPaths.length, - ), - formatList(mountPaths), - ) - : sprintf( - // TRANSLATORS: %s is a list of formatted mount points with size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a - // single mount point in the singular case). - n_( - "It is not possible to allocate the requested partition for %s.", - "It is not possible to allocate the requested partitions for %s.", - mountPaths.length, - ), - formatList(mountPaths), - ); -}; - -const UnallocatableVolumes = ({ logicalVolumes, hasBoot }) => { - const mountPaths = logicalVolumes.map((lv) => partitionUtils.pathWithSize(lv)); - - return hasBoot - ? sprintf( - // TRANSLATORS: %s is a list of formatted mount points with size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a - // single mount point in the singular case). - n_( - "It is not possible to allocate the requested partition for booting and volume for %s.", - "It is not possible to allocate the requested partition for booting and volumes %s.", - mountPaths.length, - ), - formatList(mountPaths), - ) - : sprintf( - // TRANSLATORS: %s is a list of formatted mount points with size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a - // single mount point in the singular case). - n_( - "It is not possible to allocate the requested volume for %s.", - "It is not possible to allocate the requested volumes for %s.", - mountPaths.length, - ), - formatList(mountPaths), - ); -}; - -const UnallocatableFileSystems = ({ devices, hasBoot }) => { - const mountPaths = devices.map((d) => partitionUtils.pathWithSize(d)); - - return hasBoot - ? sprintf( - // TRANSLATORS: %s is a list of formatted mount points with size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a - // single mount point in the singular case). - n_( - "It is not possible to allocate the requested partition for booting and file system for %s.", - "It is not possible to allocate the requested partition for booting and file systems %s.", - mountPaths.length, - ), - formatList(mountPaths), - ) - : sprintf( - // TRANSLATORS: %s is a list of formatted mount points with size like - // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' (or a - // single mount point in the singular case). - n_( - "It is not possible to allocate the requested file system for %s.", - "It is not possible to allocate the requested file systems for %s.", - mountPaths.length, - ), - formatList(mountPaths), - ); -}; - const Description = () => { const model = useApiModel({ suspense: true }); const partitions = model.drives.flatMap((d) => d.partitions || []); const logicalVolumes = model.volumeGroups.flatMap((vg) => vg.logicalVolumes || []); const newPartitions = partitions.filter((p) => !p.name); + // FIXME: Currently, it's not possible to reuse a logical volume, so all // volumes are treated as new. This code cannot be made future-proof due to an // internal decision not to expose unused properties, even though "#name" is // used to infer whether a "device" is new or not. // const newLogicalVolumes = logicalVolumes.filter((lv) => !lv.name); - const hasBoot = !!model.boot?.configure; - const hasPartitions = newPartitions.length !== 0; - const hasVolumes = logicalVolumes.length !== 0; + const isBootConfigured = !!model.boot?.configure; + const mountPaths = [newPartitions, logicalVolumes] + .flat() + .map((d) => partitionUtils.pathWithSize(d)); - if (!hasPartitions && !hasVolumes) { + if (mountPaths.length === 0) { return ( {_( @@ -142,17 +60,12 @@ const Description = () => { return ( <> - {hasPartitions && !hasVolumes && ( - - )} - {!hasPartitions && hasVolumes && ( - - )} - {hasPartitions && hasVolumes && ( - + {sprintf( + // TRANSLATORS: %s is a list that includes "boot partition" and one or + // more formatted mount points with size like: '"/" (at least 10 GiB), + // "/var" (20 GiB), and "swap" (2 GiB)'. + _("It is not possible to allocate space for %s."), + formatList(isBootConfigured ? [_("boot partition"), ...mountPaths] : mountPaths), )} @@ -163,8 +76,13 @@ const Description = () => { }; /** - * Information about a failed storage proposal + * Displays information to help users understand why a storage proposal + * could not be generated with the current configuration. * + * Renders nothing if: + * - The proposal could not be generated at all (known by the presense of + * configuration errors in the storage scope) + * - The generated proposal contains no errors. */ export default function ProposalFailedInfo() { const configErrors = useConfigErrors("storage"); From b6ed0fc6e6a6982d69776863a63c72d36662bbe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Sun, 4 May 2025 23:53:48 +0100 Subject: [PATCH 3/6] feat(web): add unit testing for ProposalFailedInfo --- .../storage/ProposalFailedInfo.test.tsx | 151 +++++++++++++++--- .../components/storage/ProposalFailedInfo.tsx | 4 +- 2 files changed, 135 insertions(+), 20 deletions(-) diff --git a/web/src/components/storage/ProposalFailedInfo.test.tsx b/web/src/components/storage/ProposalFailedInfo.test.tsx index b45cb276c1..97f70c1ca9 100644 --- a/web/src/components/storage/ProposalFailedInfo.test.tsx +++ b/web/src/components/storage/ProposalFailedInfo.test.tsx @@ -21,17 +21,116 @@ */ import React from "react"; +import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProposalFailedInfo from "./ProposalFailedInfo"; import { LogicalVolume } from "~/types/storage/data"; +import { Issue, IssueSeverity, IssueSource } from "~/types/issues"; +import { apiModel } from "~/api/storage/types"; -const mockApiModel = jest.fn(); +const mockUseConfigErrorsFn = jest.fn(); +let mockUseIssues = []; + +const configError: Issue = { + description: "Config error", + kind: "storage", + details: "", + source: 2, + severity: 1, +}; + +const storageIssue: Issue = { + description: "Fake Storage Issue", + details: "", + kind: "storage_issue", + source: IssueSource.Unknown, + severity: IssueSeverity.Error, +}; + +const mockApiModel: apiModel.Config = { + boot: { + configure: false, + }, + drives: [ + { + name: "/dev/vdb", + spacePolicy: "delete", + partitions: [ + { + mountPath: "/", + filesystem: { + reuse: false, + default: true, + type: "btrfs", + snapshots: true, + }, + size: { + default: true, + min: 13421772800, + }, + delete: false, + deleteIfNeeded: false, + resize: false, + resizeIfNeeded: false, + }, + { + mountPath: "swap", + filesystem: { + reuse: false, + default: true, + type: "swap", + }, + size: { + default: true, + min: 1073741824, + max: 2147483648, + }, + delete: false, + deleteIfNeeded: false, + resize: false, + resizeIfNeeded: false, + }, + { + name: "/dev/vdb1", + size: { + default: true, + min: 6430916608, + max: 6430916608, + }, + delete: true, + deleteIfNeeded: false, + resize: false, + resizeIfNeeded: false, + }, + { + name: "/dev/vdb2", + size: { + default: true, + min: 4305436160, + max: 4305436160, + }, + delete: true, + deleteIfNeeded: false, + resize: false, + resizeIfNeeded: false, + }, + ], + }, + ], + volumeGroups: [], +}; jest.mock("~/hooks/storage/api-model", () => ({ ...jest.requireActual("~/hooks/storage/api-model"), useApiModel: () => mockApiModel, })); +jest.mock("~/queries/issues", () => ({ + ...jest.requireActual("~/queries/issues"), + useConfigErrors: () => mockUseConfigErrorsFn(), + useIssues: () => mockUseIssues, +})); + // eslint-disable-next-line const fakeLogicalVolume: LogicalVolume = { // @ts-expect-error: The #name property is used to distinguish new "devices" @@ -45,29 +144,45 @@ const fakeLogicalVolume: LogicalVolume = { }; describe("ProposalFailedInfo", () => { - it("renders nothing if there are no config errors", () => { - const { container } = installerRender(); - expect(container).toBeEmptyDOMElement(); + beforeEach(() => { + mockUseIssues = []; + mockUseConfigErrorsFn.mockReturnValue([]); }); - it("renders nothing if there are no storage errors", () => { - const { container } = installerRender(); - expect(container).toBeEmptyDOMElement(); - }); + describe("when proposal can't be created due to configuration errors", () => { + beforeEach(() => { + mockUseConfigErrorsFn.mockReturnValue([configError]); + }); - describe("when there are neither, new partitions nor new logical volumes", () => { - it.todo("renders a generic warning"); + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); }); - describe("when there are only new partitions", () => { - it.todo("renders an specific warning refering to partitions"); - }); + describe("when proposal is valid", () => { + describe("and has no errors", () => { + beforeEach(() => { + mockUseIssues = []; + }); - describe("when there are only new logical volumes", () => { - it.todo("renders specific warning refering to volumes"); - }); + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("but has errors", () => { + beforeEach(() => { + mockUseIssues = [storageIssue]; + }); - describe("when there are both, new partitions and new logical volumes", () => { - it.todo("renders more generic warning refering to file systems"); + it("renders a warning alert with hints about the failure", () => { + installerRender(); + screen.getByText("Warning alert:"); + screen.getByText("Failed to calculate a storage layout"); + screen.getByText(/It is not possible to allocate space for/); + }); + }); }); }); diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index 9b019c705e..76e8462806 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -88,8 +88,8 @@ export default function ProposalFailedInfo() { const configErrors = useConfigErrors("storage"); const errors = useIssues("storage").filter((s) => s.severity === IssueSeverity.Error); - if (configErrors.length) return; - if (!errors.length) return; + if (configErrors.length !== 0) return; + if (errors.length === 0) return; return ( From 9c9ba43a809054bf60dd207abd411d68ce240b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 5 May 2025 15:46:51 +0100 Subject: [PATCH 4/6] fix(web): improvements from code review Fixes a typo and make translations less challenging by avoiding including "boot partition" as an element of the mount paths list. Co-Authored-By: Ancor Gonzalez Sosa --- web/src/components/storage/ProposalFailedInfo.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index 76e8462806..8620d65752 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -61,11 +61,14 @@ const Description = () => { <> {sprintf( - // TRANSLATORS: %s is a list that includes "boot partition" and one or - // more formatted mount points with size like: '"/" (at least 10 GiB), - // "/var" (20 GiB), and "swap" (2 GiB)'. - _("It is not possible to allocate space for %s."), - formatList(isBootConfigured ? [_("boot partition"), ...mountPaths] : mountPaths), + isBootConfigured + ? // TRANSLATORS: %s is a list of formatted mount points with a partition size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' + _("It is not possible to allocate space for the boot partition and for %s.") + : // TRANSLATORS: %s is a list of formatted mount points with a partition size like + // '"/" (at least 10 GiB), "/var" (20 GiB) and "swap" (2 GiB)' + _("It is not possible to allocate space for %s."), + formatList(mountPaths), )} @@ -80,7 +83,7 @@ const Description = () => { * could not be generated with the current configuration. * * Renders nothing if: - * - The proposal could not be generated at all (known by the presense of + * - The proposal could not be generated at all (known by the presence of * configuration errors in the storage scope) * - The generated proposal contains no errors. */ From 27768366a24a28d9b4c0bbe9e58bf52da8e1708c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 5 May 2025 15:53:42 +0100 Subject: [PATCH 5/6] doc(web): add entry to changelog --- web/package/agama-web-ui.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index a197659bd0..ebc4711a3b 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon May 5 14:50:48 UTC 2025 - David Diaz + +- Improve storage proposal warning to mention mount paths when + logical volume space allocation fails (gh#agama-project/agama#2323). + ------------------------------------------------------------------- Tue Apr 22 14:14:53 UTC 2025 - Imobach Gonzalez Sosa From 206941b03860cc64994239f6773603f634aa8bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 5 May 2025 16:35:55 +0100 Subject: [PATCH 6/6] fix(web): use richer mocking in proposal failed info test --- .../storage/ProposalFailedInfo.test.tsx | 86 ++++++++++++------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/web/src/components/storage/ProposalFailedInfo.test.tsx b/web/src/components/storage/ProposalFailedInfo.test.tsx index 97f70c1ca9..80e648210b 100644 --- a/web/src/components/storage/ProposalFailedInfo.test.tsx +++ b/web/src/components/storage/ProposalFailedInfo.test.tsx @@ -49,7 +49,11 @@ const storageIssue: Issue = { const mockApiModel: apiModel.Config = { boot: { - configure: false, + configure: true, + device: { + default: true, + name: "/dev/vdb", + }, }, drives: [ { @@ -57,67 +61,91 @@ const mockApiModel: apiModel.Config = { spacePolicy: "delete", partitions: [ { - mountPath: "/", - filesystem: { - reuse: false, + name: "/dev/vdb1", + size: { default: true, - type: "btrfs", - snapshots: true, + min: 6430916608, + max: 6430916608, }, + delete: true, + deleteIfNeeded: false, + resize: false, + resizeIfNeeded: false, + }, + { + name: "/dev/vdb2", size: { default: true, - min: 13421772800, + min: 4305436160, + max: 4305436160, }, - delete: false, + delete: true, deleteIfNeeded: false, resize: false, resizeIfNeeded: false, }, + ], + }, + { + name: "/dev/vdc", + spacePolicy: "delete", + partitions: [ { - mountPath: "swap", + mountPath: "/documents", filesystem: { reuse: false, - default: true, - type: "swap", + default: false, + type: "xfs", + label: "", }, size: { - default: true, - min: 1073741824, - max: 2147483648, + default: false, + min: 136365211648, }, delete: false, deleteIfNeeded: false, resize: false, resizeIfNeeded: false, }, + ], + }, + ], + volumeGroups: [ + { + vgName: "system", + targetDevices: ["/dev/vdb"], + logicalVolumes: [ { - name: "/dev/vdb1", + lvName: "root", + mountPath: "/", + filesystem: { + reuse: false, + default: true, + type: "btrfs", + snapshots: true, + }, size: { default: true, - min: 6430916608, - max: 6430916608, + min: 13421772800, }, - delete: true, - deleteIfNeeded: false, - resize: false, - resizeIfNeeded: false, }, { - name: "/dev/vdb2", + lvName: "swap", + mountPath: "swap", + filesystem: { + reuse: false, + default: true, + type: "swap", + }, size: { default: true, - min: 4305436160, - max: 4305436160, + min: 1073741824, + max: 2147483648, }, - delete: true, - deleteIfNeeded: false, - resize: false, - resizeIfNeeded: false, }, ], }, ], - volumeGroups: [], }; jest.mock("~/hooks/storage/api-model", () => ({