diff --git a/web/src/client/software.js b/web/src/client/software.js index 87d2ae3850..bb3157fdc6 100644 --- a/web/src/client/software.js +++ b/web/src/client/software.js @@ -48,7 +48,7 @@ const REGISTRATION_IFACE = "org.opensuse.Agama1.Registration"; /** * @typedef {object} ActionResult - * @property {boolean} success - Whether the action was successfuly done. + * @property {boolean} success - Whether the action was successfully done. * @property {string} message - Result message. */ diff --git a/web/src/components/core/IssuesPage.jsx b/web/src/components/core/IssuesPage.jsx index 7d137c5fca..6165754b5c 100644 --- a/web/src/components/core/IssuesPage.jsx +++ b/web/src/components/core/IssuesPage.jsx @@ -21,17 +21,17 @@ import React, { useCallback, useEffect, useState } from "react"; -import { HelperText, HelperTextItem, Skeleton } from "@patternfly/react-core"; +import { HelperText, HelperTextItem } from "@patternfly/react-core"; import { partition, useCancellablePromise } from "~/utils"; -import { If, Page, Section } from "~/components/core"; +import { If, Page, Section, SectionSkeleton } from "~/components/core"; import { Icon } from "~/components/layout"; import { useInstallerClient } from "~/context/installer"; import { useNotification } from "~/context/notification"; import { _ } from "~/i18n"; /** - * Renders an issue + * Item representing an issue. * @component * * @param {object} props @@ -58,54 +58,68 @@ const IssueItem = ({ issue }) => { }; /** - * Generates a specific section with issues + * Generates issue items sorted by severity. * @component * * @param {object} props * @param {import ("~/client/mixins").Issue[]} props.issues - * @param {object} props.props */ -const IssuesSection = ({ issues, ...props }) => { - if (issues.length === 0) return null; - +const IssueItems = ({ issues = [] }) => { const sortedIssues = partition(issues, i => i.severity === "error").flat(); - const issueItems = sortedIssues.map((issue, index) => { + return sortedIssues.map((issue, index) => { return ; }); - - return ( -
- {issueItems} -
- ); }; /** - * Generates the sections with issues + * Generates the sections with issues. * @component * * @param {object} props * @param {import ("~/client/issues").ClientsIssues} props.issues */ const IssuesSections = ({ issues }) => { + const productIssues = issues.product || []; + const storageIssues = issues.storage || []; + const softwareIssues = issues.software || []; + return ( - + <> + 0} + then={ +
+ +
+ } + /> + 0} + then={ +
+ +
+ } + /> + 0} + then={ +
+ +
+ } + /> + ); }; /** - * Generates the content for each section with issues. If there are no issues, then a success - * message is shown. + * Generates sections with issues. If there are no issues, then a success message is shown. * @component * * @param {object} props - * @param {import ("~/client/issues").ClientsIssues} props.issues + * @param {import ("~/client").Issues} props.issues */ const IssuesContent = ({ issues }) => { const NoIssues = () => { @@ -130,28 +144,32 @@ const IssuesContent = ({ issues }) => { }; /** - * Page to show all issues per section + * Page to show all issues. * @component */ export default function IssuesPage() { const [isLoading, setIsLoading] = useState(true); - const [issues, setIssues] = useState({}); - const { issues: client } = useInstallerClient(); + const [issues, setIssues] = useState(); + const client = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); - const [, updateNotification] = useNotification(); + const [notification, updateNotification] = useNotification(); - const loadIssues = useCallback(async () => { + const load = useCallback(async () => { setIsLoading(true); - const allIssues = await cancellablePromise(client.getAll()); - setIssues(allIssues); + const issues = await cancellablePromise(client.issues()); setIsLoading(false); - updateNotification({ issues: false }); - }, [client, cancellablePromise, setIssues, setIsLoading, updateNotification]); + return issues; + }, [client, cancellablePromise, setIsLoading]); + + const update = useCallback((issues) => { + setIssues(current => ({ ...current, ...issues })); + if (notification.issues) updateNotification({ issues: false }); + }, [notification, setIssues, updateNotification]); useEffect(() => { - loadIssues(); - return client.onIssuesChange(loadIssues); - }, [client, loadIssues]); + load().then(update); + return client.onIssuesChange(update); + }, [client, load, update]); return ( } + then={} else={} /> diff --git a/web/src/components/core/IssuesPage.test.jsx b/web/src/components/core/IssuesPage.test.jsx index cd832d397e..a46d1acd28 100644 --- a/web/src/components/core/IssuesPage.test.jsx +++ b/web/src/components/core/IssuesPage.test.jsx @@ -20,37 +20,43 @@ */ import React from "react"; -import { screen, within } from "@testing-library/react"; -import { installerRender, withNotificationProvider } from "~/test-utils"; +import { act, screen, waitFor, within } from "@testing-library/react"; +import { installerRender, createCallbackMock, withNotificationProvider } from "~/test-utils"; import { createClient } from "~/client"; import { IssuesPage } from "~/components/core"; jest.mock("~/client"); jest.mock("@patternfly/react-core", () => { - const original = jest.requireActual("@patternfly/react-core"); - return { - ...original, + ...jest.requireActual("@patternfly/react-core"), Skeleton: () =>
PFSkeleton
}; }); -let issues = { +const issues = { + product: [], storage: [ - { description: "Issue 1", details: "Details 1", source: "system", severity: "warn" }, - { description: "Issue 2", details: "Details 2", source: "config", severity: "error" } + { description: "storage issue 1", details: "Details 1", source: "system", severity: "warn" }, + { description: "storage issue 2", details: "Details 2", source: "config", severity: "error" } + ], + software: [ + { description: "software issue 1", details: "Details 1", source: "system", severity: "warn" } ] }; +let mockIssues; + +let mockOnIssuesChange; + beforeEach(() => { + mockIssues = { ...issues }; + mockOnIssuesChange = jest.fn(); + createClient.mockImplementation(() => { return { - issues: { - any: () => Promise.resolve(true), - getAll: () => Promise.resolve(issues), - onIssuesChange: jest.fn() - } + issues: jest.fn().mockResolvedValue(mockIssues), + onIssuesChange: mockOnIssuesChange }; }); }); @@ -59,20 +65,25 @@ it("loads the issues", async () => { installerRender(withNotificationProvider()); screen.getAllByText(/PFSkeleton/); - await screen.findByText(/Issue 1/); + await screen.findByText(/storage issue 1/); }); it("renders sections with issues", async () => { installerRender(withNotificationProvider()); - const section = await screen.findByText(/Storage/); - within(section).findByText(/Issue 1/); - within(section).findByText(/Issue 2/); + await waitFor(() => expect(screen.queryByText("Product")).not.toBeInTheDocument()); + + const storageSection = await screen.findByText(/Storage/); + within(storageSection).findByText(/storage issue 1/); + within(storageSection).findByText(/storage issue 2/); + + const softwareSection = await screen.findByText(/Storage/); + within(softwareSection).findByText(/software issue 1/); }); describe("if there are not issues", () => { beforeEach(() => { - issues = { storage: [] }; + mockIssues = { product: [], storage: [], software: [] }; }); it("renders a success message", async () => { @@ -81,3 +92,21 @@ describe("if there are not issues", () => { await screen.findByText(/No issues found/); }); }); + +describe("if the issues change", () => { + it("shows the new issues", async () => { + const [mockFunction, callbacks] = createCallbackMock(); + mockOnIssuesChange = mockFunction; + + installerRender(withNotificationProvider()); + + await screen.findByText("Storage"); + + mockIssues.storage = []; + act(() => callbacks.forEach(c => c({ storage: mockIssues.storage }))); + + await waitFor(() => expect(screen.queryByText("Storage")).not.toBeInTheDocument()); + const softwareSection = await screen.findByText(/Software/); + within(softwareSection).findByText(/software issue 1/); + }); +});