Skip to content

Commit

Permalink
refactor(web): use queries for dealing with software (#1483)
Browse files Browse the repository at this point in the history
Similar to #1439, #1452, and #1470, this set of changes aims to replace
the client/software with Tanstack queries.

Note that was not possible to fully drop the software client. It has to
wait until migration of
[`WithStatus`](https://github.com/openSUSE/agama/blob/bd2f35d0ead6d74931189f5619579f6c3ffa2770/web/src/client/mixins.js#L83)
and
[`WithProgress`](https://github.com/openSUSE/agama/blob/bd2f35d0ead6d74931189f5619579f6c3ffa2770/web/src/client/mixins.js#L147)
mixins too.
  • Loading branch information
dgdavid committed Jul 23, 2024
1 parent 576e266 commit 22d0125
Show file tree
Hide file tree
Showing 23 changed files with 916 additions and 862 deletions.
2 changes: 1 addition & 1 deletion web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@svgr/plugin-jsx": "^8.1.0",
"@svgr/webpack": "^8.1.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/jest-dom": "^6.2.0",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^29.5.12",
Expand Down
59 changes: 4 additions & 55 deletions web/src/client/software.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,6 @@ const SelectedBy = Object.freeze({
* @property {string} description - Product description
*/

/**
* @typedef {object} Registration
* @property {string} requirement - Registration requirement (i.e., "not-required", "optional",
* "mandatory").
* @property {string|null} code - Registration code, if any.
* @property {string|null} email - Registration email, if any.
*/

/**
* @typedef {object} RegistrationFailure
* @property {Number} id - ID of error.
* @property {string} message - Failure message.
*/

/**
* @typedef {object} ActionResult
* @property {boolean} success - Whether the action was successfully done.
Expand Down Expand Up @@ -115,43 +101,6 @@ class SoftwareBaseClient {
return this.client.post("/software/probe", {});
}

/**
* Returns how much space installation takes on disk
*
* @return {Promise<SoftwareProposal>}
*/
async getProposal() {
const response = await this.client.get("/software/proposal");
if (!response.ok) {
console.log("Failed to get software proposal: ", response);
}

return response.json();
}

/**
* Returns available patterns
*
* @return {Promise<Pattern[]>}
*/
async getPatterns() {
const response = await this.client.get("/software/patterns");
if (!response.ok) {
console.log("Failed to get software patterns: ", response);
return [];
}
/** @type Array<{ name: string, category: string, summary: string, description: string, order: string, icon: string }> */
const patterns = await response.json();
return patterns.map((pattern) => ({
name: pattern.name,
category: pattern.category,
summary: pattern.summary,
description: pattern.description,
order: parseInt(pattern.order),
icon: pattern.icon,
}));
}

/**
* @return {Promise<SoftwareConfig>}
*/
Expand Down Expand Up @@ -251,7 +200,7 @@ class ProductClient {
/**
* Returns the registration of the selected product.
*
* @return {Promise<Registration>}
* @return {Promise<import('~/types/registration').Registration>}
*/
async getRegistration() {
const response = await this.client.get("/software/registration");
Expand Down Expand Up @@ -280,7 +229,7 @@ class ProductClient {
async register(code, email = "") {
const response = await this.client.post("/software/registration", { key: code, email });
if (response.status === 422) {
/** @type RegistrationFailure */
/** @type import('~/types/registration').RegistrationFailure */
const body = await response.json();
return {
success: false,
Expand All @@ -303,7 +252,7 @@ class ProductClient {
const response = await this.client.delete("/software/registration");

if (response.status === 422) {
/** @type RegistrationFailure */
/** @type import('~/types/registration').RegistrationFailure */
const body = await response.json();
return {
success: false,
Expand All @@ -320,7 +269,7 @@ class ProductClient {
/**
* Registers a callback to run when the registration changes.
*
* @param {(registration: Registration) => void} handler - Callback function.
* @param {(registration: import('~/types/registration').Registration) => void} handler - Callback function.
*/
onRegistrationChange(handler) {
return this.client.ws().onEvent((event) => {
Expand Down
63 changes: 0 additions & 63 deletions web/src/components/overview/SoftwareSection.test.jsx

This file was deleted.

68 changes: 68 additions & 0 deletions web/src/components/overview/SoftwareSection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* 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 { act, screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import mockTestingPatterns from "~/components/software/patterns.test.json";
import testingProposal from "~/components/software/proposal.test.json";
import SoftwareSection from "~/components/overview/SoftwareSection";
import { SoftwareProposal } from "~/types/software";

let mockTestingProposal: SoftwareProposal;

jest.mock("~/queries/software", () => ({
usePatterns: () => mockTestingPatterns,
useProposal: () => mockTestingProposal,
useProposalChanges: jest.fn(),
}));

describe("SoftwareSection", () => {
describe("when the proposal does not have patterns to select", () => {
beforeEach(() => {
mockTestingProposal = { patterns: {}, size: "" };
});

it("renders nothing", () => {
const { container } = installerRender(<SoftwareSection />);
expect(container).toBeEmptyDOMElement();
});
});

describe("when the proposal has patterns to select", () => {
beforeEach(() => {
mockTestingProposal = testingProposal;
});

it("renders the required space and the selected patterns", () => {
installerRender(<SoftwareSection />);
screen.getByText("4.6 GiB");
screen.getAllByText(/GNOME/);
screen.getByText("YaST Base Utilities");
screen.getByText("YaST Desktop Utilities");
screen.getByText("Multimedia");
screen.getAllByText(/Office Software/);
expect(screen.queryByText("KDE")).toBeNull();
expect(screen.queryByText("XFCE")).toBeNull();
expect(screen.queryByText("YaST Server Utilities")).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022-2023] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -19,40 +19,21 @@
* find current contact information at www.suse.com.
*/

import React, { useEffect, useState } from "react";
import { _ } from "~/i18n";
import { useInstallerClient } from "~/context/installer";
import React from "react";
import { List, ListItem, Text, TextContent, TextVariants } from "@patternfly/react-core";
import { Em } from "~/components/core";
import { SelectedBy } from "~/types/software";
import { usePatterns, useProposal, useProposalChanges } from "~/queries/software";
import { isObjectEmpty } from "~/utils";
import { _ } from "~/i18n";

export default function SoftwareSection() {
const [proposal, setProposal] = useState({});
const [patterns, setPatterns] = useState([]);
const [selectedPatterns, setSelectedPatterns] = useState(undefined);
const client = useInstallerClient();

useEffect(() => {
client.software.getProposal().then(setProposal);
client.software.getPatterns().then(setPatterns);
}, [client]);

useEffect(() => {
return client.software.onSelectedPatternsChanged(() => {
client.software.getProposal().then(setProposal);
});
}, [client, setProposal]);

useEffect(() => {
if (proposal.patterns === undefined) return;
export default function SoftwareSection(): React.ReactNode {
const proposal = useProposal();
const patterns = usePatterns();

const ids = Object.keys(proposal.patterns);
const selected = patterns.filter((p) => ids.includes(p.name)).sort((a, b) => a.order - b.order);
setSelectedPatterns(selected);
}, [client, proposal, patterns]);
useProposalChanges();

if (selectedPatterns === undefined) {
return;
}
if (isObjectEmpty(proposal.patterns)) return;

const TextWithoutList = () => {
return (
Expand All @@ -65,6 +46,8 @@ export default function SoftwareSection() {
const TextWithList = () => {
// TRANSLATORS: %s will be replaced with the installation size, example: "5GiB".
const [msg1, msg2] = _("The installation will take %s including:").split("%s");
const selectedPatterns = patterns.filter((p) => p.selectedBy !== SelectedBy.NONE);

return (
<>
<Text>
Expand All @@ -84,7 +67,7 @@ export default function SoftwareSection() {
return (
<TextContent>
<Text component={TextVariants.h3}>{_("Software")}</Text>
{selectedPatterns.length ? <TextWithList /> : <TextWithoutList />}
{patterns.length ? <TextWithList /> : <TextWithoutList />}
</TextContent>
);
}
Loading

0 comments on commit 22d0125

Please sign in to comment.