diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index ffb421a543..a7b809aa25 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Mon Mar 9 11:05:21 UTC 2026 - José Iván López González + +- Remove code to deal with the old HTTP API + (gh#agama-project/agama#3258). + ------------------------------------------------------------------- Mon Mar 9 09:19:30 UTC 2026 - David Diaz diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 7918ca18d5..a4d6e77e9f 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -24,14 +24,19 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, mockRoutes } from "~/test-utils"; import { createClient } from "~/client"; -import { Product } from "~/types/software"; +import { Product } from "~/model/system"; import { PATHS } from "~/router"; import { PRODUCT } from "~/routes/paths"; import App from "./App"; import type { Config } from "~/model/config"; import type { Progress, Stage } from "~/model/status"; -const tumbleweed: Product = { id: "openSUSE", name: "openSUSE Tumbleweed", registration: false }; +const tumbleweed: Product = { + id: "openSUSE", + name: "openSUSE Tumbleweed", + modes: [], + registration: false, +}; const mockProgresses: jest.Mock = jest.fn(); const mockState: jest.Mock = jest.fn(); const mockSelectedProduct: jest.Mock = jest.fn(); diff --git a/web/src/api.ts b/web/src/api.ts index 2f2144b4a1..bc168c773e 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2025] SUSE LLC + * Copyright (c) [2025-2026] SUSE LLC * * All Rights Reserved. * @@ -21,13 +21,14 @@ */ import { get, patch, post, put } from "~/http"; +import { TranslatedString } from "~/i18n"; import type { ConfigModel } from "~/model/storage/config-model"; import type { Config } from "~/model/config"; import type { Issue } from "~/model/issue"; import type { Proposal } from "~/model/proposal"; import type { Question } from "~/model/question"; import type { Status } from "~/model/status"; -import type { System } from "~/model/system"; +import type { System, LicenseContent } from "~/model/system"; import type { Action, L10nSystemConfig, DiscoverISCSIConfig } from "~/model/action"; import type { AxiosResponse } from "axios"; @@ -41,6 +42,9 @@ const getExtendedConfig = (): Promise => get("/api/v2/extended_co const getSystem = (): Promise => get("/api/v2/system"); +const getLicense = (id: string, lang: string = "en"): Promise => + get(`/api/v2/licenses/${id}?lang=${lang}`); + const getProposal = (): Promise => get("/api/v2/proposal"); const getIssues = (): Promise => get("/api/v2/issues"); @@ -79,13 +83,31 @@ const probeStorageAction = () => postAction({ probeStorage: null }); const discoverISCSIAction = (config: DiscoverISCSIConfig) => postAction({ discoverISCSI: config }); +const startInstallation = () => postAction({ install: null }); + const finishInstallation = () => postAction({ finish: "reboot" }); +type PasswordCheckResult = { + success?: number; + failure?: TranslatedString; +}; + +const passwordCheck = async (password: string): Promise => { + const response: AxiosResponse = await post( + "/api/v2/private/password_check", + { + password, + }, + ); + return response.data; +}; + export { getStatus, getConfig, getExtendedConfig, getSystem, + getLicense, getProposal, getIssues, getQuestions, @@ -99,7 +121,9 @@ export { activateStorageAction, probeStorageAction, discoverISCSIAction, + startInstallation, finishInstallation, + passwordCheck, }; -export type { Response }; +export type { Response, PasswordCheckResult }; diff --git a/web/src/components/core/InstallerL10nOptions.test.tsx b/web/src/components/core/InstallerL10nOptions.test.tsx index 5c8af6a8df..cd5c8c42e6 100644 --- a/web/src/components/core/InstallerL10nOptions.test.tsx +++ b/web/src/components/core/InstallerL10nOptions.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender, mockProduct, mockRoutes } from "~/test-utils"; import { useSystem } from "~/hooks/model/system"; -import { Product } from "~/types/software"; +import { Product } from "~/model/system"; import { Keymap, Locale } from "~/model/system/l10n"; import { Progress, Stage } from "~/model/status"; import { System } from "~/model/system/network"; @@ -46,6 +46,7 @@ const keymaps: Keymap[] = [ const tumbleweed: Product = { id: "Tumbleweed", name: "openSUSE Tumbleweed", + modes: [], icon: "tumbleweed.svg", description: "Tumbleweed description...", registration: false, diff --git a/web/src/components/overview/OverviewPage.tsx b/web/src/components/overview/OverviewPage.tsx index cde34aabd7..3068da440a 100644 --- a/web/src/components/overview/OverviewPage.tsx +++ b/web/src/components/overview/OverviewPage.tsx @@ -47,17 +47,15 @@ import InstallerOptionsMenu from "~/components/core/InstallerOptionsMenu"; import InstallationSettings from "~/components/overview/InstallationSettings"; import SystemInformationSection from "~/components/overview/SystemInformationSection"; import ProductLogo from "~/components/product/ProductLogo"; -import { startInstallation } from "~/model/manager"; +import { startInstallation } from "~/api"; import { useProductInfo } from "~/hooks/model/config/product"; import { useIssues } from "~/hooks/model/issue"; import { PRODUCT } from "~/routes/paths"; import { useDestructiveActions } from "~/hooks/use-destructive-actions"; import { _ } from "~/i18n"; - -import type { Product } from "~/types/software"; - import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { useProgressTracking } from "~/hooks/use-progress-tracking"; +import type { Product } from "~/model/system"; type ConfirmationPopupProps = { product: Product; diff --git a/web/src/components/overview/RegistrationSummary.test.tsx b/web/src/components/overview/RegistrationSummary.test.tsx index 90f0cb7ba5..c1c3b7fcd6 100644 --- a/web/src/components/overview/RegistrationSummary.test.tsx +++ b/web/src/components/overview/RegistrationSummary.test.tsx @@ -49,6 +49,7 @@ describe("RegistrationSummary", () => { mockProduct({ id: "Tumbleweed", name: "openSUSE Tumbleweed", + modes: [], icon: "tumbleweed.svg", description: "Tumbleweed description...", registration: false, @@ -66,6 +67,7 @@ describe("RegistrationSummary", () => { mockProduct({ id: "Tumbleweed", name: "openSUSE Tumbleweed", + modes: [], icon: "tumbleweed.svg", description: "Tumbleweed description...", registration: true, diff --git a/web/src/components/product/LicenseDialog.test.tsx b/web/src/components/product/LicenseDialog.test.tsx index 4eb257ce7c..1ffc584c91 100644 --- a/web/src/components/product/LicenseDialog.test.tsx +++ b/web/src/components/product/LicenseDialog.test.tsx @@ -24,14 +24,15 @@ import React from "react"; import { screen, waitFor } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { useSystem } from "~/hooks/model/system"; -import { Product } from "~/types/software"; -import * as softwareApi from "~/model/software"; +import { Product } from "~/model/system"; +import * as api from "~/api"; import { Locale, Keymap } from "~/model/system/l10n"; import LicenseDialog from "./LicenseDialog"; const sle: Product = { id: "SLE", name: "SUSE Linux Enterprise", + modes: [], icon: "sle.svg", description: "SLE description", registration: true, @@ -42,7 +43,7 @@ const mockUILanguage = "de-DE"; let mockLicenseLanguage = "de-DE"; const product: Product = sle; const onCloseFn = jest.fn(); -let mockFetchLicense: jest.SpyInstance; +let mockGetLicense: jest.SpyInstance; const locales: Locale[] = [ { id: "en_US.UTF-8", language: "English", territory: "United States" }, @@ -59,11 +60,6 @@ jest.mock("~/utils", () => ({ locationReload: jest.fn(), })); -jest.mock("~/api", () => ({ - ...jest.requireActual("~/api"), - configureL10nAction: jest.fn(), -})); - jest.mock("~/hooks/model/system", () => ({ ...jest.requireActual("~/hooks/model/system"), useSystem: (): ReturnType => ({ @@ -84,7 +80,7 @@ jest.mock("~/context/installerL10n", () => ({ describe("LicenseDialog", () => { mockLicenseLanguage = mockUILanguage; beforeEach(() => { - mockFetchLicense = jest.spyOn(softwareApi, "fetchLicense").mockImplementation( + mockGetLicense = jest.spyOn(api, "getLicense").mockImplementation( jest.fn().mockImplementation(async () => ({ body: "El contenido de la licencia", language: mockLicenseLanguage, @@ -95,7 +91,7 @@ describe("LicenseDialog", () => { it("loads given product license in the interface language", async () => { installerRender(, { withL10n: true }); await waitFor(() => { - expect(mockFetchLicense).toHaveBeenCalledWith(sle.license, mockUILanguage); + expect(mockGetLicense).toHaveBeenCalledWith(sle.license, mockUILanguage); screen.getByText("El contenido de la licencia"); }); }); @@ -108,7 +104,7 @@ describe("LicenseDialog", () => { it("it warns the user that the license is not translated", async () => { installerRender(, { withL10n: true }); await waitFor(() => { - expect(mockFetchLicense).toHaveBeenCalledWith(sle.license, mockUILanguage); + expect(mockGetLicense).toHaveBeenCalledWith(sle.license, mockUILanguage); screen.getByText("El contenido de la licencia"); screen.getByText("Diese Lizenz ist in Deutsch nicht verfügbar."); }); diff --git a/web/src/components/product/LicenseDialog.tsx b/web/src/components/product/LicenseDialog.tsx index 185d9e6749..cce39124a9 100644 --- a/web/src/components/product/LicenseDialog.tsx +++ b/web/src/components/product/LicenseDialog.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2025] SUSE LLC + * Copyright (c) [2025-2026] SUSE LLC * * All Rights Reserved. * @@ -23,8 +23,8 @@ import React, { useEffect, useState } from "react"; import { Alert, ModalProps, Stack } from "@patternfly/react-core"; import { Popup } from "~/components/core"; -import { Product } from "~/types/software"; -import { fetchLicense } from "~/model/software"; +import { Product } from "~/model/system"; +import { getLicense } from "~/api"; import { useInstallerL10n } from "~/context/installerL10n"; import { sprintf } from "sprintf-js"; import supportedLanguages from "~/languages.json"; @@ -56,7 +56,7 @@ function LicenseDialog({ onClose, product }: { onClose: ModalProps["onClose"]; p useEffect(() => { language && - fetchLicense(product.license, language).then(({ body, language: foundLanguage }) => { + getLicense(product.license, language).then(({ body, language: foundLanguage }) => { setLicense(body); setLicenseLanguage(foundLanguage); }); diff --git a/web/src/components/product/RegistrationExtension.tsx b/web/src/components/product/RegistrationExtension.tsx index aec76b8e1a..707ff2a04d 100644 --- a/web/src/components/product/RegistrationExtension.tsx +++ b/web/src/components/product/RegistrationExtension.tsx @@ -32,7 +32,7 @@ import { Label, Title, } from "@patternfly/react-core"; -import { AddonInfo } from "~/types/software"; +import { AddonInfo } from "~/model/system/software"; import { mask } from "~/utils"; import { _ } from "~/i18n"; import RegistrationCodeInput from "./RegistrationCodeInput"; diff --git a/web/src/components/software/SoftwareConflictsPage.test.tsx b/web/src/components/software/SoftwareConflictsPage.test.tsx deleted file mode 100644 index 0637d08569..0000000000 --- a/web/src/components/software/SoftwareConflictsPage.test.tsx +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (c) [2025-2026] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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 { screen, within } from "@testing-library/react"; -import { installerRender } from "~/test-utils"; -import { Conflict } from "~/types/software"; -import SoftwareConflictsPage from "./SoftwareConflictsPage"; - -const conflicts = [ - { - id: 0, - description: - "the to be installed busybox-gawk-1.37.0-33.4.noarch conflicts with 'gawk' provided by the to be installed gawk-5.3.2-1.1.x86_64", - details: null, - solutions: [ - { - id: 0, - description: "Following actions will be done:", - details: - "do not install gawk-5.3.2-1.1.x86_64\ndo not install kernel-default-6.14.4-1.1.x86_64\ndo not install pattern:selinux-20241218-9.1.x86_64", - }, - { - id: 1, - description: "do not install busybox-gawk-1.37.0-33.4.noarch", - details: null, - }, - ], - }, - { - id: 1, - description: - "the to be installed tuned-2.25.1.0+git.889387b-3.1.noarch conflicts with 'tlp' provided by the to be installed tlp-1.8.0-1.1.noarch", - details: null, - solutions: [ - { - id: 0, - description: "do not install tuned-2.25.1.0+git.889387b-3.1.noarch", - details: null, - }, - { - id: 1, - description: "do not install tlp-1.8.0-1.1.noarch", - details: null, - }, - ], - }, - { - id: 2, - description: - "the to be installed pattern:microos_ra_verifier-5.0-98.1.x86_64 requires 'patterns-microos-ra_verifier', but this requirement cannot be provided", - details: - "not installable providers: patterns-microos-ra_verifier-5.0-98.1.x86_64[https-download.opensuse.org-6594e038]", - solutions: [ - { - id: 0, - description: "do not install pattern:microos_ra_verifier-5.0-98.1.x86_64", - details: null, - }, - { - id: 1, - description: "do not install pattern:microos_ra_agent-5.0-98.1.x86_64", - details: null, - }, - { - id: 2, - description: - "break pattern:microos_ra_verifier-5.0-98.1.x86_64 by ignoring some of its dependencies", - details: null, - }, - ], - }, -]; - -let mockConflicts: Conflict[]; -const mockSolveConflict = jest.fn(); - -jest.mock("~/components/layout/Header", () => () =>
Header Mock
); -jest.mock("~/components/questions/Questions", () => () =>
Questions Mock
); - -jest.mock("~/queries/software", () => ({ - ...jest.requireActual("~/queries/software"), - useConflicts: () => mockConflicts, - useConflictsMutation: () => ({ mutate: mockSolveConflict }), -})); - -describe("SofwareConflicts", () => { - beforeEach(() => { - mockConflicts = [{ ...conflicts[0] }]; - }); - - it("does not render the conflicts toolbar", () => { - installerRender(); - expect(screen.queryByText(/Multiple conflicts found/)).toBeNull(); - expect(screen.queryByText(/any order/)).toBeNull(); - expect(screen.queryByText(/resolve others/)).toBeNull(); - expect(screen.queryByText("1 of 3")).toBeNull(); - expect(screen.queryByRole("button", { name: "Skip to previous" })).toBeNull(); - expect(screen.queryByRole("button", { name: "Skip to next" })).toBeNull(); - }); - - it("allows applying the selected solution", async () => { - const { user } = installerRender(); - const applyButton = screen.getByRole("button", { name: "Apply selected solution" }); - const secondOption = screen.getByRole("radio", { - name: conflicts[0].solutions[1].description, - }); - await user.click(secondOption); - await user.click(applyButton); - expect(mockSolveConflict).toHaveBeenCalledWith({ conflictId: 0, solutionId: 1 }); - }); - - it("displays an error if no solution is selected before submission", async () => { - const { user } = installerRender(); - const applyButton = screen.getByRole("button", { name: "Apply selected solution" }); - const firstSolution = screen.getAllByRole("radio")[0]; - await user.click(applyButton); - screen.getByText("Warning alert:"); - screen.getByText("Select a solution to continue"); - await user.click(firstSolution); - await user.click(applyButton); - expect(screen.queryByText("Warning alert:")).toBeNull(); - expect(screen.queryByText("Select a solution to continue")).toBeNull(); - }); - - describe("when a conflict solution has details", () => { - beforeEach(() => { - mockConflicts = [ - { - id: 0, - description: "Fake conflict", - details: null, - solutions: [ - { - id: 0, - description: `Fake solution with details`, - details: "Action 1\nAction 2", - }, - ], - }, - ]; - }); - - it("renders details in a list, splitting by newline", () => { - installerRender(); - const details = screen.getByRole("list"); - within(details).getByText("Action 1"); - within(details).getByText("Action 2"); - }); - - describe("and the number of details is within the visible limit", () => { - it("does not render a toggle to show/hide more", () => { - installerRender(); - expect(screen.queryByRole("button", { name: /^Show.*actions$"/ })).toBeNull(); - }); - }); - - describe("but the number of details exceeds the visible limit", () => { - beforeEach(() => { - mockConflicts = [ - { - id: 0, - description: "Fake conflict", - details: null, - solutions: [ - { - id: 0, - description: `Fake solution with details`, - details: "Action 1\nAction 2\nAction 3\nAction 4", - }, - ], - }, - ]; - }); - - it("renders a toggle to show/hide all actions", async () => { - const { user } = installerRender(); - const actionsToggle = screen.getByRole("button", { name: /^Show.*actions$/ }); - const details = screen.getByRole("list"); - within(details).getByText("Action 1"); - within(details).getByText("Action 2"); - within(details).getByText("Action 3"); - expect(within(details).queryByText("Action 4")).toBeNull(); - await user.click(actionsToggle); - within(details).getByText("Action 4"); - expect(actionsToggle).toHaveTextContent("Show less actions"); - await user.click(actionsToggle); - expect(within(details).queryByText("Action 4")).toBeNull(); - }); - }); - }); - - describe("when there is more than one conflict", () => { - beforeEach(() => { - mockConflicts = conflicts; - }); - - it("renders the conflicts toolbar with information and links", () => { - installerRender(); - screen.getByText(/Multiple conflicts found/); - screen.getByText(/any order/); - screen.getByText(/resolve others/); - screen.getByText("1 of 3"); - screen.getByRole("button", { name: "Skip to previous" }); - screen.getByRole("button", { name: "Skip to next" }); - }); - - it("allows navigating between conflicts without exceeding bounds", async () => { - const { user } = installerRender(); - screen.getByText("1 of 3"); - const skipToPrevious = screen.getByRole("button", { name: "Skip to previous" }); - const skipToNext = screen.getByRole("button", { name: "Skip to next" }); - - expect(skipToPrevious).toBeDisabled(); - expect(skipToNext).not.toBeDisabled(); - - await user.click(skipToPrevious); - screen.getByText("1 of 3"); - screen.getByText(conflicts[0].description); - await user.click(skipToNext); - expect(skipToPrevious).not.toBeDisabled(); - expect(skipToNext).not.toBeDisabled(); - screen.getByText("2 of 3"); - expect(screen.queryByText(conflicts[0].description)).toBeNull(); - screen.getByText(conflicts[1].description); - await user.click(skipToNext); - screen.getByText("3 of 3"); - expect(screen.queryByText(conflicts[1].description)).toBeNull(); - screen.getByText(conflicts[2].description); - expect(skipToPrevious).not.toBeDisabled(); - expect(skipToNext).toBeDisabled(); - await user.click(skipToNext); - screen.getByText("3 of 3"); - }); - - it("does not preserve the selected option after navigating", async () => { - const { user } = installerRender(); - screen.getByText("1 of 3"); - const skipToPrevious = screen.getByRole("button", { name: "Skip to previous" }); - const skipToNext = screen.getByRole("button", { name: "Skip to next" }); - - screen.getByText("1 of 3"); - screen.getByText(conflicts[0].description); - let options = screen.getAllByRole("radio", { checked: false }); - expect(options.length).toBe(conflicts[0].solutions.length); - - await user.click(options[0]); - expect(options[0]).toBeChecked(); - - await user.click(skipToNext); - screen.getByText("2 of 3"); - screen.getByText(conflicts[1].description); - options = screen.getAllByRole("radio", { checked: false }); - expect(options.length).toBe(conflicts[1].solutions.length); - expect(options[0]).not.toBeChecked(); - - await user.click(options[0]); - expect(options[0]).toBeChecked(); - - await user.click(skipToPrevious); - options = screen.getAllByRole("radio", { checked: false }); - expect(options.length).toBe(conflicts[0].solutions.length); - expect(options[0]).not.toBeChecked(); - }); - - it("allows applying the selected solution for the current conflict", async () => { - const { user } = installerRender(); - const skipToNext = screen.getByRole("button", { name: "Skip to next" }); - - await user.click(skipToNext); - const applyButton = screen.getByRole("button", { name: "Apply selected solution" }); - const secondOption = screen.getByRole("radio", { - name: conflicts[1].solutions[1].description, - }); - await user.click(secondOption); - await user.click(applyButton); - expect(mockSolveConflict).toHaveBeenCalledWith({ conflictId: 1, solutionId: 1 }); - }); - }); - - describe("when there are no conflicts", () => { - beforeEach(() => { - mockConflicts = []; - }); - - it("does not render the solution selection form", () => { - installerRender(); - expect(screen.queryAllByRole("radio").length).toBe(0); - expect(screen.queryByRole("button", { name: "Apply selected solution" })).toBeNull(); - }); - - it("renders a message indicating there are no conflicts to address", () => { - installerRender(); - screen.queryByRole("heading", { name: "No conflicts to address" }); - screen.getByText(/All conflicts have been resolved, or none were detected/); - }); - }); -}); diff --git a/web/src/components/software/SoftwareConflictsPage.tsx b/web/src/components/software/SoftwareConflictsPage.tsx deleted file mode 100644 index 9e9fbe96d7..0000000000 --- a/web/src/components/software/SoftwareConflictsPage.tsx +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (c) [2025-2026] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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, { useState } from "react"; -import { - ActionGroup, - Alert, - Button, - ButtonProps, - Content, - Divider, - Flex, - Form, - FormGroup, - List, - ListItem, - Radio, - RadioProps, - Title, - Toolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, -} from "@patternfly/react-core"; -import { Icon } from "~/components/layout"; -import { Page, SubtleContent } from "~/components/core"; -import { ConflictSolutionOption } from "~/types/software"; -import { useConflicts, useConflictsChanges, useConflictsMutation } from "~/queries/software"; -import { isNullish } from "radashi"; -import { sprintf } from "sprintf-js"; -import { SOFTWARE } from "~/routes/paths"; -import { _ } from "~/i18n"; - -/** - * Renders a list of conflict details as a bullet list. - * Used to display all actions associated with a conflict solution. - * - * @param props - Component props. - * @param props.items - An array of strings representing the detail items to display. - */ -const DetailsList = ({ items }: { items: string[] }) => ( - - {items.map((d, i) => ( - - {d} - - ))} - -); - -type ConflictSolutionRadioProps = { - /** Newline-separated string of solution actions */ - details?: ConflictSolutionOption["details"]; - /** Max number of visible detail lines before enabling toggle behavior */ - maxVisibleDetails?: number; -} & Omit; - -/** - * A custom wrapper around PatternFly's Radio component for presenting a - * conflict solution option. Optionally displays additional details or actions - * with an expandable/collapsible list. - * - * Behavior: - * - If no details are provided, a plain radio button is rendered. - * - If a small number of details exist, they are shown directly. - * - If there are more than `maxVisibleDetails` (default 3), the list is - * collapsible. - */ -const ConflictSolutionRadio = ({ - details: rawDetails, - maxVisibleDetails = 3, - ...props -}: ConflictSolutionRadioProps) => { - const [expanded, setExpanded] = useState(false); - const details = rawDetails ? rawDetails?.split("\n") : []; - - if (details.length === 0) return ; - if (details.length <= maxVisibleDetails) - return } />; - - const visibleDetails = expanded ? details : details.slice(0, maxVisibleDetails); - const toggleText = expanded ? _("Show less actions") : _("Show more actions"); - const toggleIcon = expanded ? "unfold_less" : "unfold_more"; - const toggleVisibility = () => setExpanded(!expanded); - - return ( - - - - - } - {...props} - /> - ); -}; - -/** - * Internal component responsible of rendering the form to allow users choose - * and eventually apply a solution for given conflict - */ -const ConflictsForm = ({ conflict }): React.ReactNode => { - const { mutate: solve } = useConflictsMutation(); - const [error, setError] = useState(null); - const [chosenSolution, setChosenSolution] = useState(); - - const onSubmit = async (e) => { - e.preventDefault(); - - if (!isNullish(chosenSolution)) { - setError(null); - solve({ conflictId: conflict.id, solutionId: chosenSolution }); - } else { - setError(_("Select a solution to continue")); - } - }; - - return ( -
- {error && } - {conflict.description} - - {conflict.solutions.map((solution: ConflictSolutionOption) => ( - {solution.description}} - onChange={() => setChosenSolution(solution.id)} - isChecked={solution.id === chosenSolution} - details={solution.details} - /> - ))} - - - {_("Apply selected solution")} - - - ); -}; - -type ConflictsToolbarProps = { - current: number; - total: number; - onNext: ButtonProps["onClick"]; - onBack: ButtonProps["onClick"]; -}; -const ConflictsToolbar = ({ - current, - total, - onNext, - onBack, -}: ConflictsToolbarProps): React.ReactNode => ( - - - - - {_( - "Multiple conflicts found. You can address them in any order, and resolving one may resolve others.", - )} - - - - - - - - - { - // TRANSLATORS: This is a short status message like "1 of 3". It - // indicates the position of the current item out of a total. The - // first %d will be replaced with the current item number, the - // second %d with the total number of items. - sprintf(_("%d of %d"), current, total) - } - - - - - - - -); - -/** - * Displays content when there are no conflicts to resolve. - * Typically shown when user lands on this page with no actionable items. - */ -const NoConflictsContent = () => ( - <> - {_("No conflicts to address")} - - {_( - "All conflicts have been resolved, or none were detected. You can safely continue with your setup.", - )} - - -); - -/** - * Main content component to display and navigate between multiple conflicts. - * - * Renders a toolbar (if more than one conflict), and the conflict resolution form. - * - * It uses a `key` prop for forcing an state reset when navigating back and - * forward. - * - * See https://react.dev/learn/preserving-and-resetting-state#option-2-resetting-state-with-a-key - */ -const ConflictsContent = ({ conflicts }) => { - const [currentConflictIndex, setCurrentConflictIndex] = useState(0); - const totalConflicts = conflicts.length; - const lastConflictIndex = totalConflicts - 1; - - const onNext = async () => { - currentConflictIndex < lastConflictIndex && setCurrentConflictIndex(currentConflictIndex + 1); - }; - const onBack = async () => { - currentConflictIndex > 0 && setCurrentConflictIndex(currentConflictIndex - 1); - }; - - const currentConflict = conflicts[currentConflictIndex]; - - return ( - <> - {conflicts.length > 1 && ( - <> - - - - )} - - - ); -}; - -/** - * Top-level page for handling software conflicts resolution. - * - * Displays either a resolution form or a message when no conflicts are present. - */ -export default function SoftwareConflictsPage() { - useConflictsChanges(); - const conflicts = useConflicts(); - - return ( - - - {conflicts.length > 0 ? : } - - - - {_("Close")} - - - ); -} diff --git a/web/src/components/software/SoftwarePatternsSelection.test.tsx b/web/src/components/software/SoftwarePatternsSelection.test.tsx index 37a2ed6366..efe2614a4b 100644 --- a/web/src/components/software/SoftwarePatternsSelection.test.tsx +++ b/web/src/components/software/SoftwarePatternsSelection.test.tsx @@ -28,8 +28,6 @@ import testingProposal from "./proposal.test.json"; import SoftwarePatternsSelection from "./SoftwarePatternsSelection"; import { patchConfig } from "~/api"; -const onConfigMutationMock = { mutate: jest.fn() }; - jest.mock("~/hooks/model/system/software", () => ({ useSystem: () => ({ patterns: testingPatterns }), })); @@ -42,10 +40,6 @@ jest.mock("~/api", () => ({ patchConfig: jest.fn(), })); -jest.mock("~/queries/software", () => ({ - useConfigMutation: () => onConfigMutationMock, -})); - describe("SoftwarePatternsSelection", () => { it("displays the pattern in the correct order", async () => { installerRender(); diff --git a/web/src/components/storage/Progress.tsx b/web/src/components/storage/Progress.tsx deleted file mode 100644 index ce445bcb20..0000000000 --- a/web/src/components/storage/Progress.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) [2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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, { useEffect, useState } from "react"; -import { - Content, - Flex, - ProgressStep, - ProgressStepper, - ProgressStepProps, - Spinner, -} from "@patternfly/react-core"; -import { _ } from "~/i18n"; -import { useProgress, useProgressChanges, useResetProgress } from "~/queries/progress"; -import sizingStyles from "@patternfly/react-styles/css/utilities/Sizing/sizing"; -import { STORAGE } from "~/routes/paths"; -import { useNavigate } from "react-router"; - -type StepProps = { - id: string; - titleId: string; - isCurrent: boolean; - variant?: ProgressStepProps["variant"]; - description?: ProgressStepProps["description"]; -}; - -const ProgressContent = ({ steps, step }) => { - const stepProperties = (stepNumber: number): StepProps => { - const properties: StepProps = { - isCurrent: stepNumber === step.current, - id: `step-${stepNumber}-id`, - titleId: `step-${stepNumber}-title`, - }; - - if (stepNumber > step.current) { - properties.variant = "pending"; - properties.description =
{_("Pending")}
; - } - - if (properties.isCurrent) { - properties.variant = "info"; - } - - if (stepNumber < step.current || step.finished) { - properties.variant = "success"; - properties.description =
{_("Finished")}
; - } - - return properties; - }; - - return ( - - {steps.map((description: StepProps["description"], idx: number) => { - return ( - - {description} - - ); - })} - - ); -}; - -/** - * Shows progress steps when a product is selected. - */ -function Progress() { - useResetProgress(); - useProgressChanges(); - const navigate = useNavigate(); - - const progress = useProgress("storage"); - const [steps, setSteps] = useState(progress?.steps); - - useEffect(() => { - if (!progress) return; - if (progress.steps.length === 0) return; - if (progress.finished) navigate(STORAGE.root); - - setSteps(progress.steps); - }, [progress, steps, navigate]); - - if (!progress) return; - - return ( - - - {_("Loading storage")} - - - ); -} - -export default Progress; diff --git a/web/src/components/users/PasswordCheck.test.tsx b/web/src/components/users/PasswordCheck.test.tsx index 62184c3569..e0f5f9c04a 100644 --- a/web/src/components/users/PasswordCheck.test.tsx +++ b/web/src/components/users/PasswordCheck.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2025] SUSE LLC + * Copyright (c) [2025-2026] SUSE LLC * * All Rights Reserved. * @@ -25,11 +25,11 @@ import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import PasswordCheck from "./PasswordCheck"; -const mockCheckPasswordFn = jest.fn(); +const mockPasswordCheckFn = jest.fn(); -jest.mock("~/model/users", () => ({ - ...jest.requireActual("~/model/users"), - checkPassword: (password) => mockCheckPasswordFn(password), +jest.mock("~/api", () => ({ + ...jest.requireActual("~/api"), + passwordCheck: (password) => mockPasswordCheckFn(password), })); describe("when the password is empty", () => { @@ -41,7 +41,7 @@ describe("when the password is empty", () => { describe("when the password is not valid", () => { beforeEach(() => { - mockCheckPasswordFn.mockResolvedValueOnce({ + mockPasswordCheckFn.mockResolvedValueOnce({ failure: "Shorter than 8 characters", }); }); @@ -54,7 +54,7 @@ describe("when the password is not valid", () => { describe("when the password is weak", () => { beforeEach(() => { - mockCheckPasswordFn.mockResolvedValueOnce({ + mockPasswordCheckFn.mockResolvedValueOnce({ success: 30, }); }); @@ -67,7 +67,7 @@ describe("when the password is weak", () => { describe("when the password is strong", () => { beforeEach(() => { - mockCheckPasswordFn.mockResolvedValueOnce({ + mockPasswordCheckFn.mockResolvedValueOnce({ success: 90, }); }); diff --git a/web/src/components/users/PasswordCheck.tsx b/web/src/components/users/PasswordCheck.tsx index 894a9040ec..b76b562f38 100644 --- a/web/src/components/users/PasswordCheck.tsx +++ b/web/src/components/users/PasswordCheck.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2025] SUSE LLC + * Copyright (c) [2025-2026] SUSE LLC * * All Rights Reserved. * @@ -22,7 +22,7 @@ import React, { useEffect, useState } from "react"; import SmallWarning from "~/components/core/SmallWarning"; -import { checkPassword } from "~/model/users"; +import { passwordCheck } from "~/api"; import { _, TranslatedString } from "~/i18n"; const MINIMAL_SCORE = 50; @@ -33,7 +33,7 @@ const PasswordCheck = ({ password }: { password: string }) => { useEffect(() => { if (!password) return; - checkPassword(password).then((result) => { + passwordCheck(password).then((result) => { if (result.failure) { setError(result.failure); } else if (result.success && result.success < MINIMAL_SCORE) { diff --git a/web/src/model/action.ts b/web/src/model/action.ts index a2bbc0704b..294689ac29 100644 --- a/web/src/model/action.ts +++ b/web/src/model/action.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -type Action = ConfigureL10n | ActivateStorage | ProbeStorage | DiscoverISCSI | Finish; +type Action = ConfigureL10n | ActivateStorage | ProbeStorage | DiscoverISCSI | Install | Finish; type ConfigureL10n = { configureL10n: L10nSystemConfig; @@ -52,6 +52,10 @@ type DiscoverISCSIConfig = { initiatorPassword?: string; }; +type Install = { + install: null; +}; + type Finish = { finish: "halt" | "reboot" | "stop" | "poweroff"; }; diff --git a/web/src/model/manager.ts b/web/src/model/manager.ts deleted file mode 100644 index 88257702cf..0000000000 --- a/web/src/model/manager.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) [2024-2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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. - */ - -// @todo Move to the new API. - -import { get, post } from "~/http"; - -/** - * Starts the probing process. - */ -const startProbing = () => post("/api/manager/probe"); - -/** - * Triggers a synchronous probing process. - */ -const probe = () => post("/api/manager/probe_sync"); - -/** - * Triggers a synchronous reprobing process. - */ -const reprobe = () => post("/api/manager/reprobe_sync"); - -// we need wrapper here to pass it as plain string json -/* eslint-disable no-new-wrappers */ -const install_action: object = new String("install"); -/** - * Starts the installation process. - * - * The progress of the installation process can be tracked through installer signals. - */ -const startInstallation = () => post("/api/v2/action", install_action); - -/** - * Clean-up when installation is done. - */ -const finishInstallation = () => post("/api/v2/action"); - -/** - * Returns the binary content of the YaST logs file. - */ -const fetchLogs = () => get("/api/manager/logs/store"); - -export { startProbing, probe, reprobe, startInstallation, finishInstallation, fetchLogs }; diff --git a/web/src/model/progress.ts b/web/src/model/progress.ts deleted file mode 100644 index e4437e0fb8..0000000000 --- a/web/src/model/progress.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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. - */ - -// @todo Move to the new API. - -import { get } from "~/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/model/proposal/software.ts b/web/src/model/proposal/software.ts index c4736ec159..bdcb802289 100644 --- a/web/src/model/proposal/software.ts +++ b/web/src/model/proposal/software.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2025] SUSE LLC + * Copyright (c) [2025-2026] SUSE LLC * * All Rights Reserved. * @@ -41,5 +41,4 @@ enum SelectedBy { } export type { Proposal, PatternsSelection }; - export { SelectedBy }; diff --git a/web/src/model/software.ts b/web/src/model/software.ts deleted file mode 100644 index 7b4f14a2e8..0000000000 --- a/web/src/model/software.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (c) [2024-2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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. - */ - -// @todo Move to the new API. - -import { - AddonInfo, - Conflict, - ConflictSolution, - License, - LicenseContent, - Pattern, - Product, - RegisteredAddonInfo, - RegistrationInfo, - Repository, - SoftwareConfig, - SoftwareProposal, -} from "~/types/software"; -import { get, patch, post, put } from "~/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 available licenses - */ -const fetchLicenses = (): Promise => get("/api/software/licenses"); - -/** - * Returns the content for given license id - */ -const fetchLicense = (id: string, lang: string = "en"): Promise => - get(`/api/v2/licenses/${id}?lang=${lang}`); - -/** - * Returns an object with the registration info - */ -const fetchRegistration = (): Promise => get("/api/software/registration"); - -/** - * Returns list of available addons - */ -const fetchAddons = (): Promise => get("/api/software/registration/addons/available"); - -/** - * Returns list of already registered addons - */ -const fetchRegisteredAddons = (): Promise => - get("/api/software/registration/addons/registered"); - -/** - * Returns the list of patterns for the selected product - */ -const fetchPatterns = (): Promise => get("/api/software/patterns"); - -/** - * Returns the list of configured repositories - */ -const fetchRepositories = (): Promise => get("/api/software/repositories"); - -/** - * Returns the list of conflicts - */ -const fetchConflicts = (): Promise => get("/api/software/conflicts"); - -/** - * Updates the software configuration - * - * @param config - New software configuration - */ -const updateConfig = (config: SoftwareConfig) => put("/api/software/config", config); - -/** - * Updates the software configuration - */ -const probe = () => post("/api/software/probe"); - -/** - * Request registration of the selected addon - */ -const registerAddon = (addon: RegisteredAddonInfo) => - post("/api/software/registration/addons/register", addon); - -/** - * Request for solving a conflict by applying given solution - */ -const solveConflict = (solution: ConflictSolution) => patch("/api/software/conflicts", [solution]); - -export { - fetchAddons, - fetchConfig, - fetchConflicts, - fetchLicense, - fetchLicenses, - fetchPatterns, - fetchProducts, - fetchProposal, - fetchRegisteredAddons, - fetchRegistration, - fetchRepositories, - probe, - registerAddon, - solveConflict, - updateConfig, -}; diff --git a/web/src/model/status.ts b/web/src/model/status.ts index a8f0b86014..6d576dfdcb 100644 --- a/web/src/model/status.ts +++ b/web/src/model/status.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2026] SUSE LLC * * All Rights Reserved. * @@ -20,23 +20,8 @@ * find current contact information at www.suse.com. */ -// @todo Move to the new API. - -import { get } from "~/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 }; -}; - -// TODO: remove -export { fetchInstallerStatus }; - type Stage = "installing" | "configuring" | "finished" | "failed"; + type Scope = | "manager" | "l10n" @@ -47,6 +32,7 @@ type Scope = | "iscsi" | "dasd" | "users"; + type Progress = { index: number; scope: Scope; diff --git a/web/src/model/system.ts b/web/src/model/system.ts index 5345c35761..0b53783267 100644 --- a/web/src/model/system.ts +++ b/web/src/model/system.ts @@ -74,9 +74,19 @@ type Mode = { description: string; }; +type LicenseContent = { + /** License ID (e.g., "license.sle") */ + id: string; + /** License body */ + body: string; + /** License language (e.g., "en-US") */ + language: string; +}; + export type { System, Product, + LicenseContent, L10n, Hardware, Hostname, diff --git a/web/src/model/users.ts b/web/src/model/users.ts deleted file mode 100644 index 7f7264a602..0000000000 --- a/web/src/model/users.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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. - */ - -// @todo Move to the new API. - -import { AxiosResponse } from "axios"; -import { del, get, patch, post, put } from "~/http"; -import { FirstUser, PasswordCheckResult, RootUser } 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: Partial) => 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); - -/** - * Checks the strength of the given password. - * - * @param password - Password to check. - */ -const checkPassword = async (password: string): Promise => { - const response: AxiosResponse = await post( - "/api/v2/private/password_check", - { - password, - }, - ); - return response.data; -}; - -export { fetchFirstUser, updateFirstUser, removeFirstUser, fetchRoot, updateRoot, checkPassword }; diff --git a/web/src/queries/progress.ts b/web/src/queries/progress.ts deleted file mode 100644 index 4b3a422a17..0000000000 --- a/web/src/queries/progress.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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 the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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 { useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; -import { useInstallerClient } from "~/context/installer"; -import { Progress } from "~/types/progress"; -import { QueryHookOptions } from "~/types/queries"; -import { fetchProgress } from "~/model/progress"; - -const servicesMap = { - "/org/opensuse/Agama/Manager1": "manager", - "/org/opensuse/Agama/Software1": "software", - "/org/opensuse/Agama/Storage1": "storage", -}; - -/** - * Returns a query for retrieving 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 progressQuery = (service: string) => { - return { - queryKey: ["progress", service], - queryFn: () => fetchProgress(service), - }; -}; - -/** - * Hook that returns the progress for a given service - * - * @param service - Service to retrieve the progress from - * @param options - Query options - * @returns Progress information or undefined if unknown - */ -const useProgress = (service: string, options?: QueryHookOptions): Progress | undefined => { - const query = progressQuery(service); - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func(query); - return data; -}; - -/** - * Hook that registers a useEffect to listen for progress changes - * - * It listens for all progress changes but updates only existing - * progress queries. - */ -const useProgressChanges = () => { - const client = useInstallerClient(); - const queryClient = useQueryClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "ProgressChanged") { - const service = servicesMap[event.path]; - if (!service) { - console.warn("Unknown progress path", event.path); - return; - } - - const data = queryClient.getQueryData(["progress", service]); - if (data) { - // NOTE: steps are not coming in the updates - const steps = (data as Progress).steps; - const fromEvent = Progress.fromApi(event); - queryClient.setQueryData(["progress", service], { ...fromEvent, steps }); - } - } - }); - }, [client, queryClient]); -}; - -/** - * Hook that invalidates all the existing queries. - * - * It offers a way to clear previously cached progress information. It is expected to - * be used before starting to display the progress. - */ -const useResetProgress = () => { - const queryClient = useQueryClient(); - - React.useEffect(() => { - return () => { - queryClient.invalidateQueries({ queryKey: ["progress"] }); - }; - }, [queryClient]); -}; - -export { useProgress, useProgressChanges, useResetProgress, progressQuery }; diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts deleted file mode 100644 index 1b6942e544..0000000000 --- a/web/src/queries/software.ts +++ /dev/null @@ -1,422 +0,0 @@ -/* - * Copyright (c) [2024-2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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 { - useMutation, - useQueries, - useQuery, - useQueryClient, - useSuspenseQueries, - useSuspenseQuery, -} from "@tanstack/react-query"; -import { useInstallerClient } from "~/context/installer"; -import { - AddonInfo, - Conflict, - License, - Pattern, - PatternsSelection, - Product, - RegisteredAddonInfo, - RegistrationInfo, - Repository, - SelectedBy, - SoftwareConfig, - SoftwareProposal, -} from "~/types/software"; -import { - fetchAddons, - fetchConfig, - fetchConflicts, - fetchLicenses, - fetchPatterns, - fetchProducts, - fetchProposal, - fetchRegisteredAddons, - fetchRegistration, - fetchRepositories, - probe, - registerAddon, - solveConflict, - updateConfig, -} from "~/model/software"; -import { QueryHookOptions } from "~/types/queries"; -import { probe as systemProbe } from "~/model/manager"; - -/** - * Query to retrieve software configuration - */ -const configQuery = () => ({ - queryKey: ["software", "config"], - queryFn: fetchConfig, -}); - -/** - * Query to retrieve current software proposal - */ -const proposalQuery = () => ({ - queryKey: ["software", "proposal"], - queryFn: fetchProposal, -}); - -/** - * Query to retrieve selected product - */ -const selectedProductQuery = () => ({ - queryKey: ["software", "selectedProduct"], - queryFn: () => fetchConfig().then(({ product }) => product), - staleTime: Infinity, -}); - -/** - * Query to retrieve available products - */ -const productsQuery = () => ({ - queryKey: ["software", "products"], - queryFn: fetchProducts, - staleTime: Infinity, -}); - -/** - * Query to retrieve available licenses - */ -const licensesQuery = () => ({ - queryKey: ["software", "licenses"], - queryFn: fetchLicenses, - staleTime: Infinity, -}); - -/** - * Query to retrieve registration info - */ -const registrationQuery = () => ({ - queryKey: ["software", "registration"], - queryFn: fetchRegistration, -}); - -/** - * Query to retrieve available addons info - */ -const addonsQuery = () => ({ - queryKey: ["software", "registration", "addons"], - queryFn: fetchAddons, -}); - -/** - * Query to retrieve registered addons info - */ -const registeredAddonsQuery = () => ({ - queryKey: ["software", "registration", "addons", "registered"], - queryFn: fetchRegisteredAddons, -}); - -/** - * Query to retrieve available patterns - */ -const patternsQuery = () => ({ - queryKey: ["software", "patterns"], - queryFn: fetchPatterns, -}); - -/** - * Query to retrieve configured repositories - */ -const repositoriesQuery = () => ({ - queryKey: ["software", "repositories"], - queryFn: fetchRepositories, -}); - -/** - * Query to retrieve conflicts - */ -const conflictsQuery = () => ({ - queryKey: ["software", "conflicts"], - queryFn: fetchConflicts, -}); - -/** - * Hook that builds a mutation to update the software configuration - * - * @note it would trigger a general probing as a side-effect when mutation - * includes a product. - */ -const useConfigMutation = () => { - const queryClient = useQueryClient(); - - const query = { - mutationFn: updateConfig, - onSuccess: async (_, config: SoftwareConfig) => { - queryClient.invalidateQueries({ queryKey: ["software", "config"] }); - queryClient.invalidateQueries({ queryKey: ["software", "proposal"] }); - if (config.product) { - queryClient.invalidateQueries({ queryKey: ["software", "selectedProduct"] }); - await systemProbe(); - queryClient.invalidateQueries({ queryKey: ["storage"] }); - } - }, - }; - return useMutation(query); -}; - -/** - * Hook that builds a mutation for registering an addon - * - */ -const useRegisterAddonMutation = () => { - const queryClient = useQueryClient(); - - const query = { - mutationFn: registerAddon, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: registeredAddonsQuery().queryKey }); - }, - }; - return useMutation(query); -}; - -/** - * Hook that builds a mutation for reloading repositories - */ -const useRepositoryMutation = (callback: () => void) => { - const queryClient = useQueryClient(); - - const query = { - mutationFn: probe, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["software", "repositories"] }); - callback(); - }, - }; - return useMutation(query); -}; - -/** - * Returns available products and selected one, if any - */ -const useProduct = ( - options?: QueryHookOptions, -): { products?: Product[]; selectedProduct?: Product } => { - const func = options?.suspense ? useSuspenseQueries : useQueries; - const [ - { data: product, isPending: isSelectedProductPending }, - { data: products, isPending: isProductsPending }, - ] = func({ - queries: [selectedProductQuery(), productsQuery()], - }) as [{ data: SoftwareConfig; isPending: boolean }, { data: Product[]; isPending: boolean }]; - - if (isSelectedProductPending || isProductsPending) { - return { - products: [], - selectedProduct: undefined, - }; - } - - const selectedProduct = products.find((p: Product) => p.id === product); - return { - products, - selectedProduct, - }; -}; - -/** - * Returns available products and selected one, if any - */ -const useLicenses = (): { licenses: License[]; isPending: boolean } => { - const { data: licenses, isPending } = useQuery(licensesQuery()); - return { licenses, isPending }; -}; - -/** - * Returns a list of patterns with their selectedBy property properly set based on current proposal. - */ -const usePatterns = (): Pattern[] => { - const [{ data: proposal }, { data: patterns }] = useSuspenseQueries({ - queries: [proposalQuery(), patternsQuery()], - }); - - const selection: PatternsSelection = proposal.patterns; - - return patterns - .map((pattern: Pattern): Pattern => { - let selectedBy: SelectedBy; - switch (selection[pattern.name]) { - case 0: - selectedBy = SelectedBy.USER; - break; - case 1: - selectedBy = SelectedBy.AUTO; - break; - default: - selectedBy = SelectedBy.NONE; - } - return { ...pattern, selectedBy }; - }) - .sort((a: Pattern, b: Pattern) => a.order - b.order); -}; - -/** - * Returns current software proposal - */ -const useSoftwareProposal = (): SoftwareProposal => { - const { data: proposal } = useSuspenseQuery(proposalQuery()); - return proposal; -}; - -/** - * Returns registration info - */ -const useRegistration = (): RegistrationInfo => { - const { data: registration } = useSuspenseQuery(registrationQuery()); - return registration; -}; - -/** - * Returns details about the available addons - */ -const useAddons = (): AddonInfo[] => { - const { data: addons } = useSuspenseQuery(addonsQuery()); - return addons; -}; - -/** - * Returns list of registered addons - */ -const useRegisteredAddons = (): RegisteredAddonInfo[] => { - const { data: addons } = useSuspenseQuery(registeredAddonsQuery()); - return addons; -}; - -/** - * Returns repository info - */ -const useRepositories = (): Repository[] => { - const { data: repositories } = useSuspenseQuery(repositoriesQuery()); - return repositories; -}; - -/** - * Returns conclifts info - */ -const useConflicts = (): Conflict[] => { - const { data: conflicts } = useSuspenseQuery(conflictsQuery()); - return conflicts; -}; - -/** - * Hook that builds a mutation for solving a conflict - */ -const useConflictsMutation = () => { - const queryClient = useQueryClient(); - - const query = { - mutationFn: solveConflict, - onSuccess: async () => { - queryClient.invalidateQueries({ queryKey: conflictsQuery().queryKey }); - }, - }; - return useMutation(query); -}; - -/** - * Hook that returns a useEffect to listen for software proposal events - * - * When the configuration changes, it invalidates the config query. - */ -const useProductChanges = () => { - const client = useInstallerClient(); - const queryClient = useQueryClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "ProductChanged") { - queryClient.invalidateQueries({ queryKey: ["software"] }); - } - - if (event.type === "LocaleChanged") { - queryClient.invalidateQueries({ queryKey: ["software", "products"] }); - } - }); - }, [client, queryClient]); -}; - -/** - * Hook that returns a useEffect to listen for software proposal changes - * - * When the selected patterns change, it invalidates the proposal query. - */ -const useSoftwareProposalChanges = () => { - const client = useInstallerClient(); - const queryClient = useQueryClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "SoftwareProposalChanged") { - queryClient.invalidateQueries({ queryKey: ["software", "proposal"] }); - } - }); - }, [client, queryClient]); -}; - -/** - * Hook that registers a useEffect to listen for conflicts changes - * - */ -const useConflictsChanges = () => { - const client = useInstallerClient(); - const queryClient = useQueryClient(); - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "ConflictsChanged") { - const { conflicts } = event; - queryClient.setQueryData([conflictsQuery().queryKey], conflicts); - } - }); - }); -}; - -export { - configQuery, - productsQuery, - useAddons, - useConfigMutation, - useConflicts, - useConflictsMutation, - useConflictsChanges, - useLicenses, - usePatterns, - useProduct, - useProductChanges, - useSoftwareProposal, - useSoftwareProposalChanges, - useRegisterAddonMutation, - useRegisteredAddons, - useRegistration, - useRepositories, - useRepositoryMutation, -}; diff --git a/web/src/router.tsx b/web/src/router.tsx index bf69bdd606..17ac1cf949 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -30,7 +30,6 @@ import { InstallationProgress, LoginPage, } from "~/components/core"; -import StorageProgress from "~/components/storage/Progress"; import HostnamePage from "~/components/system/HostnamePage"; import OverviewPage from "~/components/overview/OverviewPage"; import l10nRoutes from "~/routes/l10n"; @@ -40,7 +39,7 @@ import registrationRoutes from "~/routes/registration"; import storageRoutes from "~/routes/storage"; import softwareRoutes from "~/routes/software"; import usersRoutes from "~/routes/users"; -import { HOSTNAME, ROOT as PATHS, STORAGE } from "./routes/paths"; +import { HOSTNAME, ROOT as PATHS } from "./routes/paths"; import { N_ } from "~/i18n"; const rootRoutes = () => [ @@ -91,10 +90,6 @@ const protectedRoutes = () => [ path: PATHS.installationFinished, element: , }, - { - path: STORAGE.progress, - element: , - }, ], }, ]; diff --git a/web/src/routes/paths.ts b/web/src/routes/paths.ts index 4a55df6907..fca4403430 100644 --- a/web/src/routes/paths.ts +++ b/web/src/routes/paths.ts @@ -75,7 +75,6 @@ const SOFTWARE = { const STORAGE = { root: "/storage", - progress: "/storage/progress", editBootDevice: "/storage/boot-device/edit", editEncryption: "/storage/encryption/edit", editSpacePolicy: "/storage/:collection/:index/space-policy/edit", diff --git a/web/src/routes/software.tsx b/web/src/routes/software.tsx index b47699f143..26aff0af34 100644 --- a/web/src/routes/software.tsx +++ b/web/src/routes/software.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2026] SUSE LLC * * All Rights Reserved. * @@ -23,7 +23,6 @@ import React from "react"; import SoftwarePage from "~/components/software/SoftwarePage"; import SoftwarePatternsSelection from "~/components/software/SoftwarePatternsSelection"; -import SoftwareConflictsPage from "~/components/software/SoftwareConflictsPage"; import { Route } from "~/types/routes"; import { SOFTWARE as PATHS } from "~/routes/paths"; import { N_ } from "~/i18n"; @@ -43,10 +42,6 @@ const routes = (): Route => ({ path: PATHS.patternsSelection, element: , }, - { - path: PATHS.conflicts, - element: , - }, ], }); diff --git a/web/src/test-utils.tsx b/web/src/test-utils.tsx index 57f66c2b24..f97eaa8e4d 100644 --- a/web/src/test-utils.tsx +++ b/web/src/test-utils.tsx @@ -41,8 +41,7 @@ import { StorageUiStateProvider } from "~/context/storage-ui-state"; import { DummyWSClient } from "~/client/ws"; import { Status } from "~/model/status"; import { Question } from "~/model/question"; - -import type { Product } from "~/types/software"; +import type { Product } from "~/model/system"; import type { Config as ProductConfig } from "~/model/config/product"; /** diff --git a/web/src/types/progress.ts b/web/src/types/progress.ts deleted file mode 100644 index d1ef53f6ca..0000000000 --- a/web/src/types/progress.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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 APIProgress = { - currentStep: number; - maxSteps: number; - currentTitle: string; - finished: boolean; - steps?: string[]; - path: string; -}; - -class Progress { - total: number; - current: number; - message: string; - finished: boolean; - steps: string[]; - - constructor(current: number, total: number, message: string, finished: boolean, steps: string[]) { - this.current = current; - this.total = total; - this.message = message; - this.finished = finished; - this.steps = steps; - } - - static fromApi(progress: APIProgress) { - const { - currentStep: current, - maxSteps: total, - currentTitle: message, - finished, - steps = [], - } = progress; - return new Progress(current, total, message, finished, steps); - } -} - -export { Progress }; -export type { APIProgress }; diff --git a/web/src/types/queries.ts b/web/src/types/queries.ts deleted file mode 100644 index c08e5e2237..0000000000 --- a/web/src/types/queries.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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. - */ - -/** - * Typical options for our queries hooks. - */ -export type QueryHookOptions = { - suspense?: boolean; -}; diff --git a/web/src/types/software.ts b/web/src/types/software.ts deleted file mode 100644 index 3023160290..0000000000 --- a/web/src/types/software.ts +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (c) [2024-2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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. - */ - -/** @deprecated */ - -/** - * Enum for the reasons to select a pattern - */ -enum SelectedBy { - /** Selected by the user */ - USER = 0, - /** Automatically selected as a dependency of another package */ - AUTO = 1, - /** No selected */ - NONE = 2, -} - -type Product = { - /** Product ID (e.g., "Leap") */ - id: string; - /** Product name (e.g., "openSUSE Leap 15.4") */ - name: string; - /** Product description */ - description?: string; - /** Product icon (e.g., "default.svg") */ - icon?: string; - /** If product is registrable or not */ - registration: boolean; - /** The product license id, if any */ - license?: string; - /** Translations */ - translations?: { - /** The key is the locale (e.g., "en", "pt_BR") */ - description: Record; - }; -}; - -type License = { - /** License ID (e.g., "license.sle") */ - id: string; - /** Available locales */ - languages: string[]; -}; - -type LicenseContent = { - /** License ID (e.g., "license.sle") */ - id: string; - /** License body */ - body: string; - /** License language (e.g., "en-US") */ - language: string; -}; - -type PatternsSelection = { [key: string]: SelectedBy }; - -type SoftwareProposal = { - /** Used space in human-readable form */ - size: string; - /** Selected patterns and the reason */ - patterns: PatternsSelection; -}; - -type SoftwareConfig = { - /** Product to install */ - product?: string; - /** An object where the keys are the pattern names and the values whether to install them or not */ - patterns?: { [key: string]: boolean }; - /** A list of user selected packages */ - packages?: string[]; -}; - -type Pattern = { - /** Pattern name (internal ID) */ - name: string; - /** Pattern category */ - category: string; - /** User visible pattern name */ - summary: string; - /** Long description of the pattern */ - description: string; - /** {number} order - Display order (string!) */ - order: number; - /** Icon name (not path or file name!) */ - icon: string; - /** Whether the pattern if selected and by whom */ - selectedBy?: SelectedBy; -}; - -type Repository = { - repo_id: number; - alias: string; - name: string; - raw_url: string; - product_dir: string; - enabled: boolean; - loaded: boolean; -}; - -type RegistrationInfo = { - registered: boolean; - key: string; - email: string; - url: string; -}; - -type AddonInfo = { - id: string; - version: string; - label: string; - available: boolean; - free: boolean; - recommended: boolean; - description: string; - release: string; - registration: AddonRegistered | AddonUnregistered; -}; - -type AddonRegistered = { - status: "registered"; - code?: string; -}; - -type AddonUnregistered = { - status: "notRegistered"; -}; - -type RegisteredAddonInfo = { - id: string; - version: string | null; - registrationCode: string; -}; - -type ConflictSolutionOption = { - id: number; - description: string; - details: string | null; -}; - -type Conflict = { - id: number; - description: string; - details: string | null; - solutions: ConflictSolutionOption[]; -}; - -type ConflictSolution = { - conflictId: Conflict["id"]; - solutionId: ConflictSolutionOption["id"]; -}; - -export { SelectedBy }; -export type { - AddonInfo, - Conflict, - ConflictSolution, - ConflictSolutionOption, - License, - LicenseContent, - Pattern, - PatternsSelection, - Product, - RegisteredAddonInfo, - RegistrationInfo, - Repository, - SoftwareConfig, - SoftwareProposal, -}; diff --git a/web/src/types/status.ts b/web/src/types/status.ts deleted file mode 100644 index 7c598f69b2..0000000000 --- a/web/src/types/status.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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. - */ - -/* - * Enum that represents the installation phase - */ -enum InstallationPhase { - Startup = 0, - Config = 1, - Install = 2, - Finish = 3, -} - -/* - * Status of the installer - */ -type InstallerStatus = { - /** Whether the installer is busy */ - isBusy: boolean; - /** Installation phase */ - phase: InstallationPhase; - /** Whether the installation can be performed or not */ - canInstall: boolean; - /** Whether the installer is running on Iguana */ - useIguana: boolean; -}; - -export type { InstallerStatus }; -export { InstallationPhase }; diff --git a/web/src/types/users.ts b/web/src/types/users.ts deleted file mode 100644 index 51d780f67b..0000000000 --- a/web/src/types/users.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * 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 { TranslatedString } from "~/i18n"; - -type FirstUser = { - fullName: string; - userName: string; - password: string; - hashedPassword: boolean; -}; - -type RootUser = { - password: string; - hashedPassword: boolean; - sshPublicKey: string; -}; - -type PasswordCheckResult = { - success?: number; - failure?: TranslatedString; -}; - -export type { FirstUser, RootUser, PasswordCheckResult };