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 (
-
- );
};
/**
- * 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/);
+ });
+});