diff --git a/web/src/components/core/Installation.jsx b/web/custom.d.ts similarity index 73% rename from web/src/components/core/Installation.jsx rename to web/custom.d.ts index b29b54b438..68715cd718 100644 --- a/web/src/components/core/Installation.jsx +++ b/web/custom.d.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2024] SUSE LLC * * All Rights Reserved. * @@ -20,11 +20,12 @@ * find current contact information at www.suse.com. */ -import React from "react"; -import { InstallationProgress, InstallationFinished } from "~/components/core"; - -function Installation({ isBusy }) { - return isBusy ? : ; +declare module "*.svg" { + const content: React.FunctionComponent>; + export default content; } -export default Installation; +declare module "*.svg?component" { + const content: React.FunctionComponent>; + export default content; +} diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index bbfb9dec5e..1643765ffa 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Oct 28 19:26:29 UTC 2024 - David Diaz + +- Make some general actions more accessible + (gh#agama-project/agama#1690). + ------------------------------------------------------------------- Wed Oct 23 16:26:29 UTC 2024 - David Diaz diff --git a/web/src/App.test.jsx b/web/src/App.test.tsx similarity index 80% rename from web/src/App.test.jsx rename to web/src/App.test.tsx index 0a5585f4f8..3c4aa32bbb 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -23,10 +23,10 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; - import App from "./App"; -import { createClient } from "~/client"; import { InstallationPhase } from "./types/status"; +import { createClient } from "~/client"; +import { Product } from "./types/software"; jest.mock("~/client"); @@ -39,9 +39,12 @@ jest.mock("~/api/l10n", () => ({ updateConfig: jest.fn(), })); +const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed" }; +const microos: Product = { id: "Leap Micro", name: "openSUSE Micro" }; + // list of available products -let mockProducts; -let mockSelectedProduct; +let mockProducts: Product[]; +let mockSelectedProduct: Product; jest.mock("~/queries/software", () => ({ ...jest.requireActual("~/queries/software"), @@ -62,6 +65,7 @@ jest.mock("~/queries/l10n", () => ({ jest.mock("~/queries/issues", () => ({ ...jest.requireActual("~/queries/issues"), useIssuesChanges: () => jest.fn(), + useAllIssues: () => ({ isEmtpy: true }), })); jest.mock("~/queries/storage", () => ({ @@ -88,7 +92,6 @@ jest.mock("~/context/installer", () => ({ // Mock some components, // See https://www.chakshunyu.com/blog/how-to-mock-a-react-component-in-jest/#default-export jest.mock("~/components/questions/Questions", () => () =>
Questions Mock
); -jest.mock("~/components/core/Installation", () => () =>
Installation Mock
); jest.mock("~/components/layout/Loading", () => () =>
Loading Mock
); jest.mock("~/components/product/ProductSelectionProgress", () => () =>
Product progress
); @@ -96,14 +99,11 @@ describe("App", () => { beforeEach(() => { // setting the language through a cookie document.cookie = "agamaLang=en-us; path=/;"; - createClient.mockImplementation(() => { + (createClient as jest.Mock).mockImplementation(() => { return {}; }); - mockProducts = [ - { id: "openSUSE", name: "openSUSE Tumbleweed" }, - { id: "Leap Micro", name: "openSUSE Micro" }, - ]; + mockProducts = [tumbleweed, microos]; }); afterEach(() => { @@ -142,7 +142,7 @@ describe("App", () => { describe("if the service is busy", () => { beforeEach(() => { mockClientStatus.isBusy = true; - mockSelectedProduct = { id: "Tumbleweed" }; + mockSelectedProduct = tumbleweed; }); it("redirects to product selection progress", async () => { @@ -163,15 +163,29 @@ describe("App", () => { }); }); - describe("on the installaiton phase", () => { + describe("on the busy installaiton phase", () => { + beforeEach(() => { + mockClientStatus.phase = InstallationPhase.Install; + mockClientStatus.isBusy = true; + mockSelectedProduct = tumbleweed; + }); + + it("navigates to installation progress", async () => { + installerRender(, { withL10n: true }); + await screen.findByText("Navigating to /installation/progress"); + }); + }); + + describe("on the idle installaiton phase", () => { beforeEach(() => { mockClientStatus.phase = InstallationPhase.Install; - mockSelectedProduct = { id: "Fake product" }; + mockClientStatus.isBusy = false; + mockSelectedProduct = tumbleweed; }); - it("renders the application content", async () => { + it("navigates to installation finished", async () => { installerRender(, { withL10n: true }); - await screen.findByText("Installation Mock"); + await screen.findByText("Navigating to /installation/finished"); }); }); }); diff --git a/web/src/App.jsx b/web/src/App.tsx similarity index 65% rename from web/src/App.jsx rename to web/src/App.tsx index 444e3f3f28..9fe9761571 100644 --- a/web/src/App.jsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -22,26 +22,22 @@ import React from "react"; import { Navigate, Outlet, useLocation } from "react-router-dom"; -import { Loading } from "./components/layout"; +import { ServerError } from "~/components/core"; +import { Loading, PlainLayout } from "~/components/layout"; import { Questions } from "~/components/questions"; -import { ServerError, Installation } from "~/components/core"; -import { useInstallerL10n } from "./context/installerL10n"; +import { useInstallerL10n } from "~/context/installerL10n"; import { useInstallerClientStatus } from "~/context/installer"; -import { useProduct, useProductChanges } from "./queries/software"; +import { useProduct, useProductChanges } from "~/queries/software"; import { useL10nConfigChanges } from "~/queries/l10n"; -import { useIssuesChanges } from "./queries/issues"; -import { useInstallerStatus, useInstallerStatusChanges } from "./queries/status"; -import { useDeprecatedChanges } from "./queries/storage"; -import { PATHS as PRODUCT_PATHS } from "./routes/products"; -import SimpleLayout from "./SimpleLayout"; -import { InstallationPhase } from "./types/status"; +import { useIssuesChanges } from "~/queries/issues"; +import { useInstallerStatus, useInstallerStatusChanges } from "~/queries/status"; +import { useDeprecatedChanges } from "~/queries/storage"; +import { PATHS as PRODUCT_PATHS } from "~/routes/products"; +import { PATHS as ROOT_PATHS } from "~/router"; +import { InstallationPhase } from "~/types/status"; /** * Main application component. - * - * @param {object} props - * @param {number} [props.max_attempts=3] - Connection attempts before displaying an - * error (3 by default). The component will keep trying to connect. */ function App() { const location = useLocation(); @@ -56,25 +52,33 @@ function App() { useDeprecatedChanges(); const Content = () => { - if (error) return ; + if (error) + return ( + + + + ); + + if (phase === InstallationPhase.Install && isBusy) { + return ; + } - if (phase === InstallationPhase.Install) { - return ; + if (phase === InstallationPhase.Install && !isBusy) { + return ; } - if (!products || !connected) + if (!products || !connected) return ; + + if (phase === InstallationPhase.Startup && isBusy) { return ( - + - + ); - - if (phase === InstallationPhase.Startup && isBusy) { - return ; } if (selectedProduct === undefined && location.pathname !== PRODUCT_PATHS.root) { - return ; + return ; } if ( diff --git a/web/src/SimpleLayout.jsx b/web/src/SimpleLayout.jsx deleted file mode 100644 index c1f9c8f344..0000000000 --- a/web/src/SimpleLayout.jsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of 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, { Suspense } from "react"; -import { Outlet } from "react-router-dom"; -import { - Masthead, - MastheadContent, - Page, - Toolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, -} from "@patternfly/react-core"; -import { InstallerOptions } from "./components/core"; -import { Loading } from "./components/layout"; - -/** - * Simple layout for displaying content that comes before product configuration - * TODO: improve documentation - */ -export default function SimpleLayout({ - showOutlet = true, - showInstallerOptions = false, - children, -}) { - return ( - - - - - - - {showInstallerOptions && } - - - - - - }>{showOutlet ? : children} - - ); -} diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index 4fbdc7b943..2c6ab76be7 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -67,7 +67,28 @@ --pf-v5-c-button--m-secondary--Color: var(--color-link-hover); } -.pf-v5-c-modal-box__body { +// Redefine style for primary buttons placed at top bar +.pf-v5-c-masthead__content { + .pf-v5-c-button.pf-m-primary { + --pf-v5-c-button--FontSize: 120%; + --pf-v5-c-button--m-primary--BackgroundColor: var(--color-button-primary-hover); + letter-spacing: 1px; + } + + .pf-v5-c-button.pf-m-primary:hover { + --pf-v5-c-button--m-primary--BackgroundColor: #1ea064; // var(--color-button-primary); + } + + .pf-v5-c-button.pf-m-primary:focus:hover { + --pf-v5-c-button--m-primary--BackgroundColor: #1ea064; // var(--color-button-primary); + } + + .pf-v5-c-button.pf-m-warning { + color: var(--color-button-primary); + } +} + +.pf-v5-c-button.pf-m-primary .pf-v5-c-modal-box__body { padding-block: var(--pf-v5-c-modal-box__body--PaddingTop); } @@ -272,7 +293,7 @@ // Allows the pf-m-current directly in the a element instead of li. // Needed because setting the pf-m-current in ReactRouter/NavLink (the one -// that knowst that link "isActive") +// that know the link "isActive") .pf-v5-c-tabs__link.pf-m-current { --pf-v5-c-tabs__link--after--BorderColor: var( diff --git a/web/src/components/core/About.test.tsx b/web/src/components/core/About.test.tsx deleted file mode 100644 index cbb2f1d568..0000000000 --- a/web/src/components/core/About.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) [2022-2023] 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, waitFor, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; - -import About from "./About"; - -describe("About", () => { - it("renders a help icon inside the button by default", () => { - const { container } = plainRender(); - const icon = container.querySelector("svg"); - expect(icon).toHaveAttribute("data-icon-name", "help"); - }); - - it("does not render a help icon inside the button if showIcon=false", () => { - const { container } = plainRender(); - const icon = container.querySelector("svg"); - expect(icon).toBeNull(); - }); - - it("allows setting its icon size", () => { - const { container } = plainRender(); - const icon = container.querySelector("svg"); - expect(icon.classList.contains("icon-xxs")).toBe(true); - }); - - it("allows setting its button text", () => { - plainRender(); - screen.getByRole("button", { name: "What is this?" }); - }); - - it("allows setting button props", () => { - plainRender(); - const button = screen.getByRole("button", { name: "About" }); - expect(button.classList.contains("pf-m-link")).toBe(true); - expect(button.classList.contains("pf-m-inline")).toBe(true); - }); - - it("allows user to read 'About Agama'", async () => { - const { user } = plainRender(); - - const button = screen.getByRole("button", { name: /About/i }); - await user.click(button); - - const dialog = await screen.findByRole("dialog"); - - within(dialog).getByText("About Agama"); - - const closeButton = within(dialog).getByRole("button", { name: /Close/i }); - await user.click(closeButton); - - await waitFor(() => { - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); -}); diff --git a/web/src/components/core/About.tsx b/web/src/components/core/About.tsx deleted file mode 100644 index 0eacb2ce80..0000000000 --- a/web/src/components/core/About.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) [2022-2023] 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, { useState } from "react"; -import { Button, ButtonProps, Text } from "@patternfly/react-core"; -import { Icon } from "~/components/layout"; -import { Popup } from "~/components/core"; -import { sprintf } from "sprintf-js"; -import { _ } from "~/i18n"; - -export type AboutProps = { - /** Whether render a "help" icon into the button */ - showIcon?: boolean; - /** The size for the button icon, if any */ - iconSize?: string; - /** The text for the button */ - buttonText?: string; - /** Props for the button, see {@link https://www.patternfly.org/components/button PF/Button} */ - buttonProps?: ButtonProps; -} & ButtonProps; - -/** - * Renders a button and a dialog to allow user read about what Agama is - */ -export default function About({ - showIcon = true, - iconSize = "s", - buttonText = _("About"), - buttonProps = { variant: "link" }, -}: AboutProps) { - const [isOpen, setIsOpen] = useState(false); - - const open = () => setIsOpen(true); - const close = () => setIsOpen(false); - - return ( - <> - - - - - { - // TRANSLATORS: content of the "About" popup (1/2) - _( - "Agama is an experimental installer for (open)SUSE systems. It \ -is still under development so, please, do not use it in \ -production environments. If you want to give it a try, we \ -recommend using a virtual machine to prevent any possible \ -data loss.", - ) - } - - - {sprintf( - // TRANSLATORS: content of the "About" popup (2/2) - // %s is replaced by the project URL - _("For more information, please visit the project's page at %s."), - "https://agama-project.github.io/", - )} - - - - {_("Close")} - - - - - ); -} diff --git a/web/src/components/core/InstallButton.test.tsx b/web/src/components/core/InstallButton.test.tsx index b19a50a032..1adb4570c8 100644 --- a/web/src/components/core/InstallButton.test.tsx +++ b/web/src/components/core/InstallButton.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -22,19 +22,59 @@ import React from "react"; import { screen, waitFor } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { installerRender, mockRoutes } from "~/test-utils"; import { InstallButton } from "~/components/core"; +import { IssuesList } from "~/types/issues"; +import { PATHS as PRODUCT_PATHS } from "~/routes/products"; const mockStartInstallationFn = jest.fn(); +let mockIssuesList: IssuesList; jest.mock("~/api/manager", () => ({ ...jest.requireActual("~/api/manager"), startInstallation: () => mockStartInstallationFn(), })); -describe("when the button is clicked and there are not errors", () => { - it("starts the installation after user confirmation", async () => { - const { user } = plainRender(); +jest.mock("~/queries/issues", () => ({ + ...jest.requireActual("~/queries/issues"), + useAllIssues: () => mockIssuesList, +})); + +describe("when there are installation issues", () => { + beforeEach(() => { + mockIssuesList = new IssuesList( + [ + { + description: "Fake Issue", + source: 0, + severity: 0, + details: "Fake Issue details", + }, + ], + [], + [], + [], + ); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); +}); + +describe("when there are not installation issues", () => { + beforeEach(() => { + mockIssuesList = new IssuesList([], [], [], []); + }); + + it("renders an Install button", () => { + installerRender(); + screen.getByRole("button", { name: "Install" }); + }); + + it("starts the installation after user clicks on it and accept the confirmation", async () => { + const { user } = installerRender(); const button = await screen.findByRole("button", { name: "Install" }); await user.click(button); @@ -43,8 +83,8 @@ describe("when the button is clicked and there are not errors", () => { expect(mockStartInstallationFn).toHaveBeenCalled(); }); - it("does not start the installation if the user cancels", async () => { - const { user } = plainRender(); + it("does not start the installation if the user clicks on it but cancels the confirmation", async () => { + const { user } = installerRender(); const button = await screen.findByRole("button", { name: "Install" }); await user.click(button); @@ -56,4 +96,26 @@ describe("when the button is clicked and there are not errors", () => { expect(screen.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument(); }); }); + + describe("but installer is rendering the product selection", () => { + beforeEach(() => { + mockRoutes(PRODUCT_PATHS.changeProduct); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("but installer is configuring a product", () => { + beforeEach(() => { + mockRoutes(PRODUCT_PATHS.progress); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); }); diff --git a/web/src/components/core/InstallButton.tsx b/web/src/components/core/InstallButton.tsx index 875ce79f2e..0d7c56512f 100644 --- a/web/src/components/core/InstallButton.tsx +++ b/web/src/components/core/InstallButton.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -22,11 +22,14 @@ import React, { useState } from "react"; -import { Button, Stack } from "@patternfly/react-core"; +import { Button, ButtonProps, Stack } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { _ } from "~/i18n"; import { startInstallation } from "~/api/manager"; +import { useAllIssues } from "~/queries/issues"; +import { useLocation } from "react-router-dom"; +import { PATHS as PRODUCT_PATHS } from "~/routes/products"; const InstallConfirmationPopup = ({ onAccept, onClose }) => { return ( @@ -59,23 +62,23 @@ according to the provided installation settings.", * * It starts the installation after asking for confirmation. * - * @component - * - * @example - * console.log("clicked!")} /> - * - * @param {object} props - * @param {() => void} [props.onClick] - function to call when the user clicks the button */ -const InstallButton = () => { +const InstallButton = (props: Omit) => { + const issues = useAllIssues(); const [isOpen, setIsOpen] = useState(false); + const location = useLocation(); + + if (!issues.isEmpty) return; + // Do not show the button if the user is about to change the product or the + // installer is configuring a product. + if ([PRODUCT_PATHS.changeProduct, PRODUCT_PATHS.progress].includes(location.pathname)) return; const open = async () => setIsOpen(true); const close = () => setIsOpen(false); return ( <> - diff --git a/web/src/components/core/InstallationFinished.tsx b/web/src/components/core/InstallationFinished.tsx index fdc234b9d5..0c9a464b62 100644 --- a/web/src/components/core/InstallationFinished.tsx +++ b/web/src/components/core/InstallationFinished.tsx @@ -37,7 +37,6 @@ import { Stack, Text, } from "@patternfly/react-core"; -import SimpleLayout from "~/SimpleLayout"; import { Center, Icon } from "~/components/layout"; import { EncryptionMethods } from "~/types/storage"; import { _ } from "~/i18n"; @@ -45,6 +44,9 @@ import alignmentStyles from "@patternfly/react-styles/css/utilities/Alignment/al import { useInstallerStatus } from "~/queries/status"; import { useProposalResult } from "~/queries/storage"; import { finishInstallation } from "~/api/manager"; +import { InstallationPhase } from "~/types/status"; +import { Navigate } from "react-router-dom"; +import { PATHS } from "~/router"; const TpmHint = () => { const [isExpanded, setIsExpanded] = useState(false); @@ -76,55 +78,62 @@ the machine needs to boot directly to the new boot loader.", const SuccessIcon = () => ; function InstallationFinished() { - const { useIguana } = useInstallerStatus({ suspense: true }); + const { phase, isBusy, useIguana } = useInstallerStatus({ suspense: true }); const { settings: { encryptionPassword, encryptionMethod }, } = useProposalResult(); + + if (phase !== InstallationPhase.Install) { + return ; + } + + if (isBusy) { + return ; + } + const usingTpm = encryptionPassword?.length > 0 && encryptionMethod === EncryptionMethods.TPM; return ( - -
- - - - - - - } - /> - - - {_("The installation on your machine is complete.")} - - {useIguana - ? _("At this point you can power off the machine.") - : _( - "At this point you can reboot the machine to log in to the new system.", - )} - - {usingTpm && } - - - - - - - - - - - -
-
+
+ + + + + + + } + /> + + + {_("The installation on your machine is complete.")} + + {useIguana + ? _("At this point you can power off the machine.") + : _( + "At this point you can reboot the machine to log in to the new system.", + )} + + {usingTpm && } + + + + + + + + + + + +
); } diff --git a/web/src/components/core/InstallationProgress.test.jsx b/web/src/components/core/InstallationProgress.test.tsx similarity index 72% rename from web/src/components/core/InstallationProgress.test.jsx rename to web/src/components/core/InstallationProgress.test.tsx index 6a3c40c650..f5c5e30f4b 100644 --- a/web/src/components/core/InstallationProgress.test.jsx +++ b/web/src/components/core/InstallationProgress.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -21,23 +21,24 @@ */ import React from "react"; - import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; - +import { InstallationPhase } from "~/types/status"; import InstallationProgress from "./InstallationProgress"; jest.mock("~/components/core/ProgressReport", () => () =>
ProgressReport Mock
); -jest.mock("~/components/questions/Questions", () => () =>
Questions Mock
); -describe.skip("InstallationProgress", () => { - it("uses 'Installing' as title", () => { - installerRender(); - screen.getByText("Installing"); - }); +jest.mock("~/queries/status", () => ({ + ...jest.requireActual("~/queries/status"), + useInstallerStatus: () => ({ isBusy: true, phase: InstallationPhase.Install }), +})); +describe("InstallationProgress", () => { it("renders progress report", () => { installerRender(); screen.getByText("ProgressReport Mock"); }); + + it.todo("redirects to root path when not in an installation phase"); + it.todo("redirects to installatino finished path if in an installation phase but not busy"); }); diff --git a/web/src/components/core/InstallationProgress.jsx b/web/src/components/core/InstallationProgress.tsx similarity index 66% rename from web/src/components/core/InstallationProgress.jsx rename to web/src/components/core/InstallationProgress.tsx index fcc3b09dd1..5c387413f4 100644 --- a/web/src/components/core/InstallationProgress.jsx +++ b/web/src/components/core/InstallationProgress.tsx @@ -23,14 +23,23 @@ import React from "react"; import { _ } from "~/i18n"; import ProgressReport from "./ProgressReport"; -import SimpleLayout from "~/SimpleLayout"; +import { InstallationPhase } from "~/types/status"; +import { PATHS } from "~/router"; +import { Navigate } from "react-router-dom"; +import { useInstallerStatus } from "~/queries/status"; function InstallationProgress() { - return ( - - - - ); + const { isBusy, phase } = useInstallerStatus({ suspense: true }); + + if (phase !== InstallationPhase.Install) { + return ; + } + + if (!isBusy) { + return ; + } + + return ; } export default InstallationProgress; diff --git a/web/src/components/core/InstallerOptions.jsx b/web/src/components/core/InstallerOptions.jsx deleted file mode 100644 index 40072fb63e..0000000000 --- a/web/src/components/core/InstallerOptions.jsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) [2022-2023] 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. - */ - -// @ts-check - -import React, { useState } from "react"; -import { useLocation } from "react-router-dom"; -import { - Button, - Flex, - Form, - FormGroup, - FormSelect, - FormSelectOption, -} from "@patternfly/react-core"; -import { Icon } from "~/components/layout"; -import { Popup } from "~/components/core"; -import { _ } from "~/i18n"; -import { localConnection } from "~/utils"; -import { useInstallerL10n } from "~/context/installerL10n"; -import supportedLanguages from "~/languages.json"; -import { useQuery } from "@tanstack/react-query"; -import { keymapsQuery } from "~/queries/l10n"; - -/** - * @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps - */ - -/** - * Renders the installer options - * - * @todo Write documentation - */ -export default function InstallerOptions() { - const { - language: initialLanguage, - keymap: initialKeymap, - changeLanguage, - changeKeymap, - } = useInstallerL10n(); - const [language, setLanguage] = useState(initialLanguage); - const [keymap, setKeymap] = useState(initialKeymap); - const { isPending, data: keymaps } = useQuery(keymapsQuery()); - const location = useLocation(); - const [isOpen, setIsOpen] = useState(false); - const [inProgress, setInProgress] = useState(false); - - // FIXME: Installer options should be available in the login too. - if (["/login", "/products/progress"].includes(location.pathname)) return; - if (isPending) return; - - const open = () => setIsOpen(true); - const close = () => { - setLanguage(initialLanguage); - setKeymap(initialKeymap); - setIsOpen(false); - }; - - const onSubmit = async (e) => { - e.preventDefault(); - setInProgress(true); - changeKeymap(keymap); - changeLanguage(language) - .then(close) - .catch(() => setInProgress(false)); - }; - - return ( - <> -