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 ( - - ; + + + + + + + + <ProductLogo product={product} width="1.25em" /> {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: , - }, ], });