diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx
index abe128ca4d..7918ca18d5 100644
--- a/web/src/App.test.tsx
+++ b/web/src/App.test.tsx
@@ -41,7 +41,6 @@ jest.mock("~/client");
// Mock some components,
// See https://www.chakshunyu.com/blog/how-to-mock-a-react-component-in-jest/#default-export
jest.mock("~/components/layout/Loading", () => () =>
Loading Mock
);
-jest.mock("~/components/product/ProductSelectionProgress", () => () => Product progress
);
it.todo("adapt to new api/hooks, adding needed mocking");
describe.skip("App", () => {
diff --git a/web/src/components/core/InstallButton.test.tsx b/web/src/components/core/InstallButton.test.tsx
index 2b44065363..b087f10c85 100644
--- a/web/src/components/core/InstallButton.test.tsx
+++ b/web/src/components/core/InstallButton.test.tsx
@@ -43,7 +43,6 @@ describe("InstallButton", () => {
["overview (full route)", ROOT.overview],
["login", ROOT.login],
["product selection", PRODUCT.changeProduct],
- ["product selection progress", PRODUCT.progress],
["installation progress", ROOT.installationProgress],
["installation finished", ROOT.installationFinished],
["installation exit", ROOT.installationExit],
diff --git a/web/src/components/core/InstallationProgress.tsx b/web/src/components/core/InstallationProgress.tsx
index 2d3fea18c7..ce04ff44b6 100644
--- a/web/src/components/core/InstallationProgress.tsx
+++ b/web/src/components/core/InstallationProgress.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2024] SUSE LLC
+ * Copyright (c) [2022-2026] SUSE LLC
*
* All Rights Reserved.
*
@@ -21,16 +21,61 @@
*/
import React from "react";
+import { Flex, Grid, GridItem, HelperText, HelperTextItem, Title } from "@patternfly/react-core";
+import Page from "~/components/core/Page";
+import ProgressReport from "~/components/core/ProgressReport";
+import Icon from "~/components/layout/Icon";
+import ProductLogo from "../product/ProductLogo";
+import { useProductInfo } from "~/hooks/model/config/product";
import { _ } from "~/i18n";
-import ProgressReport from "./ProgressReport";
-import Page from "./Page";
-function InstallationProgress() {
+import textStyles from "@patternfly/react-styles/css/utilities/Text/text";
+import alignmentStyles from "@patternfly/react-styles/css/utilities/Alignment/alignment";
+
+export default function InstallationProgress() {
+ const product = useProductInfo();
+
return (
-
- ;
+
+
+
+
+
+
+
+ {product?.name}
+
+
+
+ {_("Installation in progress")}
+
+
+
+
+
+
+
+
+
+
);
}
-
-export default InstallationProgress;
diff --git a/web/src/components/core/InstallerOptions.test.tsx b/web/src/components/core/InstallerOptions.test.tsx
index fffa6abcb2..f0502df03a 100644
--- a/web/src/components/core/InstallerOptions.test.tsx
+++ b/web/src/components/core/InstallerOptions.test.tsx
@@ -29,7 +29,7 @@ import { Keymap, Locale } from "~/model/system/l10n";
import { Progress, Stage } from "~/model/status";
import { System } from "~/model/system/network";
import * as utils from "~/utils";
-import { PRODUCT, ROOT } from "~/routes/paths";
+import { ROOT } from "~/routes/paths";
import InstallerOptions, { InstallerOptionsProps } from "./InstallerOptions";
import { useStatus } from "~/hooks/model/status";
@@ -135,7 +135,6 @@ describe("InstallerOptions", () => {
describe.each([
["login", ROOT.login],
- ["product selection progress", PRODUCT.progress],
["installation progress", ROOT.installationProgress],
["installation finished", ROOT.installationFinished],
])(`when the installer is rendering the %s screen`, (_, path) => {
diff --git a/web/src/components/core/InstallerOptions.tsx b/web/src/components/core/InstallerOptions.tsx
index 0b72b219a2..43f35b7c40 100644
--- a/web/src/components/core/InstallerOptions.tsx
+++ b/web/src/components/core/InstallerOptions.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2025] SUSE LLC
+ * Copyright (c) [2022-2026] SUSE LLC
*
* All Rights Reserved.
*
@@ -51,7 +51,7 @@ import { useInstallerL10n } from "~/context/installerL10n";
import { localConnection } from "~/utils";
import { _ } from "~/i18n";
import supportedLanguages from "~/languages.json";
-import { PRODUCT, ROOT, L10N } from "~/routes/paths";
+import { ROOT, L10N } from "~/routes/paths";
import { useProductInfo } from "~/hooks/model/config/product";
import { useSystem } from "~/hooks/model/system";
import { useStatus } from "~/hooks/model/status";
@@ -575,9 +575,7 @@ export default function InstallerOptions({
stage === "installing" ||
// FIXME: below condition could be a problem for a question appearing while
// product progress
- [ROOT.login, ROOT.installationProgress, ROOT.installationFinished, PRODUCT.progress].includes(
- location.pathname,
- );
+ [ROOT.login, ROOT.installationProgress, ROOT.installationFinished].includes(location.pathname);
if (skip) return;
diff --git a/web/src/components/core/Page.tsx b/web/src/components/core/Page.tsx
index 66c18e193f..72aa018ab7 100644
--- a/web/src/components/core/Page.tsx
+++ b/web/src/components/core/Page.tsx
@@ -335,6 +335,8 @@ interface StandardLayoutProps {
showQuestions?: boolean;
/** Whether to show installer options in the header */
showInstallerOptions?: boolean;
+ /** Whether the progress monitor must not be mounted */
+ hideProgressMonitor?: boolean;
/** Page content */
children?: React.ReactNode;
}
@@ -349,6 +351,7 @@ const StandardLayout = ({
title,
showQuestions = true,
showInstallerOptions = false,
+ hideProgressMonitor = false,
}: StandardLayoutProps) => {
return (
}
>
@@ -380,6 +384,8 @@ interface BasePageProps {
progress?: ProgressBackdropProps;
/** Whether to show the Questions component at the bottom of the page */
showQuestions?: boolean;
+ /** Whether the progress monitor must not be mounted */
+ hideProgressMonitor?: boolean;
/** Page content */
children?: React.ReactNode;
}
@@ -410,6 +416,8 @@ interface MinimalPageProps extends BasePageProps {
breadcrumbs?: never;
/** Installer options not available in minimal variant */
showInstallerOptions?: never;
+ /** Whether the progress monitor must not be mounted */
+ hideProgressMonitor?: never;
}
/**
@@ -477,6 +485,7 @@ const Page = ({
variant = "standard",
showQuestions = true,
showInstallerOptions = false,
+ hideProgressMonitor = false,
children,
}: PageProps): React.ReactNode => {
if (variant === "minimal") {
@@ -490,6 +499,7 @@ const Page = ({
title={title}
showQuestions={showQuestions}
showInstallerOptions={showInstallerOptions}
+ hideProgressMonitor={hideProgressMonitor}
>
{children || }
diff --git a/web/src/components/core/ProgressReport.test.tsx b/web/src/components/core/ProgressReport.test.tsx
index 31f12070e3..77227bd7d1 100644
--- a/web/src/components/core/ProgressReport.test.tsx
+++ b/web/src/components/core/ProgressReport.test.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2024] SUSE LLC
+ * Copyright (c) [2022-2026] SUSE LLC
*
* All Rights Reserved.
*
@@ -48,14 +48,14 @@ describe("ProgressReport", () => {
});
it("shows the progress including the details", () => {
- plainRender();
+ plainRender();
expect(screen.getByText(/Partition disks/)).toBeInTheDocument();
expect(screen.getByText(/Install software/)).toBeInTheDocument();
// NOTE: not finding the whole text because it is now split in two because of PF/Truncate
expect(screen.getByText(/Doing some/)).toBeInTheDocument();
- expect(screen.getByText(/\(1\/1\)/)).toBeInTheDocument();
+ expect(screen.getByText(/Step 1 of 1/)).toBeInTheDocument();
});
});
@@ -80,14 +80,14 @@ describe("ProgressReport", () => {
});
it("shows the progress including the details", () => {
- plainRender();
+ plainRender();
expect(screen.getByText(/Partition disks/)).toBeInTheDocument();
expect(screen.getByText(/Install software/)).toBeInTheDocument();
// NOTE: not finding the whole text because it is now split in two because of PF/Truncate
expect(screen.getByText(/Installing vim/)).toBeInTheDocument();
- expect(screen.getByText(/\(5\/200\)/)).toBeInTheDocument();
+ expect(screen.getByText(/Step 5 of 200/)).toBeInTheDocument();
});
});
});
diff --git a/web/src/components/core/ProgressReport.tsx b/web/src/components/core/ProgressReport.tsx
index 6eef62f4b7..fd7ee35a01 100644
--- a/web/src/components/core/ProgressReport.tsx
+++ b/web/src/components/core/ProgressReport.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2022-2025] SUSE LLC
+ * Copyright (c) [2022-2026] SUSE LLC
*
* All Rights Reserved.
*
@@ -21,8 +21,8 @@
*/
import React from "react";
+import { sprintf } from "sprintf-js";
import {
- Content,
Flex,
ProgressStep,
ProgressStepper,
@@ -30,32 +30,22 @@ import {
Spinner,
Truncate,
} from "@patternfly/react-core";
-import sizingStyles from "@patternfly/react-styles/css/utilities/Sizing/sizing";
+import Text from "~/components/core/Text";
import { _ } from "~/i18n";
import { useStatus } from "~/hooks/model/status";
import type { Progress as ProgressType } from "~/model/status";
-type StepProps = {
- id: string;
- titleId: string;
- isCurrent: boolean;
- variant?: ProgressStepProps["variant"];
- description?: ProgressStepProps["description"];
-};
-
const Progress = ({
steps,
step,
- firstStep,
detail,
}: {
steps: string[];
step: ProgressType;
- firstStep: React.ReactNode;
detail: ProgressType | undefined;
}) => {
- const stepProperties = (stepNumber: number): StepProps => {
- const properties: StepProps = {
+ const stepProperties = (stepNumber: number) => {
+ const properties: ProgressStepProps = {
isCurrent: stepNumber === step.index,
id: `step-${stepNumber}-id`,
titleId: `step-${stepNumber}-title`,
@@ -63,20 +53,17 @@ const Progress = ({
if (stepNumber > step.index) {
properties.variant = "pending";
- properties.description = {_("Pending")}
;
}
if (properties.isCurrent) {
properties.variant = "info";
+ properties.icon = ;
if (detail && detail.step !== "") {
const { step: message, index, size } = detail;
properties.description = (
- {_("In progress")}
-
-
-
- {`(${index}/${size})`}
+
+ {sprintf(_("Step %1$d of %2$d"), index, size)}
);
}
@@ -84,23 +71,14 @@ const Progress = ({
if (stepNumber < step.index) {
properties.variant = "success";
- properties.description = {_("Finished")}
;
}
return properties;
};
return (
-
- {firstStep && (
-
- {firstStep}
-
- )}
- {steps.map((description: StepProps["description"], idx: number) => {
+
+ {steps.map((description, idx: number) => {
return (
{description}
@@ -112,9 +90,9 @@ const Progress = ({
};
/**
- * Shows progress steps when a product is selected.
+ * Renders progress with a PF/ProgresStepper
*/
-function ProgressReport({ title, firstStep }: { title: string; firstStep?: React.ReactNode }) {
+export default function ProgressReport() {
const { progresses } = useStatus();
const managerProgress = progresses.find((t) => t.scope === "manager");
@@ -125,24 +103,5 @@ function ProgressReport({ title, firstStep }: { title: string; firstStep?: React
const detail = softwareProgress || storageProgress;
- return (
-
-
- {title}
-
-
- );
+ return ;
}
-
-export default ProgressReport;
diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx
index 619b880001..41f5b6638d 100644
--- a/web/src/components/layout/Header.tsx
+++ b/web/src/components/layout/Header.tsx
@@ -38,7 +38,7 @@ import {
} from "@patternfly/react-core";
import { Icon } from "~/components/layout";
import { ChangeProductOption, InstallerOptions, InstallButton, SkipTo } from "~/components/core";
-import ProgressStatusMonitor from "../core/ProgressStatusMonitor";
+import ProgressStatusMonitor from "~/components/core/ProgressStatusMonitor";
import Breadcrumbs from "~/components/core/Breadcrumbs";
import { useProductInfo } from "~/hooks/model/config/product";
import { ROOT } from "~/routes/paths";
@@ -63,6 +63,8 @@ export type HeaderProps = {
showInstallerOptions?: boolean;
/** Breadcrumb navigation items */
breadcrumbs?: BreadcrumbProps[];
+ /** Whether the progress monitor must not be mounted */
+ hideProgressMonitor?: boolean;
};
const OptionsDropdown = () => {
@@ -109,6 +111,7 @@ export default function Header({
breadcrumbs,
showSkipToContent = true,
showInstallerOptions = true,
+ hideProgressMonitor = false,
}: HeaderProps): React.ReactNode {
const product = useProductInfo();
@@ -152,9 +155,11 @@ export default function Header({
-
-
-
+ {!hideProgressMonitor && (
+
+
+
+ )}
{showInstallerOptions && (
diff --git a/web/src/components/layout/Icon.tsx b/web/src/components/layout/Icon.tsx
index fd5d316e07..bbafcbdb97 100644
--- a/web/src/components/layout/Icon.tsx
+++ b/web/src/components/layout/Icon.tsx
@@ -35,6 +35,7 @@ import CheckCircle from "@icons/check_circle.svg?component";
import ChevronLeft from "@icons/chevron_left.svg?component";
import ChevronRight from "@icons/chevron_right.svg?component";
import Delete from "@icons/delete.svg?component";
+import DeployedCodeUpdate from "@icons/deployed_code_update.svg?component";
import EditSquare from "@icons/edit_square.svg?component";
import Emergency from "@icons/emergency.svg?component";
import Error from "@icons/error.svg?component";
@@ -78,6 +79,7 @@ const icons = {
chevron_left: ChevronLeft,
chevron_right: ChevronRight,
delete: Delete,
+ deployed_code_update: DeployedCodeUpdate,
edit_square: EditSquare,
emergency: Emergency,
error: Error,
diff --git a/web/src/components/product/ProductLogo.test.tsx b/web/src/components/product/ProductLogo.test.tsx
new file mode 100644
index 0000000000..093b098e06
--- /dev/null
+++ b/web/src/components/product/ProductLogo.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) [2026] 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 { screen } from "@testing-library/react";
+import { plainRender } from "~/test-utils";
+import ProductLogo from "./ProductLogo";
+
+const product = {
+ id: "Tumbleweed",
+ name: "openSUSE Tumbleweed",
+ icon: "tumbleweed.svg",
+ description: "Tumbleweed description...",
+ registration: false,
+};
+
+describe("ProductLogo", () => {
+ it("renders nothing when product is null", () => {
+ const { container } = plainRender();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders nothing when product is undefined", () => {
+ const { container } = plainRender();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders the logo image with correct src and alt text", () => {
+ plainRender();
+
+ const img = screen.getByRole("img", { hidden: true });
+ expect(img).toHaveAttribute("src", "assets/logos/tumbleweed.svg");
+ expect(img).toHaveAttribute("alt", "openSUSE Tumbleweed logo");
+ });
+
+ it("applies default width of 80px", () => {
+ plainRender();
+
+ const img = screen.getByRole("img", { hidden: true });
+ expect(img).toHaveAttribute("width", "80px");
+ expect(img).toHaveStyle({ width: "80px" });
+ });
+
+ it("applies custom width when provided", () => {
+ plainRender();
+
+ const img = screen.getByRole("img", { hidden: true });
+ expect(img).toHaveAttribute("width", "120px");
+ expect(img).toHaveStyle({ width: "120px" });
+ });
+
+ it("applies vertical align middle style", () => {
+ plainRender();
+
+ const img = screen.getByRole("img", { hidden: true });
+ expect(img).toHaveStyle({ verticalAlign: "middle" });
+ });
+});
diff --git a/web/src/components/product/ProductLogo.tsx b/web/src/components/product/ProductLogo.tsx
index 278199cda8..25840c0d97 100644
--- a/web/src/components/product/ProductLogo.tsx
+++ b/web/src/components/product/ProductLogo.tsx
@@ -25,6 +25,8 @@ import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
export default function ProductLogo({ product, width = "80px" }) {
+ if (!product) return;
+
const logoSrc = `assets/logos/${product.icon}`;
// TRANSLATORS: %s will be replaced by a product name. E.g., "openSUSE Tumbleweed"
const logoAltText = sprintf(_("%s logo"), product.name);
diff --git a/web/src/components/product/ProductSelectionProgress.test.tsx b/web/src/components/product/ProductSelectionProgress.test.tsx
deleted file mode 100644
index 2101bbf43f..0000000000
--- a/web/src/components/product/ProductSelectionProgress.test.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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 { screen } from "@testing-library/react";
-import { installerRender } from "~/test-utils";
-import ProductSelectionProgress from "./ProductSelectionProgress";
-import { ROOT } from "~/routes/paths";
-import { Product } from "~/types/software";
-
-jest.mock("~/components/core/ProgressReport", () => () => ProgressReport Mock
);
-
-let isBusy = false;
-const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: false };
-
-jest.mock("~/queries/status", () => ({
- ...jest.requireActual("~/queries/status"),
- useInstallerStatus: () => ({ isBusy }),
-}));
-
-jest.mock("~/queries/software", () => ({
- ...jest.requireActual("~/queries/software"),
- useProduct: () => ({ selectedProduct: tumbleweed }),
-}));
-
-describe("ProductSelectionProgress", () => {
- describe("when installer is not busy", () => {
- it("redirects to the root path", async () => {
- installerRender();
- await screen.findByText(`Navigating to ${ROOT.root}`);
- });
- });
-
- describe("when installer in busy", () => {
- beforeEach(() => {
- isBusy = true;
- });
-
- it("renders progress report", () => {
- installerRender();
- screen.getByText("ProgressReport Mock");
- });
- });
-});
diff --git a/web/src/components/product/ProductSelectionProgress.tsx b/web/src/components/product/ProductSelectionProgress.tsx
deleted file mode 100644
index b971b337ce..0000000000
--- a/web/src/components/product/ProductSelectionProgress.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (c) [2024-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 { Navigate } from "react-router";
-import { Page, ProgressReport } from "~/components/core";
-import { useProduct } from "~/queries/software";
-import { useInstallerStatus } from "~/queries/status";
-import { ROOT as PATHS } from "~/routes/paths";
-import { _ } from "~/i18n";
-
-/**
- * Shows progress steps when a product is selected.
- */
-function ProductSelectionProgress() {
- const { selectedProduct } = useProduct({ suspense: true });
- const { isBusy } = useInstallerStatus({ suspense: true });
-
- if (!isBusy) return ;
-
- return (
-
-
-
- );
-}
-
-export default ProductSelectionProgress;
diff --git a/web/src/components/product/index.ts b/web/src/components/product/index.ts
index 5ddad57168..ef49972a04 100644
--- a/web/src/components/product/index.ts
+++ b/web/src/components/product/index.ts
@@ -21,6 +21,5 @@
*/
export { default as ProductSelectionPage } from "./ProductSelectionPage";
-export { default as ProductSelectionProgress } from "./ProductSelectionProgress";
export { default as ProductRegistrationPage } from "./ProductRegistrationPage";
export { default as LicenseDialog } from "./LicenseDialog";
diff --git a/web/src/routes/paths.ts b/web/src/routes/paths.ts
index 3657dfce9e..251bca6d5d 100644
--- a/web/src/routes/paths.ts
+++ b/web/src/routes/paths.ts
@@ -38,7 +38,6 @@ const NETWORK = {
const PRODUCT = {
root: "/products",
changeProduct: "/products",
- progress: "/products/progress",
};
const REGISTRATION = {
@@ -116,7 +115,6 @@ const HOSTNAME = {
const SIDE_PATHS = [
ROOT.login,
PRODUCT.changeProduct,
- PRODUCT.progress,
ROOT.installationProgress,
ROOT.installationFinished,
ROOT.installationExit,
diff --git a/web/src/routes/products.tsx b/web/src/routes/products.tsx
index 7185b5e9cd..70f68d3954 100644
--- a/web/src/routes/products.tsx
+++ b/web/src/routes/products.tsx
@@ -1,5 +1,5 @@
/*
- * Copyright (c) [2024] SUSE LLC
+ * Copyright (c) [2024-2026] SUSE LLC
*
* All Rights Reserved.
*
@@ -21,7 +21,7 @@
*/
import React from "react";
-import { ProductSelectionPage, ProductSelectionProgress } from "~/components/product";
+import { ProductSelectionPage } from "~/components/product";
import { Route } from "~/types/routes";
import { PRODUCT as PATHS } from "~/routes/paths";
@@ -32,10 +32,6 @@ const routes = (): Route => ({
index: true,
element: ,
},
- {
- path: PATHS.progress,
- element: ,
- },
],
});