From 5beec0ef2c4c6d968e195e4587c5979d3465f664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 27 Feb 2026 15:46:45 +0000 Subject: [PATCH 01/13] feat(web): allow injecting additional content into ProgressBackdrop Add an `extraContent` prop so consumers can render additional UI below the progress information in the backdrop overlay, such as per-device DASD formatting details driven by websocket events. --- .../components/core/ProgressBackdrop.test.tsx | 26 +++++++++++++++++-- web/src/components/core/ProgressBackdrop.tsx | 22 +++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/web/src/components/core/ProgressBackdrop.test.tsx b/web/src/components/core/ProgressBackdrop.test.tsx index c15ca8e10f..57ba45532b 100644 --- a/web/src/components/core/ProgressBackdrop.test.tsx +++ b/web/src/components/core/ProgressBackdrop.test.tsx @@ -21,7 +21,7 @@ */ import React from "react"; -import { screen, waitFor, within } from "@testing-library/react"; +import { act, screen, waitFor, within } from "@testing-library/react"; import { installerRender, mockProgresses } from "~/test-utils"; import useTrackQueriesRefetch from "~/hooks/use-track-queries-refetch"; import { COMMON_PROPOSAL_KEYS } from "~/hooks/model/proposal"; @@ -143,7 +143,9 @@ describe("ProgressBackdrop", () => { // Simulate queries completing by calling the callback const startedAt = Date.now(); - mockCallback(startedAt, startedAt + 100); + act(() => { + mockCallback(startedAt, startedAt + 100); + }); // Backdrop should be hidden await waitFor(() => { @@ -289,4 +291,24 @@ describe("ProgressBackdrop", () => { }); }); }); + describe("when extraContent is provided", () => { + it("renders the extra content below the progress information", () => { + mockProgresses([ + { + scope: "software", + step: "Installing packages", + steps: [], + index: 1, + size: 3, + }, + ]); + + installerRender( + Extra content} />, + ); + + const backdrop = screen.getByRole("alert", { name: /Installing packages/ }); + within(backdrop).getByText("Extra content"); + }); + }); }); diff --git a/web/src/components/core/ProgressBackdrop.tsx b/web/src/components/core/ProgressBackdrop.tsx index 0e4ced567e..2e85f923a3 100644 --- a/web/src/components/core/ProgressBackdrop.tsx +++ b/web/src/components/core/ProgressBackdrop.tsx @@ -21,15 +21,17 @@ */ import React from "react"; -import { Alert, Backdrop, Flex, FlexItem, Spinner } from "@patternfly/react-core"; import { concat } from "radashi"; import { sprintf } from "sprintf-js"; +import { Alert, Backdrop, Flex, FlexItem, Spinner } from "@patternfly/react-core"; +import NestedContent from "~/components/core/NestedContent"; import { COMMON_PROPOSAL_KEYS } from "~/hooks/model/proposal"; -import type { Scope } from "~/model/status"; -import { _ } from "~/i18n"; -import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { useProgressTracking } from "~/hooks/use-progress-tracking"; +import { _ } from "~/i18n"; + +import type { Scope } from "~/model/status"; +import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; /** * Props for the ProgressBackdrop component. */ @@ -64,6 +66,16 @@ export type ProgressBackdropProps = { * > */ ensureRefetched?: string | string[]; + /** + * Additional content to render below the progress information. + * + * Use this to display extra UI within the backdrop overlay, such as + * per-device progress details for long-running operations. + * + * @example + * } /> + */ + extraContent?: React.ReactNode; }; /** @@ -84,6 +96,7 @@ export type ProgressBackdropProps = { export default function ProgressBackdrop({ scope, ensureRefetched, + extraContent, }: ProgressBackdropProps): React.ReactNode { const { loading: isBlocked, progress } = useProgressTracking( scope, @@ -118,6 +131,7 @@ export default function ProgressBackdrop({ } /> + {extraContent && {extraContent}} ); } From 63b7ba163a9b55334e605cc465f046fd085b656c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 2 Mar 2026 13:15:52 +0000 Subject: [PATCH 02/13] reafactor(web): adapt DASDFormatProgress to API v2 event model Use DASDFormatChanged events to drive progress state and render progress card only when formatting is active. --- .../storage/dasd/DASDFormatProgress.test.tsx | 167 ++++++++++-------- .../storage/dasd/DASDFormatProgress.tsx | 103 +++++++---- 2 files changed, 166 insertions(+), 104 deletions(-) diff --git a/web/src/components/storage/dasd/DASDFormatProgress.test.tsx b/web/src/components/storage/dasd/DASDFormatProgress.test.tsx index 9815d64aca..581a693259 100644 --- a/web/src/components/storage/dasd/DASDFormatProgress.test.tsx +++ b/web/src/components/storage/dasd/DASDFormatProgress.test.tsx @@ -21,89 +21,112 @@ */ import React from "react"; -import { screen } from "@testing-library/react"; +import { screen, act } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import type { Device } from "~/model/system/dasd"; - import DASDFormatProgress from "./DASDFormatProgress"; -// FIXME: adapt to new API -type FormatSummary = { - total: number; - step: number; - done: boolean; -}; - -type FormatJob = { - jobId: string; - summary?: { [key: string]: FormatSummary }; -}; - -/* eslint-disable @typescript-eslint/no-unused-vars */ -let mockDASDFormatJobs: FormatJob[]; -let mockDASDDevices: Device[]; - -// Skipped during migration to v2 -describe.skip("DASDFormatProgress", () => { - describe("when there is already some progress", () => { - beforeEach(() => { - mockDASDFormatJobs = [ - { - jobId: "0.0.0200", - summary: { - "0.0.0200": { - total: 5, - step: 1, - done: false, - }, - }, - }, - ]; - - mockDASDDevices = [ - { - channel: "0.0.0200", - active: false, - deviceName: "dasda", - type: "eckd", - formatted: false, - diag: false, - status: "active", - accessType: "rw", - partitionInfo: "1", - }, - ]; +const mockOnEvent = jest.fn(); + +jest.mock("~/context/installer", () => ({ + useInstallerClient: () => ({ + onEvent: mockOnEvent, + }), +})); + +describe("DASDFormatProgress", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("when there are no signal progress events", () => { + it("renders nothing", () => { + mockOnEvent.mockImplementation(() => jest.fn()); + + const { container } = installerRender(); + + expect(container).toBeEmptyDOMElement(); }); + }); - it("renders the progress", () => { - installerRender(); - expect(screen.queryByRole("progressbar")).toBeInTheDocument(); - screen.getByText("0.0.0200 - dasda"); + describe("when signal contains empty summary", () => { + it("renders nothing", () => { + let eventCallback: (event: unknown) => void; + + mockOnEvent.mockImplementation((cb) => { + eventCallback = cb; + return jest.fn(); + }); + + const { container } = installerRender(); + + act(() => { + eventCallback({ + type: "DASDFormatChanged", + summary: [], + }); + }); + + expect(container).toBeEmptyDOMElement(); }); }); - describe("when there are no running jobs", () => { - beforeEach(() => { - mockDASDFormatJobs = []; - - mockDASDDevices = [ - { - channel: "0.0.0200", - active: false, - deviceName: "dasda", - type: "eckd", - formatted: false, - diag: false, - status: "active", - accessType: "rw", - partitionInfo: "1", - }, - ]; + describe("when there is progress", () => { + it("renders progress bars after DASDFormatChanged event", () => { + let eventCallback: (event: unknown) => void; + + mockOnEvent.mockImplementation((cb) => { + eventCallback = cb; + return jest.fn(); + }); + + installerRender(); + + act(() => { + eventCallback({ + type: "DASDFormatChanged", + summary: [ + { + channel: "0.0.0200", + totalCylinders: 5, + formattedCylinders: 1, + finished: false, + }, + ], + }); + }); + + screen.getByRole("progressbar"); + screen.getByText("0.0.0200"); }); + }); + + describe("when progress is finished", () => { + it("renders progress bar with success variant", () => { + let eventCallback: (event: unknown) => void; + + mockOnEvent.mockImplementation((cb) => { + eventCallback = cb; + return jest.fn(); + }); - it("does not render any progress", () => { installerRender(); - expect(screen.queryByRole("progressbar")).not.toBeInTheDocument(); + + act(() => { + eventCallback({ + type: "DASDFormatChanged", + summary: [ + { + channel: "0.0.0200", + totalCylinders: 5, + formattedCylinders: 5, + finished: true, + }, + ], + }); + }); + + const progressbar = screen.getByRole("progressbar"); + expect(progressbar.closest(".pf-v6-c-progress")).toHaveClass("pf-m-success"); }); }); }); diff --git a/web/src/components/storage/dasd/DASDFormatProgress.tsx b/web/src/components/storage/dasd/DASDFormatProgress.tsx index 7877781f7b..23b20ea71d 100644 --- a/web/src/components/storage/dasd/DASDFormatProgress.tsx +++ b/web/src/components/storage/dasd/DASDFormatProgress.tsx @@ -20,53 +20,92 @@ * find current contact information at www.suse.com. */ -import React from "react"; -import { Progress, Stack } from "@patternfly/react-core"; -import { Popup } from "~/components/core"; +import React, { useEffect, useState } from "react"; +import { Card, CardBody, CardTitle, Progress, Stack } from "@patternfly/react-core"; +import { useInstallerClient } from "~/context/installer"; import { _ } from "~/i18n"; import type { Device } from "~/model/system/dasd"; -// FIXME: adapt to new API +/** + * Summary of an ongoing DASD formatting operation for a single device. + * + * It is received from the installer client via the `DASDFormatChanged` event + * and represents the current formatting state of one DASD device. + */ type FormatSummary = { - total: number; - step: number; - done: boolean; -}; - -type FormatJob = { - jobId: string; - summary?: { [key: string]: FormatSummary }; + /** + * The channel identifier of the DASD device (e.g. "0.0.0200"). + */ + channel: Device["channel"]; + /** + * Total number of cylinders to be formatted. + */ + totalCylinders: number; + /** + * Number of cylinders that have already been formatted. + */ + formattedCylinders: number; + /** + * Whether the formatting operation has completed. + */ + finished: boolean; }; -const DeviceProgress = ({ device, progress }: { device: Device; progress: FormatSummary }) => ( +/** + * Renders a small progress bar for a single DASD formatting operation. + */ +const DeviceProgress = ({ progress }: { progress: FormatSummary }) => ( ); +/** + * Displays progress information for currently running DASD format operations. + * + * The component subscribes to the installer client's `DASDFormatChanged` events + * and updates its internal state accordingly. + * + * Rendering behavior: + * - If no formatting operations are running, nothing is rendered. + * - If at least one formatting summary is present, a progress card is shown. + * + * The component automatically unsubscribes from installer events when unmounted. + */ export default function DASDFormatProgress() { - const devices = []; // FIXME: use APIv2 equivalent to useDASDDevices(); - const runningJobs: FormatJob[] = []; // FIXME use APIv2 equivalent to useDASDRunningFormatJobs() + const client = useInstallerClient(); + const [progress, setProgress] = useState([]); + + useEffect(() => { + if (!client) return; + + return client.onEvent((event) => { + if (event.type === "DASDFormatChanged") { + setProgress(event.summary); + } + }); + }, [client]); + + if (progress.length === 0) { + return null; + } return ( - 0} disableFocusTrap> - - {runningJobs.map((job) => - Object.entries(job.summary).map(([id, progress]) => { - const device = devices.find((d) => d.id === id); - return ( - - ); - }), - )} - - + + {_("Formatting devices progress")} + + + {progress.map((p) => { + return ; + })} + + + ); } From 085ccc7c383fe49056d96b1c925e9fd18e1a29b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 2 Mar 2026 13:17:31 +0000 Subject: [PATCH 03/13] feat(web): inject DASDFormatProgress in DASDPage progess --- web/src/components/storage/dasd/DASDPage.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/components/storage/dasd/DASDPage.tsx b/web/src/components/storage/dasd/DASDPage.tsx index 9a0ba9c275..06adaf2eeb 100644 --- a/web/src/components/storage/dasd/DASDPage.tsx +++ b/web/src/components/storage/dasd/DASDPage.tsx @@ -24,7 +24,8 @@ import React from "react"; import { isEmpty } from "radashi"; import { EmptyState, EmptyStateBody } from "@patternfly/react-core"; import Page from "~/components/core/Page"; -import DASDTable from "./DASDTable"; +import DASDTable from "~/components/storage/dasd/DASDTable"; +import DASDFormatProgress from "~/components/storage/dasd/DASDFormatProgress"; import { useSystem } from "~/hooks/model/system/dasd"; import { STORAGE } from "~/routes/paths"; import { _ } from "~/i18n"; @@ -67,7 +68,10 @@ export default function DASDPage() { return ( , + }} > From 29d9a422ab1e7eb6a789a770667715e337390d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 2 Mar 2026 13:33:45 +0000 Subject: [PATCH 04/13] refactor(web): update progress backdrop layout to improve readability Refactors the ProgressBackdrop component to improve readability by wrapping the progress indicator and extra content in a Card layout. This provides a clear visual container, structured spacing, and scrollable content for long messages, making progress steps easier to read and follow. The overlay still subtly dims the background, ensuring focus remains on the progress information without obscuring the underlying page. --- web/src/assets/styles/index.scss | 8 +- web/src/components/core/ProgressBackdrop.tsx | 99 ++++++++++++++------ 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/web/src/assets/styles/index.scss b/web/src/assets/styles/index.scss index 60b0db257b..0868f18c29 100644 --- a/web/src/assets/styles/index.scss +++ b/web/src/assets/styles/index.scss @@ -188,14 +188,18 @@ strong { overflow: hidden; } +// ProgressBackdrop overlay styles .pf-v6-c-page__main-container:has(.agm-main-content-overlay) { position: relative; .agm-main-content-overlay { position: absolute; - padding-block-start: 2.2rem; backdrop-filter: blur(2px); - background-color: color-mix(in srgb, var(--agm-t--color--fog) 50%, transparent); + background-color: color-mix( + in srgb, + var(--pf-t--global--background--color--backdrop--default) 80%, + transparent + ); } } diff --git a/web/src/components/core/ProgressBackdrop.tsx b/web/src/components/core/ProgressBackdrop.tsx index 2e85f923a3..6bdcc4ac16 100644 --- a/web/src/components/core/ProgressBackdrop.tsx +++ b/web/src/components/core/ProgressBackdrop.tsx @@ -23,7 +23,16 @@ import React from "react"; import { concat } from "radashi"; import { sprintf } from "sprintf-js"; -import { Alert, Backdrop, Flex, FlexItem, Spinner } from "@patternfly/react-core"; +import { + Alert, + Backdrop, + Card, + CardBody, + CardTitle, + Flex, + FlexItem, + Spinner, +} from "@patternfly/react-core"; import NestedContent from "~/components/core/NestedContent"; import { COMMON_PROPOSAL_KEYS } from "~/hooks/model/proposal"; import { useProgressTracking } from "~/hooks/use-progress-tracking"; @@ -32,6 +41,10 @@ import { _ } from "~/i18n"; import type { Scope } from "~/model/status"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; +import sizingStyles from "@patternfly/react-styles/css/utilities/Sizing/sizing"; +import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import shadowStyles from "@patternfly/react-styles/css/utilities/BoxShadow/box-shadow"; + /** * Props for the ProgressBackdrop component. */ @@ -42,7 +55,6 @@ export type ProgressBackdropProps = { * displayed. */ scope: Scope; - /** * Additional query keys to track during progress operations. * @@ -106,32 +118,63 @@ export default function ProgressBackdrop({ if (!isBlocked) return null; return ( - - } - title={ - - - - {progress ? ( - <> - {progress.step}{" "} - {sprintf(_("(step %s of %s)"), progress.index, progress.size)} - - ) : ( - <>{_("Refreshing data...")} - )} - - - } - /> - {extraContent && {extraContent}} + + + + + } + title={ + + + + {progress ? ( + <> + {progress.step}{" "} + + {sprintf(_("(step %s of %s)"), progress.index, progress.size)} + + + ) : ( + <>{_("Refreshing data...")} + )} + + + } + /> + + + {extraContent && {extraContent}} + + + ); } From 27a0311b4bf5172979a5902cf2cc7cdbc77669f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 2 Mar 2026 13:59:00 +0000 Subject: [PATCH 05/13] feat(web): allow customizing the ProgressBackdrop waiting label Add a `waitingLabel` prop to ProgressBackdrop so consumers can override the default "Refreshing data..." message shown while queries finish refetching after an operation completes. --- .../components/core/ProgressBackdrop.test.tsx | 29 ++++++++++++++++++- web/src/components/core/ProgressBackdrop.tsx | 15 +++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/web/src/components/core/ProgressBackdrop.test.tsx b/web/src/components/core/ProgressBackdrop.test.tsx index 57ba45532b..0ea4417434 100644 --- a/web/src/components/core/ProgressBackdrop.test.tsx +++ b/web/src/components/core/ProgressBackdrop.test.tsx @@ -85,7 +85,7 @@ describe("ProgressBackdrop", () => { }); }); - it("shows 'Refreshing data...' message temporarily", async () => { + it("shows 'Refreshing data...' message temporarily by default", async () => { // Start with active progress mockProgresses([ { @@ -115,6 +115,33 @@ describe("ProgressBackdrop", () => { expect(mockStartTracking).toHaveBeenCalled(); }); + it("shows custom `waitingLabel` when provided", async () => { + mockProgresses([ + { + scope: "storage", + step: "Calculating proposal", + steps: ["Calculating proposal"], + index: 1, + size: 1, + }, + ]); + + const { rerender } = installerRender( + , + ); + + const backdrop = screen.getByRole("alert", { name: /Calculating proposal/ }); + + mockProgresses([]); + rerender(); + + await waitFor(() => { + within(backdrop).getByText(/Applying storage settings/); + }); + + expect(within(backdrop).queryByText(/Refreshing data/)).toBeNull(); + }); + it("hides backdrop after queries are refetched", async () => { // Start with active progress mockProgresses([ diff --git a/web/src/components/core/ProgressBackdrop.tsx b/web/src/components/core/ProgressBackdrop.tsx index 6bdcc4ac16..97d4aee9b1 100644 --- a/web/src/components/core/ProgressBackdrop.tsx +++ b/web/src/components/core/ProgressBackdrop.tsx @@ -88,6 +88,16 @@ export type ProgressBackdropProps = { * } /> */ extraContent?: React.ReactNode; + /** + * Label displayed when no active progress step is available but the backdrop + * is still visible because queries have not finished refetching yet. + * + * Defaults to `"Refreshing data..."` if not provided. + * + * @example + * + */ + waitingLabel?: string; }; /** @@ -109,6 +119,9 @@ export default function ProgressBackdrop({ scope, ensureRefetched, extraContent, + // TRANSLATORS: Message shown next to a spinner while the UI is being updated + // after an operation has completed. + waitingLabel = _("Refreshing data..."), }: ProgressBackdropProps): React.ReactNode { const { loading: isBlocked, progress } = useProgressTracking( scope, @@ -163,7 +176,7 @@ export default function ProgressBackdrop({ ) : ( - <>{_("Refreshing data...")} + <>{waitingLabel} )} From 262216b61330cd7149be51533c4846f098669c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 2 Mar 2026 15:45:55 +0000 Subject: [PATCH 06/13] fix(web): minor CSS improvements --- web/src/components/core/ProgressBackdrop.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/components/core/ProgressBackdrop.tsx b/web/src/components/core/ProgressBackdrop.tsx index 97d4aee9b1..f1aeb8af8d 100644 --- a/web/src/components/core/ProgressBackdrop.tsx +++ b/web/src/components/core/ProgressBackdrop.tsx @@ -164,7 +164,9 @@ export default function ProgressBackdrop({ id="progressStatus" gap={{ default: "gapMd" }} alignItems={{ default: "alignItemsCenter" }} + flexWrap={{ default: "nowrap" }} className={textStyles.fontSizeLg} + style={{ textWrap: "balance" }} > From a1f5ba30963ebcf0c43e65bbd41ce957e31c258e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 2 Mar 2026 16:00:39 +0000 Subject: [PATCH 07/13] doc(web): add entry to changes file --- web/package/agama-web-ui.changes | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 459f734d17..ef3c098741 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Mon Mar 2 15:59:09 UTC 2026 - David Diaz + +- Restores DASD format progress with API v2 event model + (gh#agama-project/agama#3143). +- Improves progress backdrop layout and readbility + (gh#agama-project/agama#2947). + ------------------------------------------------------------------- Fri Feb 27 14:39:08 UTC 2026 - David Diaz From f25eb433339b07dcf2cbcb9489a20d73955dd36a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz?= <1691872+dgdavid@users.noreply.github.com> Date: Tue, 3 Mar 2026 07:53:10 +0000 Subject: [PATCH 08/13] Update web/src/components/storage/dasd/DASDFormatProgress.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Iván López --- web/src/components/storage/dasd/DASDFormatProgress.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/storage/dasd/DASDFormatProgress.tsx b/web/src/components/storage/dasd/DASDFormatProgress.tsx index 23b20ea71d..e8b5fd7d92 100644 --- a/web/src/components/storage/dasd/DASDFormatProgress.tsx +++ b/web/src/components/storage/dasd/DASDFormatProgress.tsx @@ -98,7 +98,7 @@ export default function DASDFormatProgress() { return ( - {_("Formatting devices progress")} + {_("Formatting devices")} {progress.map((p) => { From 95c3dfe8e0be541b17c765721276da8723135232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 3 Mar 2026 10:03:05 +0000 Subject: [PATCH 09/13] fix(web): use "auto" instead of "scroll" for overflow To avoid displaying disabled scrollbars when there is no scroll. --- web/src/components/core/ProgressBackdrop.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/core/ProgressBackdrop.tsx b/web/src/components/core/ProgressBackdrop.tsx index f1aeb8af8d..464ef13f14 100644 --- a/web/src/components/core/ProgressBackdrop.tsx +++ b/web/src/components/core/ProgressBackdrop.tsx @@ -185,7 +185,7 @@ export default function ProgressBackdrop({ } /> - + {extraContent && {extraContent}} From c821ef8395f7672b3f9dbdcadc2d3700df6e249c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 3 Mar 2026 10:05:48 +0000 Subject: [PATCH 10/13] refactor(web): sort DASD format progress by channel The backend does not guarantee the order of devices in DASDFormatChanged events, so sort by channel on the client side to prevent progress bars from dancing on each update. --- .../storage/dasd/DASDFormatProgress.test.tsx | 19 ++++++++++--------- .../storage/dasd/DASDFormatProgress.tsx | 3 ++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/web/src/components/storage/dasd/DASDFormatProgress.test.tsx b/web/src/components/storage/dasd/DASDFormatProgress.test.tsx index 581a693259..e272c96331 100644 --- a/web/src/components/storage/dasd/DASDFormatProgress.test.tsx +++ b/web/src/components/storage/dasd/DASDFormatProgress.test.tsx @@ -71,7 +71,7 @@ describe("DASDFormatProgress", () => { }); describe("when there is progress", () => { - it("renders progress bars after DASDFormatChanged event", () => { + it("renders progress bars sorted by channel after DASDFormatChanged event", () => { let eventCallback: (event: unknown) => void; mockOnEvent.mockImplementation((cb) => { @@ -85,18 +85,19 @@ describe("DASDFormatProgress", () => { eventCallback({ type: "DASDFormatChanged", summary: [ - { - channel: "0.0.0200", - totalCylinders: 5, - formattedCylinders: 1, - finished: false, - }, + { channel: "0.0.0500", totalCylinders: 5, formattedCylinders: 1, finished: false }, + { channel: "0.0.0160", totalCylinders: 5, formattedCylinders: 5, finished: true }, + { channel: "0.0.0200", totalCylinders: 5, formattedCylinders: 1, finished: false }, ], }); }); - screen.getByRole("progressbar"); - screen.getByText("0.0.0200"); + const progresses = screen.getAllByRole("progressbar"); + expect(progresses[0]).toHaveAccessibleName("0.0.0160"); + expect(progresses[1]).toHaveAccessibleName("0.0.0200"); + expect(progresses[2]).toHaveAccessibleName("0.0.0500"); + // Finished progress should use success variant + expect(progresses[0].closest(".pf-v6-c-progress")).toHaveClass("pf-m-success"); }); }); diff --git a/web/src/components/storage/dasd/DASDFormatProgress.tsx b/web/src/components/storage/dasd/DASDFormatProgress.tsx index e8b5fd7d92..fe42d68eda 100644 --- a/web/src/components/storage/dasd/DASDFormatProgress.tsx +++ b/web/src/components/storage/dasd/DASDFormatProgress.tsx @@ -23,6 +23,7 @@ import React, { useEffect, useState } from "react"; import { Card, CardBody, CardTitle, Progress, Stack } from "@patternfly/react-core"; import { useInstallerClient } from "~/context/installer"; +import { sortCollection } from "~/utils"; import { _ } from "~/i18n"; import type { Device } from "~/model/system/dasd"; @@ -87,7 +88,7 @@ export default function DASDFormatProgress() { return client.onEvent((event) => { if (event.type === "DASDFormatChanged") { - setProgress(event.summary); + setProgress(sortCollection(event.summary, "asc", (p) => p.channel)); } }); }, [client]); From d6430ea21cfda8398803186d8934c32138bfbd14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 3 Mar 2026 10:07:33 +0000 Subject: [PATCH 11/13] feat(web): clear DASD format progress on DASDFormatFinished event --- .../storage/dasd/DASDFormatProgress.test.tsx | 23 +++++++++---------- .../storage/dasd/DASDFormatProgress.tsx | 4 ++++ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/web/src/components/storage/dasd/DASDFormatProgress.test.tsx b/web/src/components/storage/dasd/DASDFormatProgress.test.tsx index e272c96331..073d8003eb 100644 --- a/web/src/components/storage/dasd/DASDFormatProgress.test.tsx +++ b/web/src/components/storage/dasd/DASDFormatProgress.test.tsx @@ -101,33 +101,32 @@ describe("DASDFormatProgress", () => { }); }); - describe("when progress is finished", () => { - it("renders progress bar with success variant", () => { + describe("when a DASDFormatFinished event is received", () => { + it("clears the progress", () => { let eventCallback: (event: unknown) => void; - mockOnEvent.mockImplementation((cb) => { eventCallback = cb; return jest.fn(); }); - installerRender(); + const { container } = installerRender(); act(() => { eventCallback({ type: "DASDFormatChanged", summary: [ - { - channel: "0.0.0200", - totalCylinders: 5, - formattedCylinders: 5, - finished: true, - }, + { channel: "0.0.0200", totalCylinders: 5, formattedCylinders: 5, finished: true }, ], }); }); - const progressbar = screen.getByRole("progressbar"); - expect(progressbar.closest(".pf-v6-c-progress")).toHaveClass("pf-m-success"); + screen.getByRole("progressbar"); + + act(() => { + eventCallback({ type: "DASDFormatFinished" }); + }); + + expect(container).toBeEmptyDOMElement(); }); }); }); diff --git a/web/src/components/storage/dasd/DASDFormatProgress.tsx b/web/src/components/storage/dasd/DASDFormatProgress.tsx index fe42d68eda..e02d5aa897 100644 --- a/web/src/components/storage/dasd/DASDFormatProgress.tsx +++ b/web/src/components/storage/dasd/DASDFormatProgress.tsx @@ -90,6 +90,10 @@ export default function DASDFormatProgress() { if (event.type === "DASDFormatChanged") { setProgress(sortCollection(event.summary, "asc", (p) => p.channel)); } + + if (event.type === "DASDFormatFinished") { + setProgress([]); + } }); }, [client]); From 89f4da240f76c2d727f92842af84d960a41d89cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 3 Mar 2026 10:13:20 +0000 Subject: [PATCH 12/13] doc(web): fix changelog --- web/package/agama-web-ui.changes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 6ed8b09a33..447ffd0385 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,5 +1,5 @@ ------------------------------------------------------------------- -Tue Mar 3 10:59:09 UTC 2026 - David Diaz +Tue Mar 3 10:09:09 UTC 2026 - David Diaz - Restores DASD format progress with API v2 event model (gh#agama-project/agama#3143). From 7cd2994c0a69af37033aad36307cf17ae3a3a3f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Tue, 3 Mar 2026 11:57:35 +0000 Subject: [PATCH 13/13] fix: offer format option for active DASD --- web/src/components/storage/dasd/DASDTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/storage/dasd/DASDTable.tsx b/web/src/components/storage/dasd/DASDTable.tsx index ccc0094577..78b00b535b 100644 --- a/web/src/components/storage/dasd/DASDTable.tsx +++ b/web/src/components/storage/dasd/DASDTable.tsx @@ -241,7 +241,7 @@ const buildActions = ({ deactivate: device.active, diagOn: !device.diag, diagOff: device.diag, - format: !device.formatted, + format: device.active, }; return actions.filter((a) => keptActions[a.id]);