From c1e272bf97de617e3cce3edca9d232fc785af6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 5 Aug 2024 11:19:13 +0100 Subject: [PATCH 01/13] chore(web): add axios --- web/package-lock.json | 27 +++++++++++++++++++-------- web/package.json | 1 + 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 2a2c00d278..deed477f22 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,6 +13,7 @@ "@patternfly/react-core": "^5.1.1", "@patternfly/react-table": "^5.1.1", "@tanstack/react-query": "^5.49.2", + "axios": "^1.7.3", "fast-sort": "^3.4.0", "ipaddr.js": "^2.1.0", "react": "^18.2.0", @@ -5929,8 +5930,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/attr-accept": { "version": "2.2.2", @@ -5955,6 +5955,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", + "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -6810,7 +6821,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -8087,7 +8097,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -10143,7 +10152,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -10200,7 +10208,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -14517,7 +14524,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -14526,7 +14532,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -16062,6 +16067,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/web/package.json b/web/package.json index 4dae56cd87..78ff3b9e17 100644 --- a/web/package.json +++ b/web/package.json @@ -110,6 +110,7 @@ "@patternfly/react-core": "^5.1.1", "@patternfly/react-table": "^5.1.1", "@tanstack/react-query": "^5.49.2", + "axios": "^1.7.3", "fast-sort": "^3.4.0", "ipaddr.js": "^2.1.0", "react": "^18.2.0", From 58943ab85681f45e41032092fe442eda477844c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 5 Aug 2024 14:11:11 +0100 Subject: [PATCH 02/13] refactor(web): add an api/http module --- web/src/api/http.ts | 68 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 web/src/api/http.ts diff --git a/web/src/api/http.ts b/web/src/api/http.ts new file mode 100644 index 0000000000..d61b655287 --- /dev/null +++ b/web/src/api/http.ts @@ -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 axios from "axios"; + +const http = axios.create({ + responseType: "json", +}); + +/** + * Retrieves the object from given URL + * + * @param url - HTTP URL + * @return data from the response body + */ +const get = (url: string) => http.get(url).then(({ data }) => data); + +/** + * Performs a PATCH request with the given URL and data + * + * @param url - endpoint URL + * @param data - Request payload + */ +const patch = (url: string, data: object) => http.patch(url, data); + +/** + * Performs a PUT request with the given URL and data + * + * @param url - endpoint URL + * @param data - request payload + */ +const put = (url: string, data: object) => http.put(url, data); + +/** + * Performs a POST request with the given URL and data + * + * @param url - endpoint URL + * @param data - request payload + */ +const post = (url: string, data: object) => http.post(url, data); + +/** + * Performs a DELETE request on the given URL + * + * @param url - endpoint URL + * @param data - request payload + */ +const del = (url: string) => http.delete(url); + +export { get, patch, post, put, del }; From b204bca7ef73a406fb4f4de235fc49f5dfd0ed23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 5 Aug 2024 14:12:12 +0100 Subject: [PATCH 03/13] refactor(web): use axios to talk to the software service --- web/src/api/software.ts | 52 +++++++++++++++++++++++++++++++++++++ web/src/queries/software.ts | 35 +++++++++++-------------- 2 files changed, 67 insertions(+), 20 deletions(-) create mode 100644 web/src/api/software.ts diff --git a/web/src/api/software.ts b/web/src/api/software.ts new file mode 100644 index 0000000000..f61d06bae8 --- /dev/null +++ b/web/src/api/software.ts @@ -0,0 +1,52 @@ +/* + * 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 { Pattern, Product, SoftwareConfig, SoftwareProposal } from "~/types/software"; +import { get, put } from "~/api/http"; + +/** + * Returns the software configuration + */ +const fetchConfig = (): Promise => get("/api/software/config"); + +/** + * Returns the software proposal + */ +const fetchProposal = (): Promise => get("/api/software/proposal"); + +/** + * Returns the list of known products + */ +const fetchProducts = (): Promise => get("/api/software/products"); + +/** + * Returns the list of patterns for the selected product + */ +const fetchPatterns = (): Promise => get("/api/software/patterns"); + +/** + * Updates the software configuration + * + * @param config - New software configuration + */ +const updateConfig = (config: SoftwareConfig) => put("/api/software/config", config); + +export { fetchConfig, fetchPatterns, fetchProposal, fetchProducts, updateConfig }; diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index fc41aafbea..ce89e3ce01 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -36,13 +36,20 @@ import { SoftwareConfig, SoftwareProposal, } from "~/types/software"; +import { + fetchConfig, + fetchPatterns, + fetchProducts, + fetchProposal, + updateConfig, +} from "~/api/software"; /** * Query to retrieve software configuration */ const configQuery = () => ({ queryKey: ["software/config"], - queryFn: () => fetch("/api/software/config").then((res) => res.json()), + queryFn: fetchConfig, }); /** @@ -50,7 +57,7 @@ const configQuery = () => ({ */ const proposalQuery = () => ({ queryKey: ["software/proposal"], - queryFn: () => fetch("/api/software/proposal").then((res) => res.json()), + queryFn: fetchProposal, }); /** @@ -58,7 +65,7 @@ const proposalQuery = () => ({ */ const productsQuery = () => ({ queryKey: ["software/products"], - queryFn: () => fetch("/api/software/products").then((res) => res.json()), + queryFn: fetchProducts, staleTime: Infinity, }); @@ -67,11 +74,7 @@ const productsQuery = () => ({ */ const selectedProductQuery = () => ({ queryKey: ["software/product"], - queryFn: async () => { - const response = await fetch("/api/software/config"); - const { product } = await response.json(); - return product; - }, + queryFn: () => fetchConfig().then(({ product }) => product), }); /** @@ -79,7 +82,7 @@ const selectedProductQuery = () => ({ */ const patternsQuery = () => ({ queryKey: ["software/patterns"], - queryFn: () => fetch("/api/software/patterns").then((res) => res.json()), + queryFn: fetchPatterns, }); /** @@ -93,16 +96,8 @@ const useConfigMutation = () => { const client = useInstallerClient(); const query = { - mutationFn: (newConfig: SoftwareConfig) => - fetch("/api/software/config", { - // FIXME: use "PATCH" instead - method: "PUT", - body: JSON.stringify(newConfig), - headers: { - "Content-Type": "application/json", - }, - }), - onSuccess: (_, config: SoftwareConfig) => { + mutationFn: updateConfig, + onSuccess: (_: any, config: SoftwareConfig) => { queryClient.invalidateQueries({ queryKey: ["software/config"] }); queryClient.invalidateQueries({ queryKey: ["software/proposal"] }); if (config.product) { @@ -126,7 +121,7 @@ const useProduct = ( { data: products, isPending: isProductsPending }, ] = func({ queries: [selectedProductQuery(), productsQuery()], - }); + }) as [{ data: string; isPending: boolean }, { data: Product[]; isPending: boolean }]; if (isSelectedPending || isProductsPending) { return {}; From 63c63e5672dc56f4a26337008380a4b0ffc411b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 5 Aug 2024 14:38:15 +0100 Subject: [PATCH 04/13] refactor(web): use axios to talk to the l10n service --- web/src/api/l10n.ts | 70 +++++++++++++++++++++++++++++++++++++++++ web/src/queries/l10n.ts | 44 +++++--------------------- web/src/types/l10n.ts | 47 +++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 36 deletions(-) create mode 100644 web/src/api/l10n.ts diff --git a/web/src/api/l10n.ts b/web/src/api/l10n.ts new file mode 100644 index 0000000000..0c117293dc --- /dev/null +++ b/web/src/api/l10n.ts @@ -0,0 +1,70 @@ +/* + * 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 { get, patch } from "~/api/http"; +import { Keymap, Locale, LocaleConfig, Timezone } from "~/types/l10n"; +import { timezoneUTCOffset } from "~/utils"; + +/** + * Returns the l10n configuration + */ +const fetchConfig = () => get("/api/l10n/config"); + +/** + * Returns the list of known locales for installation + */ +const fetchLocales = async (): Promise => { + const json = await get("/api/l10n/locales"); + return json.map(({ id, language, territory }): Locale => { + return { id, name: language, territory }; + }); +}; + +/** + * Returns the list of known timezones + */ +const fetchTimezones = async (): Promise => { + const json = await get("/api/l10n/timezones"); + return json.map(({ code, parts, country }): Timezone => { + const offset = timezoneUTCOffset(code); + return { id: code, parts, country, utcOffset: offset }; + }); +}; + +/** + * Returns the list of known keymaps + */ +const fetchKeymaps = async (): Promise => { + const json = await get("/api/l10n/keymaps"); + const keymaps: Keymap[] = json.map(({ id, description }): Keymap => { + return { id, name: description }; + }); + return keymaps.sort((a, b) => (a.name < b.name ? -1 : 1)); +}; + +/** + * Updates the l10n configuration for the system to install + * + * @param config - Localization configuration + */ +const updateConfig = (config: LocaleConfig) => patch("/api/l10n/config", config); + +export { fetchConfig, fetchKeymaps, fetchLocales, fetchTimezones, updateConfig }; diff --git a/web/src/queries/l10n.ts b/web/src/queries/l10n.ts index 525178a93d..dd5e65809f 100644 --- a/web/src/queries/l10n.ts +++ b/web/src/queries/l10n.ts @@ -22,7 +22,7 @@ import React from "react"; import { useQueryClient, useMutation, useSuspenseQueries } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; -import { timezoneUTCOffset } from "~/utils"; +import { fetchConfig, fetchKeymaps, fetchLocales, fetchTimezones, updateConfig } from "~/api/l10n"; /** * Returns a query for retrieving the localization configuration @@ -30,7 +30,7 @@ import { timezoneUTCOffset } from "~/utils"; const configQuery = () => { return { queryKey: ["l10n/config"], - queryFn: () => fetch("/api/l10n/config").then((res) => res.json()), + queryFn: fetchConfig, }; }; @@ -39,13 +39,7 @@ const configQuery = () => { */ const localesQuery = () => ({ queryKey: ["l10n/locales"], - queryFn: async (): Promise => { - const response = await fetch("/api/l10n/locales"); - const locales = await response.json(); - return locales.map(({ id, language, territory }): Locale => { - return { id, name: language, territory }; - }); - }, + queryFn: fetchLocales, staleTime: Infinity, }); @@ -54,14 +48,7 @@ const localesQuery = () => ({ */ const timezonesQuery = () => ({ queryKey: ["l10n/timezones"], - queryFn: async (): Promise => { - const response = await fetch("/api/l10n/timezones"); - const timezones = await response.json(); - return timezones.map(({ code, parts, country }): Timezone => { - const offset = timezoneUTCOffset(code); - return { id: code, parts, country, utcOffset: offset }; - }); - }, + queryFn: fetchTimezones, staleTime: Infinity, }); @@ -70,14 +57,7 @@ const timezonesQuery = () => ({ */ const keymapsQuery = () => ({ queryKey: ["l10n/keymaps"], - queryFn: async (): Promise => { - const response = await fetch("/api/l10n/keymaps"); - const json = await response.json(); - const keymaps = json.map(({ id, description }): Keymap => { - return { id, name: description }; - }); - return keymaps.sort((a, b) => (a.name < b.name ? -1 : 1)); - }, + queryFn: fetchKeymaps, staleTime: Infinity, }); @@ -87,17 +67,9 @@ const keymapsQuery = () => ({ * It does not require to call `useMutation`. */ const useConfigMutation = () => { - const query = { - mutationFn: (newConfig) => - fetch("/api/l10n/config", { - method: "PATCH", - body: JSON.stringify(newConfig), - headers: { - "Content-Type": "application/json", - }, - }), - }; - return useMutation(query); + return useMutation({ + mutationFn: updateConfig, + }); }; /** diff --git a/web/src/types/l10n.ts b/web/src/types/l10n.ts index 64dbcd1354..00b3892008 100644 --- a/web/src/types/l10n.ts +++ b/web/src/types/l10n.ts @@ -1,3 +1,24 @@ +/* + * 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. + */ + type Keymap = { /** * Keyboard id (e.g., "us"). @@ -42,3 +63,29 @@ type Timezone = { */ utcOffset: number; }; + +type LocaleConfig = { + /** + * List of locales to install (e.g., ["en_US.UTF-8"]). + */ + locales?: string[]; + /** + * Selected keymap for installation (e.g., "en"). + */ + keymap?: string; + /** + * Selected timezone for installation (e.g., "Atlantic/Canary"). + */ + timezone?: string; + + /** + * Locale to be used in the UI. + */ + ui_locale?: string; + /** + * Locale to be used in the UI. + */ + ui_keymap?: string; +}; + +export type { Keymap, Locale, Timezone, LocaleConfig }; From 38f76937be5b26ca84b785e869dc4657f191dc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 5 Aug 2024 14:46:55 +0100 Subject: [PATCH 05/13] refactor(web): use axios to fetch issues --- web/src/api/issues.ts | 37 +++++++++++++++++++++++++++++++++++++ web/src/queries/issues.ts | 24 +++++------------------- web/src/types/issues.ts | 7 ++++++- 3 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 web/src/api/issues.ts diff --git a/web/src/api/issues.ts b/web/src/api/issues.ts new file mode 100644 index 0000000000..2cf9c77064 --- /dev/null +++ b/web/src/api/issues.ts @@ -0,0 +1,37 @@ +/* + * 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 { get } from "~/api/http"; +import { IssuesScope } from "~/types/issues"; + +const URLS = { + product: "software/issues/product", + software: "software/issues/software", + users: "users/issues", + storage: "storage/issues", +}; + +/** + * Return the issues of the given scope. + */ +const fetchIssues = (scope: IssuesScope) => get(`/api/${URLS[scope]}`); + +export { fetchIssues }; diff --git a/web/src/queries/issues.ts b/web/src/queries/issues.ts index 9692301995..8fc03bb953 100644 --- a/web/src/queries/issues.ts +++ b/web/src/queries/issues.ts @@ -28,16 +28,8 @@ import { useSuspenseQuery, } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; -import { Issue, IssuesList } from "~/types/issues"; - -type IssuesScope = "product" | "software" | "storage" | "users"; - -const URLS = { - product: "software/issues/product", - software: "software/issues/software", - users: "users/issues", - storage: "storage/issues", -}; +import { Issue, IssuesList, IssuesScope } from "~/types/issues"; +import { fetchIssues } from "~/api/issues"; const scopesFromPath = { "/org/opensuse/Agama/Software1": "software", @@ -49,15 +41,15 @@ const scopesFromPath = { const issuesQuery = (scope: IssuesScope) => { return { queryKey: ["issues", scope], - queryFn: () => fetch(`/api/${URLS[scope]}`).then((res) => res.json()), + queryFn: () => fetchIssues(scope), }; }; /** * Returns the issues for the given scope. * - * @param {IssuesScope} scope - Scope to get the issues from. - * @return {Issue[]} + * @param scope - Scope to get the issues from. + * @return issues for the given scope. */ const useIssues = (scope: IssuesScope) => { const { data } = useSuspenseQuery(issuesQuery(scope)); @@ -74,12 +66,6 @@ const useAllIssues = () => { const [{ data: product }, { data: software }, { data: storage }, { data: users }] = useSuspenseQueries({ queries }); - const list = { - product: product as Issue[], - software: software as Issue[], - storage: storage as Issue[], - users: users as Issue[], - }; return new IssuesList(product, software, storage, users); }; diff --git a/web/src/types/issues.ts b/web/src/types/issues.ts index 480f258510..75958ac4d0 100644 --- a/web/src/types/issues.ts +++ b/web/src/types/issues.ts @@ -19,6 +19,11 @@ * find current contact information at www.suse.com. */ +/** + * Known scopes for issues. + */ +type IssuesScope = "product" | "software" | "storage" | "users"; + /** * Source of the issue * @@ -80,4 +85,4 @@ class IssuesList { } export { IssueSource, IssuesList, IssueSeverity }; -export type { Issue }; +export type { Issue, IssuesScope }; From 50883c0b045738734513b552c283cd146e78eadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 5 Aug 2024 15:03:48 +0100 Subject: [PATCH 06/13] refactor(web): use axios to talk to the users service --- web/src/api/users.ts | 54 ++++++++++++++++++++++++++++++++++++++++ web/src/queries/users.ts | 47 +++++++++++----------------------- 2 files changed, 68 insertions(+), 33 deletions(-) create mode 100644 web/src/api/users.ts diff --git a/web/src/api/users.ts b/web/src/api/users.ts new file mode 100644 index 0000000000..88a40cf313 --- /dev/null +++ b/web/src/api/users.ts @@ -0,0 +1,54 @@ +/* + * 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 { del, get, patch, put } from "~/api/http"; +import { FirstUser, RootUser, RootUserChanges } from "~/types/users"; + +/** + * Returns the first user's definition + */ +const fetchFirstUser = (): Promise => get("/api/users/first"); + +/** + * Updates the first user's definition + * + * @param user - Full first user's definition + */ +const updateFirstUser = (user: FirstUser) => put("/api/users/first", user); + +/** + * Removes the first user definition + */ +const removeFirstUser = () => del("/api/users/first"); + +/** + * Returns the root user configuration + */ +const fetchRoot = (): Promise => get("/api/users/root"); + +/** + * Updates the root user configuration + * + * @param changes - Changes to apply to the root user configuration + */ +const updateRoot = (changes: Partial) => patch("/api/users/root", changes); + +export { fetchFirstUser, updateFirstUser, removeFirstUser, fetchRoot, updateRoot }; diff --git a/web/src/queries/users.ts b/web/src/queries/users.ts index 8946268478..2ecd196a97 100644 --- a/web/src/queries/users.ts +++ b/web/src/queries/users.ts @@ -20,17 +20,24 @@ */ import React from "react"; -import { QueryClient, useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { _ } from "~/i18n"; -import { FirstUser, RootUser, RootUserChanges } from "~/types/users"; +import { RootUser, RootUserChanges } from "~/types/users"; +import { + fetchFirstUser, + fetchRoot, + removeFirstUser, + updateFirstUser, + updateRoot, +} from "~/api/users"; /** * Returns a query for retrieving the first user configuration */ const firstUserQuery = () => ({ queryKey: ["users", "firstUser"], - queryFn: () => fetch("/api/users/first").then((res) => res.json()), + queryFn: fetchFirstUser, }); /** @@ -47,20 +54,7 @@ const useFirstUser = () => { const useFirstUserMutation = () => { const queryClient = useQueryClient(); const query = { - mutationFn: (user: FirstUser) => - fetch("/api/users/first", { - method: "PUT", - body: JSON.stringify({ ...user, data: {} }), - headers: { - "Content-Type": "application/json", - }, - }).then((response) => { - if (response.ok) { - return response.json(); - } else { - throw new Error(_("Please, try again")); - } - }), + mutationFn: updateFirstUser, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users", "firstUser"] }), }; return useMutation(query); @@ -69,13 +63,7 @@ const useFirstUserMutation = () => { const useRemoveFirstUserMutation = () => { const queryClient = useQueryClient(); const query = { - mutationFn: () => - fetch("/api/users/first", { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }), + mutationFn: removeFirstUser, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["users", "firstUser"] }); }, @@ -113,7 +101,7 @@ const useFirstUserChanges = () => { */ const rootUserQuery = () => ({ queryKey: ["users", "root"], - queryFn: () => fetch("/api/users/root").then((res) => res.json()), + queryFn: fetchRoot, }); const useRootUser = () => { @@ -127,14 +115,7 @@ const useRootUser = () => { const useRootUserMutation = () => { const queryClient = useQueryClient(); const query = { - mutationFn: (changes: Partial) => - fetch("/api/users/root", { - method: "PATCH", - body: JSON.stringify({ ...changes, passwordEncrypted: false }), - headers: { - "Content-Type": "application/json", - }, - }), + mutationFn: updateRoot, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["users", "root"] }), }; return useMutation(query); From efb2b15945f11487d8795863af9bb1ff3cd68a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Aug 2024 10:10:23 +0100 Subject: [PATCH 07/13] refactor(web): use axios to get the installer status --- web/src/api/status.ts | 33 +++++++++++++++++++++++++++++++++ web/src/queries/status.ts | 9 ++------- 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 web/src/api/status.ts diff --git a/web/src/api/status.ts b/web/src/api/status.ts new file mode 100644 index 0000000000..ecef91dad9 --- /dev/null +++ b/web/src/api/status.ts @@ -0,0 +1,33 @@ +/* + * 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 { get } from "~/api/http"; +import { InstallerStatus } from "~/types/status"; + +/** + * Returns the installer status information + */ +const fetchInstallerStatus = async (): Promise => { + const { phase, isBusy, useIguana, canInstall } = await get("/api/manager/installer"); + return { phase, isBusy, useIguana, canInstall }; +}; + +export { fetchInstallerStatus }; diff --git a/web/src/queries/status.ts b/web/src/queries/status.ts index d5e391e8ae..c76167a5e8 100644 --- a/web/src/queries/status.ts +++ b/web/src/queries/status.ts @@ -21,6 +21,7 @@ import { useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import React from "react"; +import { fetchInstallerStatus } from "~/api/status"; import { useInstallerClient } from "~/context/installer"; import { InstallerStatus } from "~/types/status"; @@ -31,13 +32,7 @@ const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; */ const statusQuery = () => ({ queryKey: ["status"], - queryFn: (): Promise => - fetch(`/api/manager/installer`) - .then((res) => res.json()) - .then((body) => { - const { phase, isBusy, useIguana, canInstall } = body; - return { phase, isBusy, useIguana, canInstall }; - }), + queryFn: fetchInstallerStatus, }); /** From a5002deb76bd699d5bfe5fe5ebf94e4a3bd795ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Aug 2024 10:16:47 +0100 Subject: [PATCH 08/13] refactor(web): use axios to get the progress --- web/src/api/progress.ts | 38 +++++++++++++++++++++++++++++++++++++ web/src/queries/progress.ts | 6 ++---- web/src/types/progress.ts | 5 +++-- 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 web/src/api/progress.ts diff --git a/web/src/api/progress.ts b/web/src/api/progress.ts new file mode 100644 index 0000000000..eb0e12e6f8 --- /dev/null +++ b/web/src/api/progress.ts @@ -0,0 +1,38 @@ +/* + * 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 { get } from "~/api/http"; +import { APIProgress, Progress } from "~/types/progress"; + +/** + * Returns the progress information for a given service + * + * At this point, the services that implement the progress API are + * "manager", "software" and "storage". + * + * @param service - Service to retrieve the progress from (e.g., "manager") + */ +const fetchProgress = async (service: string): Promise => { + const progress: APIProgress = await get(`/api/${service}/progress`); + return Progress.fromApi(progress); +}; + +export { fetchProgress }; diff --git a/web/src/queries/progress.ts b/web/src/queries/progress.ts index 7820c3be9c..7953606fee 100644 --- a/web/src/queries/progress.ts +++ b/web/src/queries/progress.ts @@ -23,6 +23,7 @@ import React from "react"; import { useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; import { Progress } from "~/types/progress"; +import { fetchProgress } from "~/api/progress"; const servicesMap = { "org.opensuse.Agama.Manager1": "manager", @@ -41,10 +42,7 @@ const servicesMap = { const progressQuery = (service: string) => { return { queryKey: ["progress", service], - queryFn: () => - fetch(`/api/${service}/progress`) - .then((res) => res.json()) - .then((body) => Progress.fromApi(body)), + queryFn: () => fetchProgress(service), }; }; diff --git a/web/src/types/progress.ts b/web/src/types/progress.ts index d32da63524..0669a3b7f6 100644 --- a/web/src/types/progress.ts +++ b/web/src/types/progress.ts @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -type ProgressApi = { +type APIProgress = { currentStep: number; maxSteps: number; currentTitle: string; @@ -43,7 +43,7 @@ class Progress { this.steps = steps; } - static fromApi(progress: ProgressApi) { + static fromApi(progress: APIProgress) { const { currentStep: current, maxSteps: total, @@ -56,3 +56,4 @@ class Progress { } export { Progress }; +export type { APIProgress }; From 93b06cf0c19f83ba56f97361f88f4fb588253696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Aug 2024 11:00:11 +0100 Subject: [PATCH 09/13] refactor(web): use axios to talk to the questions service --- web/src/api/questions.ts | 74 ++++++++++++++++++++++++++++++++++++ web/src/queries/questions.ts | 51 +++---------------------- 2 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 web/src/api/questions.ts diff --git a/web/src/api/questions.ts b/web/src/api/questions.ts new file mode 100644 index 0000000000..8c6299206d --- /dev/null +++ b/web/src/api/questions.ts @@ -0,0 +1,74 @@ +/* + * 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 { get, put } from "~/api/http"; +import { Answer, Question, QuestionType } from "~/types/questions"; + +type APIQuestion = { + generic?: Question; + withPassword?: Pick; +}; + +/** + * Internal method to build proper question objects + * + * TODO: improve/simplify it once the backend API is improved. + */ +function buildQuestion(httpQuestion: APIQuestion) { + const question: Question = { ...httpQuestion.generic }; + + if (httpQuestion.generic) { + question.type = QuestionType.generic; + question.answer = httpQuestion.generic.answer; + } + + if (httpQuestion.withPassword) { + question.type = QuestionType.withPassword; + question.password = httpQuestion.withPassword.password; + } + + return question; +} + +/** + * Returns the list of questions + */ +const fetchQuestions = async (): Promise => { + const apiQuestions: APIQuestion[] = await get("/api/questions"); + return apiQuestions.map(buildQuestion); +}; + +/** + * Update a questions' answer + * + * The answer is part of the Question object. + */ +const updateAnswer = async (question: Question): Promise => { + const answer: Answer = { generic: { answer: question.answer } }; + + if (question.type === QuestionType.withPassword) { + answer.withPassword = { password: question.password }; + } + + await put(`/api/questions/${question.id}/answer`, answer); +}; + +export { fetchQuestions, updateAnswer }; diff --git a/web/src/queries/questions.ts b/web/src/queries/questions.ts index f5b56f41b9..23c7958bf0 100644 --- a/web/src/queries/questions.ts +++ b/web/src/queries/questions.ts @@ -22,40 +22,15 @@ import React from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; -import { Answer, Question, QuestionType } from "~/types/questions"; - -type APIQuestion = { - generic?: Question; - withPassword?: Pick; -}; - -/** - * Internal method to build proper question objects - * - * TODO: improve/simplify it once the backend API is improved. - */ -function buildQuestion(httpQuestion: APIQuestion) { - const question: Question = { ...httpQuestion.generic }; - - if (httpQuestion.generic) { - question.type = QuestionType.generic; - question.answer = httpQuestion.generic.answer; - } - - if (httpQuestion.withPassword) { - question.type = QuestionType.withPassword; - question.password = httpQuestion.withPassword.password; - } - - return question; -} +import { Question } from "~/types/questions"; +import { fetchQuestions, updateAnswer } from "~/api/questions"; /** * Query to retrieve questions */ const questionsQuery = () => ({ queryKey: ["questions"], - queryFn: () => fetch("/api/questions").then((res) => res.json()), + queryFn: fetchQuestions, }); /** @@ -65,21 +40,7 @@ const questionsQuery = () => ({ */ const useQuestionsConfig = () => { const query = { - mutationFn: (question: Question) => { - const answer: Answer = { generic: { answer: question.answer } }; - - if (question.type === QuestionType.withPassword) { - answer.withPassword = { password: question.password }; - } - - return fetch(`/api/questions/${question.id}/answer`, { - method: "PUT", - body: JSON.stringify(answer), - headers: { - "Content-Type": "application/json", - }, - }); - }, + mutationFn: (question: Question) => updateAnswer(question), }; return useMutation(query); }; @@ -106,8 +67,8 @@ const useQuestionsChanges = () => { * Hook for retrieving available questions */ const useQuestions = () => { - const { data, isPending } = useQuery(questionsQuery()); - return isPending ? [] : data.map(buildQuestion); + const { data: questions, isPending } = useQuery(questionsQuery()); + return isPending ? [] : questions; }; export { questionsQuery, useQuestions, useQuestionsConfig, useQuestionsChanges }; From 6b17c4a5dc914dee21b802f0fc296b2d3f10f214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Aug 2024 12:08:03 +0100 Subject: [PATCH 10/13] fix(web): handle ServiceStatusChanged events properly --- web/src/queries/status.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/queries/status.ts b/web/src/queries/status.ts index c76167a5e8..98315ac7c3 100644 --- a/web/src/queries/status.ts +++ b/web/src/queries/status.ts @@ -73,7 +73,7 @@ const useInstallerStatusChanges = () => { if (type === "ServiceStatusChanged" && event.service === MANAGER_SERVICE) { const { status } = event; - queryClient.setQueryData(["status"], { ...data, busy: status === 1 }); + queryClient.setQueryData(["status"], { ...data, isBusy: status === 1 }); } if (type === "IssuesChanged") { From 39958cfb2ca004dd23df614b3f5e16f912b89b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 6 Aug 2024 12:08:27 +0100 Subject: [PATCH 11/13] chore(web): drop unused imports --- web/src/components/core/ProgressReport.jsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index bd5c2ae7ba..d072cdcb27 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -35,13 +35,7 @@ import { import { _ } from "~/i18n"; import { Center } from "~/components/layout"; -import { - progressQuery, - useProgress, - useProgressChanges, - useResetProgress, -} from "~/queries/progress"; -import { useQuery } from "@tanstack/react-query"; +import { useProgress, useProgressChanges, useResetProgress } from "~/queries/progress"; const Progress = ({ steps, step, firstStep, detail }) => { const stepProperties = (stepNumber) => { From 8971b7ea9551138b084ec64c111d14c98440bdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 7 Aug 2024 09:37:29 +0100 Subject: [PATCH 12/13] chore(web): add missing type to fetchIssues --- web/src/api/issues.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/api/issues.ts b/web/src/api/issues.ts index 2cf9c77064..9daef5c4a6 100644 --- a/web/src/api/issues.ts +++ b/web/src/api/issues.ts @@ -20,7 +20,7 @@ */ import { get } from "~/api/http"; -import { IssuesScope } from "~/types/issues"; +import { Issue, IssuesScope } from "~/types/issues"; const URLS = { product: "software/issues/product", @@ -32,6 +32,6 @@ const URLS = { /** * Return the issues of the given scope. */ -const fetchIssues = (scope: IssuesScope) => get(`/api/${URLS[scope]}`); +const fetchIssues = (scope: IssuesScope): Promise => get(`/api/${URLS[scope]}`); export { fetchIssues }; From d3c9540e4187be560b979ba70e48b4fb589c2e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Wed, 7 Aug 2024 13:02:12 +0100 Subject: [PATCH 13/13] chore(web): add missing return type --- web/src/api/l10n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/api/l10n.ts b/web/src/api/l10n.ts index 0c117293dc..5975307857 100644 --- a/web/src/api/l10n.ts +++ b/web/src/api/l10n.ts @@ -26,7 +26,7 @@ import { timezoneUTCOffset } from "~/utils"; /** * Returns the l10n configuration */ -const fetchConfig = () => get("/api/l10n/config"); +const fetchConfig = (): Promise => get("/api/l10n/config"); /** * Returns the list of known locales for installation