Skip to content

Commit

Permalink
[web] Show product and software issues
Browse files Browse the repository at this point in the history
  • Loading branch information
joseivanlopez committed Oct 26, 2023
1 parent 8302b9b commit fd11baa
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 57 deletions.
2 changes: 1 addition & 1 deletion web/src/client/software.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/

Expand Down
94 changes: 56 additions & 38 deletions web/src/components/core/IssuesPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <IssueItem key={`issue-${index}`} issue={issue} />;
});

return (
<Section { ...props }>
{issueItems}
</Section>
);
};

/**
* 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 (
<IssuesSection
key="storage-issues"
title="Storage"
icon="hard_drive"
issues={issues.storage || []}
/>
<>
<If
condition={productIssues.length > 0}
then={
<Section key="product-issues" title={_("Product")} icon="hard_drive">
<IssueItems issues={productIssues} />
</Section>
}
/>
<If
condition={storageIssues.length > 0}
then={
<Section key="storage-issues" title={_("Storage")} icon="hard_drive">
<IssueItems issues={storageIssues} />
</Section>
}
/>
<If
condition={softwareIssues.length > 0}
then={
<Section key="software-issues" title={_("Software")} icon="hard_drive">
<IssueItems issues={softwareIssues} />
</Section>
}
/>
</>
);
};

/**
* 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 = () => {
Expand All @@ -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 (
<Page
Expand All @@ -163,7 +181,7 @@ export default function IssuesPage() {
>
<If
condition={isLoading}
then={<Skeleton />}
then={<SectionSkeleton numRows={4} />}
else={<IssuesContent issues={issues} />}
/>
</Page>
Expand Down
65 changes: 47 additions & 18 deletions web/src/components/core/IssuesPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => <div>PFSkeleton</div>
};
});

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
};
});
});
Expand All @@ -59,20 +65,25 @@ it("loads the issues", async () => {
installerRender(withNotificationProvider(<IssuesPage />));

screen.getAllByText(/PFSkeleton/);
await screen.findByText(/Issue 1/);
await screen.findByText(/storage issue 1/);
});

it("renders sections with issues", async () => {
installerRender(withNotificationProvider(<IssuesPage />));

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 () => {
Expand All @@ -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(<IssuesPage />));

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

0 comments on commit fd11baa

Please sign in to comment.