Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(web): use queries for dealing with software #1483

Merged
merged 22 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
74654d4
feat(web): Use queries for patterns selection
dgdavid Jul 18, 2024
6d763b7
refactor(web): migrate software queries to TypeScript
dgdavid Jul 18, 2024
8df2c4d
refactor(web): add useProposalChanges hook
dgdavid Jul 18, 2024
479395d
fix(web): export SelectedBy enum
dgdavid Jul 18, 2024
30ed16c
fix(web): export all software types
dgdavid Jul 18, 2024
a7d1f5a
chore(web): update @testing-library/jest-dom
dgdavid Jul 18, 2024
cc16b83
refactor(web) Migrate components/software to TypeScript
dgdavid Jul 18, 2024
aa457fa
fix(web): use same queryClient at software queries
dgdavid Jul 22, 2024
b348db0
refactor(web): change how types are exported
dgdavid Jul 22, 2024
5f9bd2e
refactor(web): improve typing in software components
dgdavid Jul 22, 2024
1896c03
fix(web): add esModuleInterop to tsconfig
dgdavid Jul 22, 2024
5f484e1
fix(web): more tsconfig adjustments
dgdavid Jul 22, 2024
9de8dc3
fix(web) bring back software tests
dgdavid Jul 22, 2024
c394a05
fix(web) move registration types to their own file
dgdavid Jul 22, 2024
3706d25
fix(web) drop ActionResult type from software.ts
dgdavid Jul 22, 2024
ac594f7
fix(web): move internal type
dgdavid Jul 23, 2024
03e024e
refactor(web) adapt overview SoftwareSection to queries
dgdavid Jul 23, 2024
9e037c5
refactor(web) move overview SoftwareSection to TypeScript
dgdavid Jul 23, 2024
04c5d36
fix(web) improve software config mutation
dgdavid Jul 23, 2024
e7c4c82
fix(web) updates from code review
dgdavid Jul 23, 2024
f69c3ef
fix(web) do not test query changes from component
dgdavid Jul 23, 2024
3d320e3
fix(web) drop dead code in software client
dgdavid Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
dgdavid marked this conversation as resolved.
Show resolved Hide resolved

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
Loading