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 7 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,7 @@
* find current contact information at www.suse.com.
*/

// @ts-check

import React, { useEffect, useState } from "react";

import { useInstallerClient } from "~/context/installer";
import { useCancellablePromise } from "~/utils";
import { useIssues } from "~/queries/issues";
import { BUSY } from "~/client/status";
import { _ } from "~/i18n";
import { ButtonLink, CardField, IssuesHint, Page, SectionSkeleton } from "~/components/core";
import UsedSize from "./UsedSize";
import { SelectedBy } from "~/client/software";
import React from "react";
import {
CardBody,
DescriptionList,
Expand All @@ -41,44 +30,17 @@ import {
GridItem,
Stack,
} from "@patternfly/react-core";

/**
* @typedef {Object} Pattern
* @property {string} name - pattern name (internal ID)
* @property {string} category - pattern category
* @property {string} summary - pattern name (user visible)
* @property {string} description - long description of the pattern
* @property {number} order - display order (string!)
* @property {number} selectedBy - who selected the pattern
*/

/**
* Builds a list of patterns include its selection status
*
* @param {import("~/client/software").Pattern[]} patterns - Patterns from the HTTP API
* @param {Object.<string, number>} selection - Patterns selection
* @return {Pattern[]} List of patterns including its selection status
*/
function buildPatterns(patterns, selection) {
return patterns
.map((pattern) => {
const selectedBy = selection[pattern.name] !== undefined ? selection[pattern.name] : 2;
return {
...pattern,
selectedBy,
};
})
.sort((a, b) => a.order - b.order);
}
import { ButtonLink, CardField, IssuesHint, Page, SectionSkeleton } from "~/components/core";
import UsedSize from "./UsedSize";
import { SelectedBy } from "~/types/software";
import { useIssues } from "~/queries/issues";
import { usePatterns, useProposal, useProposalChanges } from "~/queries/software";
import { _ } from "~/i18n";

/**
* List of selected patterns.
* @component
* @param {object} props
* @param {Pattern[]} props.patterns - List of patterns, including selected and unselected ones.
* @return {JSX.Element}
*/
const SelectedPatternsList = ({ patterns }) => {
const SelectedPatternsList = ({ patterns }: { patterns: Pattern[] }) => {
const selected = patterns.filter((p) => p.selectedBy !== SelectedBy.NONE);

if (selected.length === 0) {
Expand Down Expand Up @@ -126,54 +88,16 @@ const NoPatterns = () => (
</CardBody>
</CardField>
);
// FIXME: move build patterns to utils

/**
* Software page component
* @component
* @returns {JSX.Element}
*/
function SoftwarePage() {
const issues = useIssues("software");
const [status, setStatus] = useState(BUSY);
const [patterns, setPatterns] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [proposal, setProposal] = useState({ patterns: {}, size: "" });
const client = useInstallerClient();
const { cancellablePromise } = useCancellablePromise();

useEffect(() => {
cancellablePromise(client.software.getStatus().then(setStatus));

return client.software.onStatusChange(setStatus);
}, [client, cancellablePromise]);
const proposal = useProposal();
const patterns = usePatterns();

useEffect(() => {
if (!patterns) return;

return client.software.onSelectedPatternsChanged((selection) => {
client.software.getProposal().then((proposal) => setProposal(proposal));
setPatterns(buildPatterns(patterns, selection));
});
}, [client.software, patterns]);

useEffect(() => {
if (!isLoading) return;

const loadPatterns = async () => {
const patterns = await cancellablePromise(client.software.getPatterns());
const proposal = await cancellablePromise(client.software.getProposal());
setPatterns(buildPatterns(patterns, proposal.patterns));
setProposal(proposal);
setIsLoading(false);
};

loadPatterns();
}, [client.software, patterns, cancellablePromise, isLoading]);

if (status === BUSY || isLoading) {
<SectionSkeleton numRows={5} />;
}
dgdavid marked this conversation as resolved.
Show resolved Hide resolved
useProposalChanges();

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* find current contact information at www.suse.com.
*/

import React, { useCallback, useEffect, useState } from "react";
import React, { useState } from "react";
import {
Card,
CardBody,
Expand All @@ -33,36 +33,15 @@ import {
SearchInput,
Stack,
} from "@patternfly/react-core";

import { Section, Page } from "~/components/core";
import { SelectedBy } from "~/types/software";
import { useConfigMutation, usePatterns } from "~/queries/software";
import { _ } from "~/i18n";
import { SelectedBy } from "~/client/software";
import { useInstallerClient } from "~/context/installer";
import { useCancellablePromise } from "~/utils";

/**
* @typedef {Object} Pattern
* @property {string} name pattern name (internal ID)
* @property {string} group pattern group
* @property {string} summary pattern name (user visible)
* @property {string} description long description of the pattern
* @property {string} order display order
* @property {string} icon icon name (not path or file name!)
* @property {number} selected who selected the pattern, undefined
* means it is not selected to install
*/

/**
* @typedef {Object.<string, Array<Pattern>} PatternGroups mapping "group name" =>
* list of patterns
*/

/**
* Group the patterns with the same group name
* @param {Array<Pattern>} patterns input
* @return {PatternGroups}
*/
function groupPatterns(patterns) {
function groupPatterns(patterns: Pattern[]): PatternsGroups {
const groups = {};

patterns.forEach((pattern) => {
Expand Down Expand Up @@ -90,119 +69,69 @@ function groupPatterns(patterns) {

/**
* Sort pattern group names
* @param {PatternGroups} groups input
* @returns {Array<string>} sorted pattern group names
*/
function sortGroups(groups) {
function sortGroups(groups: PatternsGroups): string[] {
return Object.keys(groups).sort((g1, g2) => {
const order1 = groups[g1][0].order;
const order2 = groups[g2][0].order;
return order1 - order2;
});
}

/**
* Builds a list of patterns include its selection status
*
* @param {import("~/client/software").Pattern[]} patterns - Patterns from the HTTP API
* @param {Object.<string, number>} selection - Patterns selection
* @return {Pattern[]} List of patterns including its selection status
*/
function buildPatterns(patterns, selection) {
return patterns
.map((pattern) => {
const selectedBy = selection[pattern.name] !== undefined ? selection[pattern.name] : 2;
return {
...pattern,
selectedBy,
};
})
.sort((a, b) => a.order - b.order);
}
const filterPatterns = (patterns: Pattern[] = [], searchValue = "") => {
if (searchValue.trim() === "") return patterns;

// case insensitive search
const searchData = searchValue.toUpperCase();
return patterns.filter(
(p) =>
p.name.toUpperCase().indexOf(searchData) !== -1 ||
p.description.toUpperCase().indexOf(searchData) !== -1,
);
};

const NoMatches = () => <b>{_("None of the patterns match the filter.")}</b>;

/**
* Pattern selector component
*/
function SoftwarePatternsSelection() {
const client = useInstallerClient();
const [patterns, setPatterns] = useState([]);
const [proposal, setProposal] = useState({ patterns: {}, size: "" });
const [isLoading, setIsLoading] = useState(true);
const [visiblePatterns, setVisiblePatterns] = useState(patterns);
const patterns = usePatterns();
const config = useConfigMutation();
const [searchValue, setSearchValue] = useState("");
const { cancellablePromise } = useCancellablePromise();

useEffect(() => {
if (patterns.length !== 0) return;

const loadPatterns = async () => {
const patterns = await cancellablePromise(client.software.getPatterns());
const proposal = await cancellablePromise(client.software.getProposal());
setPatterns(buildPatterns(patterns, proposal.patterns));
setProposal(proposal);
setIsLoading(false);
};

loadPatterns();
}, [client.software, patterns, cancellablePromise]);

useEffect(() => {
if (!patterns) return;

// filtering - search the required text in the name and pattern description
if (searchValue !== "") {
// case insensitive search
const searchData = searchValue.toUpperCase();
const filtered = patterns.filter(
(p) =>
p.name.toUpperCase().indexOf(searchData) !== -1 ||
p.description.toUpperCase().indexOf(searchData) !== -1,
);
setVisiblePatterns(filtered);
} else {
setVisiblePatterns(patterns);
}

return client.software.onSelectedPatternsChanged((selection) => {
client.software.getProposal().then((proposal) => setProposal(proposal));
setPatterns(buildPatterns(patterns, selection));
});
}, [patterns, searchValue, client.software]);

const onToggle = useCallback(
(name) => {
const selected = patterns
.filter((p) => p.selectedBy === SelectedBy.USER)
.reduce((all, p) => {
all[p.name] = true;
return all;
}, {});
const pattern = patterns.find((p) => p.name === name);
selected[name] = pattern.selectedBy === SelectedBy.NONE;
const onToggle = (name: string) => {
const selected = patterns
.filter((p) => p.selectedBy === SelectedBy.USER)
.reduce((all, p) => {
all[p.name] = true;
return all;
}, {});
const pattern = patterns.find((p) => p.name === name);
selected[name] = pattern.selectedBy === SelectedBy.NONE;

client.software.selectPatterns(selected);
},
[patterns, client.software],
);
config.mutate({ patterns: selected });
};

// FIXME: use loading indicator when busy, we cannot know if it will be
dgdavid marked this conversation as resolved.
Show resolved Hide resolved
// quickly or not in advance.

// initial empty screen, the patterns are loaded very quickly, no need for any progress
const visiblePatterns = filterPatterns(patterns, searchValue);
if (visiblePatterns.length === 0 && searchValue === "") return null;

const groups = groupPatterns(visiblePatterns);

// FIXME: use a switch instead of a checkbox since these patterns are going to
// be selected/deselected immediately.
// TODO: extract to a DataListSelector component or so.
let selector = sortGroups(groups).map((groupName) => {
const selector = sortGroups(groups).map((groupName) => {
const selectedIds = groups[groupName]
.filter((p) => p.selectedBy !== SelectedBy.NONE)
.map((p) => p.name);
return (
<Section key={groupName} title={groupName}>
<DataList isCompact>
<DataList isCompact aria-label={groupName}>
{groups[groupName].map((option) => (
<DataListItem key={option.name}>
<DataListItemRow>
Expand Down Expand Up @@ -237,10 +166,6 @@ function SoftwarePatternsSelection() {
);
});

if (selector.length === 0) {
selector = <b>{_("None of the patterns match the filter.")}</b>;
}

return (
<>
<Page.Header>
Expand All @@ -260,7 +185,7 @@ function SoftwarePatternsSelection() {

<Page.MainContent>
<Card isRounded>
<CardBody>{selector}</CardBody>
<CardBody>{selector.length > 0 ? selector : <NoMatches />}</CardBody>
</Card>
</Page.MainContent>

Expand Down
Loading
Loading