From 9cf360879bddbeb8a3fe9ff7f7d357b2ad5e0293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Tue, 22 Oct 2024 17:34:27 +0100 Subject: [PATCH 01/14] refactor(web): drop core/About component Because at this moment it does not provide any useful information for the user. Moreover, most probably a user in front of the Agama UI is aware of what Agama is, especially now that it has its own documentation site. --- web/src/components/core/About.test.tsx | 77 ------------------ web/src/components/core/About.tsx | 94 ---------------------- web/src/components/core/LoginPage.test.tsx | 10 --- web/src/components/core/LoginPage.tsx | 19 +---- web/src/components/core/index.js | 1 - web/src/components/layout/Sidebar.test.tsx | 6 -- web/src/components/layout/Sidebar.tsx | 3 +- 7 files changed, 3 insertions(+), 207 deletions(-) delete mode 100644 web/src/components/core/About.test.tsx delete mode 100644 web/src/components/core/About.tsx 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/LoginPage.test.tsx b/web/src/components/core/LoginPage.test.tsx index f28b6e192f..d03e595902 100644 --- a/web/src/components/core/LoginPage.test.tsx +++ b/web/src/components/core/LoginPage.test.tsx @@ -113,15 +113,5 @@ describe("LoginPage", () => { within(form_error).getByText(/Could not authenticate/); }); }); - - it("renders a button to know more about the project", async () => { - const { user } = plainRender(); - const button = screen.getByRole("button", { name: "More about this" }); - - await user.click(button); - - const dialog = await screen.findByRole("dialog"); - within(dialog).getByText(/About/); - }); }); }); diff --git a/web/src/components/core/LoginPage.tsx b/web/src/components/core/LoginPage.tsx index a14eae213f..fccb1b0936 100644 --- a/web/src/components/core/LoginPage.tsx +++ b/web/src/components/core/LoginPage.tsx @@ -22,18 +22,8 @@ import React, { useState } from "react"; import { Navigate } from "react-router-dom"; -import { - ActionGroup, - Button, - Card, - Flex, - FlexItem, - Form, - FormGroup, - Grid, - GridItem, -} from "@patternfly/react-core"; -import { About, EmptyState, FormValidationError, Page, PasswordInput } from "~/components/core"; +import { ActionGroup, Button, Card, Form, FormGroup, Grid, GridItem } from "@patternfly/react-core"; +import { EmptyState, FormValidationError, Page, PasswordInput } from "~/components/core"; import { Center } from "~/components/layout"; import { AuthErrors, useAuth } from "~/context/auth"; import { _ } from "~/i18n"; @@ -109,11 +99,6 @@ user privileges.", - - - - - diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index 53b8dfcbbe..057a5ea7c3 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -20,7 +20,6 @@ * find current contact information at www.suse.com. */ -export { default as About } from "./About"; export { default as ChangeProductLink } from "./ChangeProductLink"; export { default as Description } from "./Description"; export { default as Section } from "./Section"; diff --git a/web/src/components/layout/Sidebar.test.tsx b/web/src/components/layout/Sidebar.test.tsx index 6f7992158d..4ac9985d8e 100644 --- a/web/src/components/layout/Sidebar.test.tsx +++ b/web/src/components/layout/Sidebar.test.tsx @@ -25,7 +25,6 @@ import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import Sidebar from "./Sidebar"; -jest.mock("~/components/core/About", () => () =>
About Mock
); jest.mock("~/components/core/LogsButton", () => () =>
LogsButton Mock
); jest.mock("~/components/core/ChangeProductLink", () => () =>
ChangeProductLink Mock
); @@ -47,11 +46,6 @@ describe("Sidebar", () => { screen.getByRole("link", { name: "L10n" }); }); - it("mounts core/About component", () => { - installerRender(); - screen.getByText("About Mock"); - }); - it("mounts core/LogsButton component", () => { installerRender(); screen.getByText("LogsButton Mock"); diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index c0894b95fa..9b719a8a8d 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -24,7 +24,7 @@ import React from "react"; import { NavLink } from "react-router-dom"; import { Nav, NavItem, NavList, PageSidebar, PageSidebarBody, Stack } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; -import { About, LogsButton, ChangeProductLink } from "~/components/core"; +import { LogsButton, ChangeProductLink } from "~/components/core"; import { rootRoutes } from "~/router"; import { _ } from "~/i18n"; @@ -68,7 +68,6 @@ export default function Sidebar(): React.ReactNode { - From f10153bd56e96c1ec6d3c64a1247d52999bd2bad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 24 Oct 2024 12:49:10 +0100 Subject: [PATCH 02/14] fix(web): restructure internals of layout A step forward to simplify or at least improve how the application is laid out with full header and sidebar or not depending on the route or the action the installer is displaying or triggering. There are a lot of changes here and there, but basically what this commit does is * Drop SimpleLayout component * Rename Main to Layout and add two convenience predefined variants: Full and Plain. * Adapt layout/Header for displaying more or less things depending on props, sent via Layout component. * Remove the installer options "cog button" in favor of a dropdown in the header right corner and move the installer options as an element inside. Such a dropdown will hold the "Download logs" actions in the short term. * Adapt needed parts to accommodate above changes. --- web/src/App.jsx | 48 ++++--- web/src/App.test.jsx | 21 ++- web/src/SimpleLayout.jsx | 62 --------- web/src/components/core/Installation.jsx | 30 ----- .../components/core/InstallationFinished.tsx | 97 ++++++++------ .../components/core/InstallationProgress.jsx | 21 ++- web/src/components/core/InstallerOptions.jsx | 125 +++++++----------- web/src/components/core/ServerError.tsx | 37 +++--- web/src/components/core/index.js | 1 - web/src/components/layout/Header.test.tsx | 74 +++++++++-- web/src/components/layout/Header.tsx | 113 +++++++++++++--- web/src/components/layout/Icon.jsx | 2 + web/src/components/layout/Layout.tsx | 90 +++++++++++++ web/src/components/layout/Main.tsx | 42 ------ web/src/components/layout/index.js | 2 +- web/src/router.js | 45 +++++-- 16 files changed, 459 insertions(+), 351 deletions(-) delete mode 100644 web/src/SimpleLayout.jsx delete mode 100644 web/src/components/core/Installation.jsx create mode 100644 web/src/components/layout/Layout.tsx delete mode 100644 web/src/components/layout/Main.tsx diff --git a/web/src/App.jsx b/web/src/App.jsx index 444e3f3f28..7b2827eaec 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -22,19 +22,19 @@ 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. @@ -56,25 +56,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/App.test.jsx b/web/src/App.test.jsx index 0a5585f4f8..bdd87400ea 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -88,7 +88,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
); @@ -163,15 +162,29 @@ describe("App", () => { }); }); - describe("on the installaiton phase", () => { + describe("on the busy installaiton phase", () => { beforeEach(() => { mockClientStatus.phase = InstallationPhase.Install; + mockClientStatus.isBusy = true; + mockSelectedProduct = { id: "Fake product" }; + }); + + 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; + mockClientStatus.isBusy = false; mockSelectedProduct = { id: "Fake product" }; }); - 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/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/components/core/Installation.jsx b/web/src/components/core/Installation.jsx deleted file mode 100644 index b29b54b438..0000000000 --- a/web/src/components/core/Installation.jsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) [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 { InstallationProgress, InstallationFinished } from "~/components/core"; - -function Installation({ isBusy }) { - return isBusy ? : ; -} - -export default Installation; 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.jsx b/web/src/components/core/InstallationProgress.jsx index fcc3b09dd1..c0daecf34b 100644 --- a/web/src/components/core/InstallationProgress.jsx +++ b/web/src/components/core/InstallationProgress.jsx @@ -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 { useInstallerClientStatus } from "~/context/installer"; function InstallationProgress() { - return ( - - - - ); + const { isBusy, phase } = useInstallerClientStatus({ 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 index 40072fb63e..8e19ac0bbf 100644 --- a/web/src/components/core/InstallerOptions.jsx +++ b/web/src/components/core/InstallerOptions.jsx @@ -23,16 +23,7 @@ // @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 { Flex, Form, FormGroup, FormSelect, FormSelectOption } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { _ } from "~/i18n"; import { localConnection } from "~/utils"; @@ -50,7 +41,7 @@ import { keymapsQuery } from "~/queries/l10n"; * * @todo Write documentation */ -export default function InstallerOptions() { +export default function InstallerOptions({ isOpen = false, onClose }) { const { language: initialLanguage, keymap: initialKeymap, @@ -60,19 +51,14 @@ export default function InstallerOptions() { 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); + onClose(); }; const onSubmit = async (e) => { @@ -85,66 +71,57 @@ export default function InstallerOptions() { }; return ( - <> - diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 7603ff12cf..afc3c5c599 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -45,7 +45,7 @@ import { useProduct } from "~/queries/software"; import { _ } from "~/i18n"; import { InstallationPhase } from "~/types/status"; import { useInstallerStatus } from "~/queries/status"; -import { InstallerOptions } from "../core"; +import { InstallButton, InstallerOptions } from "../core"; import { useLocation } from "react-router-dom"; import { PATHS } from "~/router"; @@ -150,6 +150,9 @@ export default function Header({ + + + diff --git a/web/src/components/overview/OverviewPage.test.tsx b/web/src/components/overview/OverviewPage.test.tsx index bfea953219..c025225506 100644 --- a/web/src/components/overview/OverviewPage.test.tsx +++ b/web/src/components/overview/OverviewPage.test.tsx @@ -34,7 +34,7 @@ const tumbleweed: Product = { description: "Tumbleweed description...", }; -const mockIssuesList = new IssuesList([], [], [], []); +let mockIssuesList: IssuesList = new IssuesList([], [], [], []); jest.mock("~/queries/software", () => ({ ...jest.requireActual("~/queries/software"), @@ -45,6 +45,7 @@ jest.mock("~/queries/software", () => ({ jest.mock("~/queries/issues", () => ({ ...jest.requireActual("~/queries/issues"), useIssuesChanges: () => jest.fn().mockResolvedValue(mockIssuesList), + useAllIssues: () => mockIssuesList, })); jest.mock("~/components/overview/L10nSection", () => () =>
Localization Section
); @@ -60,4 +61,42 @@ describe("when a product is selected", () => { screen.findByText("Software Section"); screen.findByText("Install Button"); }); + + it("renders found issues, if any", () => {}); +}); + +describe("when there are issues", () => { + beforeEach(() => { + mockIssuesList = new IssuesList( + [ + { + description: "Fake Issue", + details: "Fake Issue details", + source: 0, + severity: 1, + }, + ], + [], + [], + [], + ); + }); + + it("renders the issues section", () => { + installerRender(); + screen.findByText("Installation blocking issues"); + screen.findByText("Fake Issue"); + screen.findByText("Fake Issue details"); + }); +}); + +describe("when there are no issues", () => { + beforeEach(() => { + mockIssuesList = new IssuesList([], [], [], []); + }); + + it("does not render the issues section", () => { + installerRender(); + expect(screen.queryByText("Installation blocking issues")).toBeNull(); + }); }); diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx index 7e992ff7a3..f72e598b25 100644 --- a/web/src/components/overview/OverviewPage.tsx +++ b/web/src/components/overview/OverviewPage.tsx @@ -35,8 +35,7 @@ import { Stack, } from "@patternfly/react-core"; import { Link } from "react-router-dom"; -import { Center } from "~/components/layout"; -import { EmptyState, InstallButton, Page } from "~/components/core"; +import { Page } from "~/components/core"; import L10nSection from "./L10nSection"; import StorageSection from "./StorageSection"; import SoftwareSection from "./SoftwareSection"; @@ -44,14 +43,6 @@ import { _ } from "~/i18n"; import { useAllIssues } from "~/queries/issues"; import { IssuesList as IssuesListType, IssueSeverity } from "~/types/issues"; -const ReadyForInstallation = () => ( -
- - - -
-); - const IssuesList = ({ issues }: { issues: IssuesListType }) => { const scopeHeaders = { users: _("Users"), @@ -90,19 +81,13 @@ const IssuesList = ({ issues }: { issues: IssuesListType }) => { ); }; -const ResultSection = () => { - const issues = useAllIssues(); - - const resultSectionProps = issues.isEmpty - ? {} - : { - title: _("Installation"), - description: _("Before installing, please check the following problems."), - }; - +const IssuesSection = ({ issues }: { issues: IssuesListType }) => { return ( - - {issues.isEmpty ? : } + + ); }; @@ -123,6 +108,8 @@ const OverviewSection = () => ( ); export default function OverviewPage() { + const issues = useAllIssues(); + return ( @@ -136,12 +123,14 @@ export default function OverviewPage() { - + - - - + {!issues.isEmpty && ( + + + + )} From 4547eb91b1a8d2d6b106f04ec385be0ce9f844a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 25 Oct 2024 17:29:42 +0100 Subject: [PATCH 05/14] fix(web): make broken tests work again They stop working by the lack of `useAllIssues` mock after moving the core/InstallaButton to the core/Header and start using such and issues query to decide if the button is displayed or not. --- web/src/App.test.jsx | 1 + web/src/components/layout/Header.test.tsx | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index bdd87400ea..e1628a3575 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -62,6 +62,7 @@ jest.mock("~/queries/l10n", () => ({ jest.mock("~/queries/issues", () => ({ ...jest.requireActual("~/queries/issues"), useIssuesChanges: () => jest.fn(), + useAllIssues: () => ({ isEmtpy: true }), })); jest.mock("~/queries/storage", () => ({ diff --git a/web/src/components/layout/Header.test.tsx b/web/src/components/layout/Header.test.tsx index 5518f0a8d1..7be86b62ff 100644 --- a/web/src/components/layout/Header.test.tsx +++ b/web/src/components/layout/Header.test.tsx @@ -57,6 +57,11 @@ jest.mock("~/queries/status", () => ({ }), })); +jest.mock("~/queries/issues", () => ({ + ...jest.requireActual("~/queries/issues"), + useAllIssues: () => ({ isEmtpy: true }), +})); + const doesNotRenderInstallerL10nOptions = () => it("does not render the installer localization options", async () => { const { user } = installerRender(
); From cf0d3f54f45ec88b30edb4b717b9ad495cef5f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 25 Oct 2024 17:32:54 +0100 Subject: [PATCH 06/14] fix(web): drop leftover className --- web/src/components/layout/Header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index afc3c5c599..46640818d7 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -151,7 +151,7 @@ export default function Header({ - + From 7a35334f4b383790bd3f6ea11aff682eeb838aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Fri, 25 Oct 2024 17:50:36 +0100 Subject: [PATCH 07/14] refactor(web): tweak ChangeProduct button look&feel For adding some space between the action and the viewport edges, without investing too much time on it because its final appeareance and location is not decided yet. --- web/src/components/layout/Sidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index c081fbd2b9..aa768d0218 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -65,8 +65,8 @@ export default function Sidebar(): React.ReactNode { - - + + From e02c3a2c36c8ab8278e8b35daec986d330bb9087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Sun, 27 Oct 2024 22:10:40 +0000 Subject: [PATCH 08/14] refactor(web): migrate some files to TypeScript More specifically, the .js or .jsx files touched in previous commits for https://github.com/agama-project/agama/pull/1690. Apart from the expected changes to make the migration possible, there were two changes that worth a remark: * Make optional `description` and `icon` for the software/Product type. * Add the `custom.d.ts` file and make tsconfig.json file aware of it in order to avoid TypeScript complaints in the src/layout/Icon.ts file because missing types in the icons library. To know more read https://webpack.js.org/guides/typescript/#importing-other-assets --- web/custom.d.ts | 31 +++++++++++++++++++ web/src/{App.test.jsx => App.test.tsx} | 24 +++++++------- web/src/{App.jsx => App.tsx} | 6 +--- ...test.jsx => InstallationProgress.test.tsx} | 17 +++++----- ...nProgress.jsx => InstallationProgress.tsx} | 6 ++-- ...tallerOptions.jsx => InstallerOptions.tsx} | 15 +++++---- ...eport.test.jsx => ProgressReport.test.tsx} | 4 +-- ...{ProgressReport.jsx => ProgressReport.tsx} | 31 ++++++++++--------- .../layout/{Icon.test.jsx => Icon.test.tsx} | 4 +-- .../components/layout/{Icon.jsx => Icon.tsx} | 26 ++++++---------- web/src/types/software.ts | 4 +-- web/tsconfig.json | 2 +- web/webpack.config.js | 2 +- 13 files changed, 96 insertions(+), 76 deletions(-) create mode 100644 web/custom.d.ts rename web/src/{App.test.jsx => App.test.tsx} (91%) rename web/src/{App.jsx => App.tsx} (93%) rename web/src/components/core/{InstallationProgress.test.jsx => InstallationProgress.test.tsx} (74%) rename web/src/components/core/{InstallationProgress.jsx => InstallationProgress.tsx} (90%) rename web/src/components/core/{InstallerOptions.jsx => InstallerOptions.tsx} (94%) rename web/src/components/core/{ProgressReport.test.jsx => ProgressReport.test.tsx} (95%) rename web/src/components/core/{ProgressReport.jsx => ProgressReport.tsx} (85%) rename web/src/components/layout/{Icon.test.jsx => Icon.test.tsx} (99%) rename web/src/components/layout/{Icon.jsx => Icon.tsx} (91%) diff --git a/web/custom.d.ts b/web/custom.d.ts new file mode 100644 index 0000000000..68715cd718 --- /dev/null +++ b/web/custom.d.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +declare module "*.svg" { + const content: React.FunctionComponent>; + export default content; +} + +declare module "*.svg?component" { + const content: React.FunctionComponent>; + export default content; +} diff --git a/web/src/App.test.jsx b/web/src/App.test.tsx similarity index 91% rename from web/src/App.test.jsx rename to web/src/App.test.tsx index e1628a3575..71ce5d07b8 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.tsx @@ -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"), @@ -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 () => { @@ -167,7 +167,7 @@ describe("App", () => { beforeEach(() => { mockClientStatus.phase = InstallationPhase.Install; mockClientStatus.isBusy = true; - mockSelectedProduct = { id: "Fake product" }; + mockSelectedProduct = tumbleweed; }); it("navigates to installation progress", async () => { @@ -180,7 +180,7 @@ describe("App", () => { beforeEach(() => { mockClientStatus.phase = InstallationPhase.Install; mockClientStatus.isBusy = false; - mockSelectedProduct = { id: "Fake product" }; + mockSelectedProduct = tumbleweed; }); it("navigates to installation finished", async () => { diff --git a/web/src/App.jsx b/web/src/App.tsx similarity index 93% rename from web/src/App.jsx rename to web/src/App.tsx index 7b2827eaec..3057801f3b 100644 --- a/web/src/App.jsx +++ b/web/src/App.tsx @@ -38,10 +38,6 @@ 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(); @@ -82,7 +78,7 @@ function App() { } if (selectedProduct === undefined && location.pathname !== PRODUCT_PATHS.root) { - return ; + return ; } if ( diff --git a/web/src/components/core/InstallationProgress.test.jsx b/web/src/components/core/InstallationProgress.test.tsx similarity index 74% rename from web/src/components/core/InstallationProgress.test.jsx rename to web/src/components/core/InstallationProgress.test.tsx index 6a3c40c650..5ceed02538 100644 --- a/web/src/components/core/InstallationProgress.test.jsx +++ b/web/src/components/core/InstallationProgress.test.tsx @@ -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 90% rename from web/src/components/core/InstallationProgress.jsx rename to web/src/components/core/InstallationProgress.tsx index c0daecf34b..5c387413f4 100644 --- a/web/src/components/core/InstallationProgress.jsx +++ b/web/src/components/core/InstallationProgress.tsx @@ -26,10 +26,10 @@ import ProgressReport from "./ProgressReport"; import { InstallationPhase } from "~/types/status"; import { PATHS } from "~/router"; import { Navigate } from "react-router-dom"; -import { useInstallerClientStatus } from "~/context/installer"; +import { useInstallerStatus } from "~/queries/status"; function InstallationProgress() { - const { isBusy, phase } = useInstallerClientStatus({ suspense: true }); + const { isBusy, phase } = useInstallerStatus({ suspense: true }); if (phase !== InstallationPhase.Install) { return ; @@ -39,7 +39,7 @@ function InstallationProgress() { return ; } - return ; + return ; } export default InstallationProgress; diff --git a/web/src/components/core/InstallerOptions.jsx b/web/src/components/core/InstallerOptions.tsx similarity index 94% rename from web/src/components/core/InstallerOptions.jsx rename to web/src/components/core/InstallerOptions.tsx index 8e19ac0bbf..2e338579b0 100644 --- a/web/src/components/core/InstallerOptions.jsx +++ b/web/src/components/core/InstallerOptions.tsx @@ -20,8 +20,6 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React, { useState } from "react"; import { Flex, Form, FormGroup, FormSelect, FormSelectOption } from "@patternfly/react-core"; import { Popup } from "~/components/core"; @@ -32,16 +30,17 @@ import supportedLanguages from "~/languages.json"; import { useQuery } from "@tanstack/react-query"; import { keymapsQuery } from "~/queries/l10n"; -/** - * @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps - */ +type InstallerOptionsProps = { + isOpen: boolean; + onClose?: () => void; +}; /** * Renders the installer options * * @todo Write documentation */ -export default function InstallerOptions({ isOpen = false, onClose }) { +export default function InstallerOptions({ isOpen = false, onClose }: InstallerOptionsProps) { const { language: initialLanguage, keymap: initialKeymap, @@ -51,7 +50,7 @@ export default function InstallerOptions({ isOpen = false, onClose }) { const [language, setLanguage] = useState(initialLanguage); const [keymap, setKeymap] = useState(initialKeymap); const { isPending, data: keymaps } = useQuery(keymapsQuery()); - const [inProgress, setInProgress] = useState(false); + const [inProgress, setInProgress] = useState(false); if (isPending) return; @@ -61,7 +60,7 @@ export default function InstallerOptions({ isOpen = false, onClose }) { onClose(); }; - const onSubmit = async (e) => { + const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setInProgress(true); changeKeymap(keymap); diff --git a/web/src/components/core/ProgressReport.test.jsx b/web/src/components/core/ProgressReport.test.tsx similarity index 95% rename from web/src/components/core/ProgressReport.test.jsx rename to web/src/components/core/ProgressReport.test.tsx index c16804b020..53059b87ff 100644 --- a/web/src/components/core/ProgressReport.test.jsx +++ b/web/src/components/core/ProgressReport.test.tsx @@ -54,7 +54,7 @@ describe("ProgressReport", () => { }); it("shows the progress including the details", () => { - plainRender(); + plainRender(); expect(screen.getByText(/Partition disks/)).toBeInTheDocument(); expect(screen.getByText(/Install software/)).toBeInTheDocument(); @@ -84,7 +84,7 @@ describe("ProgressReport", () => { }); it("shows the progress including the details", () => { - plainRender(); + plainRender(); expect(screen.getByText(/Partition disks/)).toBeInTheDocument(); expect(screen.getByText(/Install software/)).toBeInTheDocument(); diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.tsx similarity index 85% rename from web/src/components/core/ProgressReport.jsx rename to web/src/components/core/ProgressReport.tsx index 6fd9267f00..6e5fc3ec22 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.tsx @@ -29,6 +29,7 @@ import { GridItem, ProgressStep, ProgressStepper, + ProgressStepProps, Spinner, Stack, Truncate, @@ -37,10 +38,19 @@ import { import { _ } from "~/i18n"; import { Center } from "~/components/layout"; import { useProgress, useProgressChanges, useResetProgress } from "~/queries/progress"; +import { Progress as ProgressType } from "~/types/progress"; + +type StepProps = { + id: string; + titleId: string; + isCurrent: boolean; + variant?: ProgressStepProps["variant"]; + description?: ProgressStepProps["description"]; +}; const Progress = ({ steps, step, firstStep, detail }) => { - const stepProperties = (stepNumber) => { - const properties = { + const stepProperties = (stepNumber: number): StepProps => { + const properties: StepProps = { isCurrent: stepNumber === step.current, id: `step-${stepNumber}-id`, titleId: `step-${stepNumber}-title`, @@ -85,7 +95,7 @@ const Progress = ({ steps, step, firstStep, detail }) => { {firstStep} )} - {steps.map((description, idx) => { + {steps.map((description: StepProps["description"], idx: number) => { return ( {description} @@ -96,18 +106,16 @@ const Progress = ({ steps, step, firstStep, detail }) => { ); }; -function findDetail(progresses) { +function findDetail(progresses: ProgressType[]) { return progresses.find((progress) => { return progress?.finished === false; }); } /** - * @component - * * Shows progress steps when a product is selected. */ -function ProgressReport({ title, firstStep }) { +function ProgressReport({ title, firstStep }: { title: string; firstStep?: React.ReactNode }) { useResetProgress(); const progress = useProgress("manager", { suspense: true }); const [steps, setSteps] = useState(progress.steps); @@ -123,14 +131,7 @@ function ProgressReport({ title, firstStep }) { const detail = findDetail([softwareProgress, storageProgress]); const Content = () => ( - + ); return ( diff --git a/web/src/components/layout/Icon.test.jsx b/web/src/components/layout/Icon.test.tsx similarity index 99% rename from web/src/components/layout/Icon.test.jsx rename to web/src/components/layout/Icon.test.tsx index 871796fc52..58161dc707 100644 --- a/web/src/components/layout/Icon.test.jsx +++ b/web/src/components/layout/Icon.test.tsx @@ -20,13 +20,11 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; import { plainRender } from "~/test-utils"; import { Icon } from "~/components/layout"; -let consoleErrorSpy; +let consoleErrorSpy: jest.SpyInstance; describe("Icon", () => { beforeAll(() => { diff --git a/web/src/components/layout/Icon.jsx b/web/src/components/layout/Icon.tsx similarity index 91% rename from web/src/components/layout/Icon.jsx rename to web/src/components/layout/Icon.tsx index 521840ce0b..acab440e37 100644 --- a/web/src/components/layout/Icon.jsx +++ b/web/src/components/layout/Icon.tsx @@ -87,11 +87,6 @@ import WifiOff from "@icons/wifi_off.svg?component"; import { SiLinux } from "@icons-pack/react-simple-icons"; -/** - * @typedef {string|number} IconSize - * @typedef {keyof icons} IconName - */ - const icons = { add_a_photo: AddAPhoto, apps: Apps, @@ -157,6 +152,13 @@ const icons = { const PREDEFINED_SIZES = ["xxxs", "xxs", "xs", "s", "m", "l", "xl", "xxl", "xxxl"]; +type IconProps = React.SVGAttributes & { + /** Name of the desired icon */ + name: keyof typeof icons; + /** Size used for both, width and height.It can be a CSS unit or one of PREDEFINED_SIZES */ + size?: string | number; +}; + /** * Agama Icon component * @@ -167,19 +169,11 @@ const PREDEFINED_SIZES = ["xxxs", "xxs", "xs", "s", "m", "l", "xl", "xxl", "xxxl * @example * * - * @param {object} props - Component props - * @param {IconName} props.name - Name of the desired icon. - * @param {string} [props.className=""] - CSS classes. - * @param {IconSize} [props.size] - Size used for both, width and height. - * @param {string} [props.color] - Color for the icon, currently based on PF - * text utils - * It can be a CSS unit or one of PREDEFINED_SIZES. - * @param {object} [props.otherProps] Other props sent to SVG icon. Please, note - * that width and height will be overwritten by the size value if it was given. + * @note width and height props will be overwritten by the size value if it was given. * * @returns {JSX.Element|null} null if requested icon is not available or given a falsy value as name; JSX block otherwise. */ -export default function Icon({ name, size, color, ...otherProps }) { +export default function Icon({ name, size, color, ...otherProps }: IconProps) { // NOTE: Reaching this is unlikely, but let's be safe. if (!name || !icons[name]) { console.error(`Icon '${name}' not found.`); @@ -188,7 +182,7 @@ export default function Icon({ name, size, color, ...otherProps }) { let classes = otherProps.className || ""; - if (size && PREDEFINED_SIZES.includes(size)) { + if (size && typeof size === "string" && PREDEFINED_SIZES.includes(size)) { classes += ` icon-${size}`; } else if (size) { otherProps.width = size; diff --git a/web/src/types/software.ts b/web/src/types/software.ts index b80d8b9a52..822d60855a 100644 --- a/web/src/types/software.ts +++ b/web/src/types/software.ts @@ -38,9 +38,9 @@ type Product = { /** Product name (e.g., "openSUSE Leap 15.4") */ name: string; /** Product description */ - description: string; + description?: string; /** Product icon (e.g., "default.svg") */ - icon: string; + icon?: string; }; type PatternsSelection = { [key: string]: SelectedBy }; diff --git a/web/tsconfig.json b/web/tsconfig.json index cef27da6b5..bc9ff63fb9 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -17,5 +17,5 @@ "@icons/*": ["node_modules/@material-symbols/svg-400/outlined/*"] } }, - "include": ["src"] + "include": ["src", "custom.d.ts"] } diff --git a/web/webpack.config.js b/web/webpack.config.js index f7df3e4c7a..ac73bbbbe2 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -220,7 +220,7 @@ module.exports = { }, { test: /\.svg$/i, - issuer: /\.jsx?$/, + issuer: /\.(j|t)sx?$/, resourceQuery: /component/, // *.svg?component use: ["@svgr/webpack"], }, From 1c457eee31bc39b24fe00a3aab23de3e5a14bc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Sun, 27 Oct 2024 23:04:50 +0000 Subject: [PATCH 09/14] doc(web): update copyright date for touched files --- web/src/App.test.tsx | 2 +- web/src/App.tsx | 2 +- web/src/components/core/InstallButton.test.tsx | 2 +- web/src/components/core/InstallButton.tsx | 2 +- web/src/components/core/InstallationProgress.test.tsx | 2 +- web/src/components/core/InstallerOptions.tsx | 2 +- web/src/components/layout/Icon.test.tsx | 2 +- web/src/index.js | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 71ce5d07b8..3c4aa32bbb 100644 --- a/web/src/App.test.tsx +++ 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. * diff --git a/web/src/App.tsx b/web/src/App.tsx index 3057801f3b..9fe9761571 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/core/InstallButton.test.tsx b/web/src/components/core/InstallButton.test.tsx index 8bf2c289ff..035ecd281b 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. * diff --git a/web/src/components/core/InstallButton.tsx b/web/src/components/core/InstallButton.tsx index cb9de5798d..1078595160 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. * diff --git a/web/src/components/core/InstallationProgress.test.tsx b/web/src/components/core/InstallationProgress.test.tsx index 5ceed02538..f5c5e30f4b 100644 --- a/web/src/components/core/InstallationProgress.test.tsx +++ 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. * diff --git a/web/src/components/core/InstallerOptions.tsx b/web/src/components/core/InstallerOptions.tsx index 2e338579b0..cb109ee4fc 100644 --- a/web/src/components/core/InstallerOptions.tsx +++ b/web/src/components/core/InstallerOptions.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/components/layout/Icon.test.tsx b/web/src/components/layout/Icon.test.tsx index 58161dc707..89e978f2af 100644 --- a/web/src/components/layout/Icon.test.tsx +++ b/web/src/components/layout/Icon.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023] SUSE LLC + * Copyright (c) [2023-2024] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/index.js b/web/src/index.js index 185360df48..e678bef2a8 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * From 2c8c2fe5e13066775985b701708b6d1d368a7b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 28 Oct 2024 10:21:58 +0000 Subject: [PATCH 10/14] fix(web): do not show install button in product selection --- web/src/components/core/InstallButton.test.tsx | 18 +++++++++++++++--- web/src/components/core/InstallButton.tsx | 5 +++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/web/src/components/core/InstallButton.test.tsx b/web/src/components/core/InstallButton.test.tsx index 035ecd281b..a77a79ff00 100644 --- a/web/src/components/core/InstallButton.test.tsx +++ b/web/src/components/core/InstallButton.test.tsx @@ -22,9 +22,10 @@ import React from "react"; import { screen, waitFor } from "@testing-library/react"; -import { installerRender, 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; @@ -73,7 +74,7 @@ describe("when there are not installation issues", () => { }); it("starts the installation after user clicks on it and accept the confirmation", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const button = await screen.findByRole("button", { name: "Install" }); await user.click(button); @@ -83,7 +84,7 @@ describe("when there are not installation issues", () => { }); it("does not start the installation if the user clicks on it but cancels the confirmation", async () => { - const { user } = plainRender(); + const { user } = installerRender(); const button = await screen.findByRole("button", { name: "Install" }); await user.click(button); @@ -95,4 +96,15 @@ describe("when there are not installation issues", () => { expect(screen.queryByRole("button", { name: "Continue" })).not.toBeInTheDocument(); }); }); + + describe("but installer is in the product selection path", () => { + beforeEach(() => { + mockRoutes(PRODUCT_PATHS.changeProduct); + }); + + 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 1078595160..83d4dbca38 100644 --- a/web/src/components/core/InstallButton.tsx +++ b/web/src/components/core/InstallButton.tsx @@ -28,6 +28,8 @@ 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 ( @@ -64,8 +66,11 @@ according to the provided installation settings.", 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. + if (location.pathname === PRODUCT_PATHS.changeProduct) return; const open = async () => setIsOpen(true); const close = () => setIsOpen(false); From d14ff337e85e821fb6c81ad70700eee621330741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 28 Oct 2024 10:52:05 +0000 Subject: [PATCH 11/14] feature(web): render issues link at header when needed As a counterpart of the InstallButton, an IssuesLink is displayed when the installation is not possible to make the user aware why the InstallButton is not there. This is a temporary navigation link to the overview page that, most probably, will be replaced by a toggler for a Notification Drawer https://www.patternfly.org/components/notification-drawer always accessible from the top bar. --- web/src/components/core/IssuesLink.test.tsx | 86 +++++++++++++++++++++ web/src/components/core/IssuesLink.tsx | 58 ++++++++++++++ web/src/components/core/Link.tsx | 2 +- web/src/components/core/index.js | 1 + web/src/components/layout/Header.tsx | 3 +- 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 web/src/components/core/IssuesLink.test.tsx create mode 100644 web/src/components/core/IssuesLink.tsx diff --git a/web/src/components/core/IssuesLink.test.tsx b/web/src/components/core/IssuesLink.test.tsx new file mode 100644 index 0000000000..81385bfc32 --- /dev/null +++ b/web/src/components/core/IssuesLink.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 from "react"; +import { screen } from "@testing-library/react"; +import { installerRender, mockRoutes } from "~/test-utils"; +import { IssuesLink } 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(), +})); + +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 the issues link", () => { + installerRender(); + screen.getByRole("link", { name: "Installation issues" }); + }); + + describe("but installer is in the product selection path", () => { + beforeEach(() => { + mockRoutes(PRODUCT_PATHS.changeProduct); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); +}); + +describe("when there are no installation issues", () => { + beforeEach(() => { + mockIssuesList = new IssuesList([], [], [], []); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/web/src/components/core/IssuesLink.tsx b/web/src/components/core/IssuesLink.tsx new file mode 100644 index 0000000000..9fa1e48a87 --- /dev/null +++ b/web/src/components/core/IssuesLink.tsx @@ -0,0 +1,58 @@ +/* + * 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 from "react"; +import { useLocation } from "react-router-dom"; +import { useAllIssues } from "~/queries/issues"; +import Link, { LinkProps } from "~/components/core/Link"; +import { PATHS as PRODUCT_PATHS } from "~/routes/products"; +import { PATHS as ROOT_PATHS } from "~/router"; +import { Icon } from "../layout"; +import { Tooltip } from "@patternfly/react-core"; +import { _ } from "~/i18n"; + +/** + * Installation issues link + * + * As a counterpart of the InstallButton, it shows a button with a warning icon + * when the installation is not possible because there are installation issues. + */ +const IssuesLink = (props: Omit) => { + const issues = useAllIssues(); + const location = useLocation(); + + if (issues.isEmpty) return; + // Do not show the button if the user is about to change the product. + if (location.pathname === PRODUCT_PATHS.changeProduct) return; + + return ( + + + + + + ); +}; + +export default IssuesLink; diff --git a/web/src/components/core/Link.tsx b/web/src/components/core/Link.tsx index 5975d56c3a..dcc570f2cb 100644 --- a/web/src/components/core/Link.tsx +++ b/web/src/components/core/Link.tsx @@ -24,7 +24,7 @@ import React from "react"; import { Button, ButtonProps } from "@patternfly/react-core"; import { useHref } from "react-router-dom"; -type LinkProps = Omit & { +export type LinkProps = Omit & { /** The target route */ to: string; /** Whether use PF/Button primary variant */ diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index 8bdb3e5528..781be69513 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -32,6 +32,7 @@ export { default as EmailInput } from "./EmailInput"; export { default as InstallationFinished } from "./InstallationFinished"; export { default as InstallationProgress } from "./InstallationProgress"; export { default as InstallButton } from "./InstallButton"; +export { default as IssuesLink } from "./IssuesLink"; export { default as IssuesHint } from "./IssuesHint"; export { default as SectionSkeleton } from "./SectionSkeleton"; export { default as ListSearch } from "./ListSearch"; diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 46640818d7..6f60a3721c 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -45,7 +45,7 @@ import { useProduct } from "~/queries/software"; import { _ } from "~/i18n"; import { InstallationPhase } from "~/types/status"; import { useInstallerStatus } from "~/queries/status"; -import { InstallButton, InstallerOptions } from "../core"; +import { InstallButton, InstallerOptions, IssuesLink } from "~/components/core"; import { useLocation } from "react-router-dom"; import { PATHS } from "~/router"; @@ -151,6 +151,7 @@ export default function Header({ + From e9f33e0ff1e56f71b94b4c110a05090f585f04d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 28 Oct 2024 11:03:28 +0000 Subject: [PATCH 12/14] fix(web): adjust when install/isuses actions are shown It make no sense to show neither, InstallButton nor IssuesLink, when the installer is configuring a product. So, this change avoid mounting them at product progress path. --- web/src/components/core/InstallButton.test.tsx | 13 ++++++++++++- web/src/components/core/InstallButton.tsx | 5 +++-- web/src/components/core/IssuesLink.test.tsx | 13 ++++++++++++- web/src/components/core/IssuesLink.tsx | 5 +++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/web/src/components/core/InstallButton.test.tsx b/web/src/components/core/InstallButton.test.tsx index a77a79ff00..1adb4570c8 100644 --- a/web/src/components/core/InstallButton.test.tsx +++ b/web/src/components/core/InstallButton.test.tsx @@ -97,7 +97,7 @@ describe("when there are not installation issues", () => { }); }); - describe("but installer is in the product selection path", () => { + describe("but installer is rendering the product selection", () => { beforeEach(() => { mockRoutes(PRODUCT_PATHS.changeProduct); }); @@ -107,4 +107,15 @@ describe("when there are not installation issues", () => { 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 83d4dbca38..dce5853ce7 100644 --- a/web/src/components/core/InstallButton.tsx +++ b/web/src/components/core/InstallButton.tsx @@ -69,8 +69,9 @@ const InstallButton = (props: Omit) => { const location = useLocation(); if (!issues.isEmpty) return; - // Do not show the button if the user is about to change the product. - if (location.pathname === PRODUCT_PATHS.changeProduct) 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); diff --git a/web/src/components/core/IssuesLink.test.tsx b/web/src/components/core/IssuesLink.test.tsx index 81385bfc32..de6ae3a37f 100644 --- a/web/src/components/core/IssuesLink.test.tsx +++ b/web/src/components/core/IssuesLink.test.tsx @@ -62,7 +62,7 @@ describe("when there are installation issues", () => { screen.getByRole("link", { name: "Installation issues" }); }); - describe("but installer is in the product selection path", () => { + describe("but installer is rendering the product selection", () => { beforeEach(() => { mockRoutes(PRODUCT_PATHS.changeProduct); }); @@ -72,6 +72,17 @@ describe("when there are installation issues", () => { expect(container).toBeEmptyDOMElement(); }); }); + + describe("but installer is configuring the product", () => { + beforeEach(() => { + mockRoutes(PRODUCT_PATHS.progress); + }); + + it("renders nothing", () => { + const { container } = installerRender(); + expect(container).toBeEmptyDOMElement(); + }); + }); }); describe("when there are no installation issues", () => { diff --git a/web/src/components/core/IssuesLink.tsx b/web/src/components/core/IssuesLink.tsx index 9fa1e48a87..ea596df2e6 100644 --- a/web/src/components/core/IssuesLink.tsx +++ b/web/src/components/core/IssuesLink.tsx @@ -41,8 +41,9 @@ const IssuesLink = (props: Omit) => { const location = useLocation(); if (issues.isEmpty) return; - // Do not show the button if the user is about to change the product. - if (location.pathname === PRODUCT_PATHS.changeProduct) 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; return ( Date: Mon, 28 Oct 2024 11:22:44 +0000 Subject: [PATCH 13/14] fix(web): improve top bar actions look&feel --- web/src/assets/styles/patternfly-overrides.scss | 6 +++++- web/src/components/core/InstallButton.tsx | 2 +- web/src/components/layout/Header.test.tsx | 2 +- web/src/components/layout/Header.tsx | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index d87a36d06b..2c6ab76be7 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -82,6 +82,10 @@ .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 { @@ -289,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/InstallButton.tsx b/web/src/components/core/InstallButton.tsx index dce5853ce7..0d7c56512f 100644 --- a/web/src/components/core/InstallButton.tsx +++ b/web/src/components/core/InstallButton.tsx @@ -78,7 +78,7 @@ const InstallButton = (props: Omit) => { return ( <> - diff --git a/web/src/components/layout/Header.test.tsx b/web/src/components/layout/Header.test.tsx index 7be86b62ff..cd2e5c7c8e 100644 --- a/web/src/components/layout/Header.test.tsx +++ b/web/src/components/layout/Header.test.tsx @@ -59,7 +59,7 @@ jest.mock("~/queries/status", () => ({ jest.mock("~/queries/issues", () => ({ ...jest.requireActual("~/queries/issues"), - useAllIssues: () => ({ isEmtpy: true }), + useAllIssues: () => ({ isEmpty: true }), })); const doesNotRenderInstallerL10nOptions = () => diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 6f60a3721c..c187652f19 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -150,8 +150,8 @@ export default function Header({ - - + + From 956ca58fef565bdabdbc78cb470bd83adda7f66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Mon, 28 Oct 2024 19:34:26 +0000 Subject: [PATCH 14/14] doc(web): add entry in changes file --- web/package/agama-web-ui.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 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