diff --git a/rust/agama-server/src/manager/web.rs b/rust/agama-server/src/manager/web.rs index 4dc7e352e8..79d87c4f2c 100644 --- a/rust/agama-server/src/manager/web.rs +++ b/rust/agama-server/src/manager/web.rs @@ -43,10 +43,10 @@ pub struct ManagerState<'a> { pub struct InstallerStatus { /// Current installation phase. phase: InstallationPhase, - /// List of busy services. - busy: Vec, + /// Whether the service is busy. + is_busy: bool, /// Whether Agama is running on Iguana. - iguana: bool, + use_iguana: bool, /// Whether it is possible to start the installation. can_install: bool, } @@ -183,8 +183,8 @@ async fn installer_status( let status = InstallerStatus { phase, can_install, - busy: state.manager.busy_services().await?, - iguana: state.manager.use_iguana().await?, + is_busy: state.manager.is_busy().await, + use_iguana: state.manager.use_iguana().await?, }; Ok(Json(status)) } diff --git a/rust/agama-server/src/web/event.rs b/rust/agama-server/src/web/event.rs index 87a4c94787..5b11eae12b 100644 --- a/rust/agama-server/src/web/event.rs +++ b/rust/agama-server/src/web/event.rs @@ -49,9 +49,6 @@ pub enum Event { InstallationPhaseChanged { phase: InstallationPhase, }, - BusyServicesChanged { - services: Vec, - }, ServiceStatusChanged { service: String, status: u32, diff --git a/web/src/App.jsx b/web/src/App.jsx index 5ddfced24a..877f94d305 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -27,12 +27,12 @@ import { ServerError, Installation } from "~/components/core"; import { useInstallerL10n } from "./context/installerL10n"; import { useInstallerClientStatus } from "~/context/installer"; import { useProduct, useProductChanges } from "./queries/software"; -import { CONFIG, INSTALL, STARTUP } from "~/client/phase"; -import { BUSY } from "~/client/status"; import { useL10nConfigChanges } from "~/queries/l10n"; import { useIssuesChanges } from "./queries/issues"; +import { useInstallerStatus, useInstallerStatusChanges } from "./queries/status"; import { PATHS as PRODUCT_PATHS } from "./routes/products"; import SimpleLayout from "./SimpleLayout"; +import { InstallationPhase } from "./types/status"; /** * Main application component. @@ -43,18 +43,20 @@ import SimpleLayout from "./SimpleLayout"; */ function App() { const location = useLocation(); - const { connected, error, phase, status } = useInstallerClientStatus(); + const { isBusy, phase } = useInstallerStatus({ suspense: true }); + const { connected, error } = useInstallerClientStatus(); const { selectedProduct, products } = useProduct(); const { language } = useInstallerL10n(); useL10nConfigChanges(); useProductChanges(); useIssuesChanges(); + useInstallerStatusChanges(); const Content = () => { if (error) return ; - if (phase === INSTALL) { - return ; + if (phase === InstallationPhase.Install) { + return ; } if (!products || !connected) @@ -64,7 +66,7 @@ function App() { ); - if ((phase === STARTUP && status === BUSY) || phase === undefined || status === undefined) { + if (phase === InstallationPhase.Startup && isBusy) { return ; } @@ -72,7 +74,11 @@ function App() { return ; } - if (phase === CONFIG && status === BUSY && location.pathname !== PRODUCT_PATHS.progress) { + if ( + phase === InstallationPhase.Config && + isBusy && + location.pathname !== PRODUCT_PATHS.progress + ) { return ; } diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index a4a4b0d96d..a3a2a4ec51 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -25,11 +25,7 @@ import { installerRender } from "~/test-utils"; import App from "./App"; import { createClient } from "~/client"; -import { STARTUP, CONFIG, INSTALL } from "~/client/phase"; -import { IDLE, BUSY } from "~/client/status"; -import { useL10nConfigChanges } from "./queries/l10n"; -import { useProductChanges } from "./queries/software"; -import { useIssuesChanges } from "./queries/issues"; +import { InstallationPhase } from "./types/status"; jest.mock("~/client"); @@ -59,15 +55,19 @@ jest.mock("~/queries/issues", () => ({ })); const mockClientStatus = { - connected: true, - error: false, - phase: STARTUP, - status: BUSY, + phase: InstallationPhase.Startup, + isBusy: true, }; +jest.mock("~/queries/status", () => ({ + ...jest.requireActual("~/queries/status"), + useInstallerStatus: () => mockClientStatus, + useInstallerStatusChanges: () => jest.fn(), +})); + jest.mock("~/context/installer", () => ({ ...jest.requireActual("~/context/installer"), - useInstallerClientStatus: () => mockClientStatus, + useInstallerClientStatus: () => ({ connected: true, error: false }), })); // Mock some components, @@ -115,8 +115,8 @@ describe("App", () => { describe("when the service is busy during startup", () => { beforeEach(() => { - mockClientStatus.phase = STARTUP; - mockClientStatus.status = BUSY; + mockClientStatus.phase = InstallationPhase.Startup; + mockClientStatus.isBusy = true; }); it("renders the Loading screen", async () => { @@ -125,14 +125,14 @@ describe("App", () => { }); }); - describe("on the CONFIG phase", () => { + describe("on the configuration phase", () => { beforeEach(() => { - mockClientStatus.phase = CONFIG; + mockClientStatus.phase = InstallationPhase.Config; }); describe("if the service is busy", () => { beforeEach(() => { - mockClientStatus.status = BUSY; + mockClientStatus.isBusy = true; mockSelectedProduct = { id: "Tumbleweed" }; }); @@ -144,7 +144,7 @@ describe("App", () => { describe("if the service is not busy", () => { beforeEach(() => { - mockClientStatus.status = IDLE; + mockClientStatus.isBusy = false; }); it("renders the application content", async () => { @@ -154,9 +154,9 @@ describe("App", () => { }); }); - describe("on the INSTALL phase", () => { + describe("on the installaiton phase", () => { beforeEach(() => { - mockClientStatus.phase = INSTALL; + mockClientStatus.phase = InstallationPhase.Install; mockSelectedProduct = { id: "Fake product" }; }); diff --git a/web/src/client/index.js b/web/src/client/index.js index b1920f1f18..95185d56aa 100644 --- a/web/src/client/index.js +++ b/web/src/client/index.js @@ -23,10 +23,7 @@ import { L10nClient } from "./l10n"; import { ManagerClient } from "./manager"; -import { Monitor } from "./monitor"; -import { ProductClient, SoftwareClient } from "./software"; import { StorageClient } from "./storage"; -import phase from "./phase"; import { QuestionsClient } from "./questions"; import { NetworkClient } from "./network"; import { HTTPClient, WSClient } from "./http"; @@ -35,10 +32,7 @@ import { HTTPClient, WSClient } from "./http"; * @typedef {object} InstallerClient * @property {L10nClient} l10n - localization client. * @property {ManagerClient} manager - manager client. - * property {Monitor} monitor - service monitor. (FIXME) * @property {NetworkClient} network - network client. - * @property {ProductClient} product - product client. - * @property {SoftwareClient} software - software client. * @property {StorageClient} storage - storage client. * @property {QuestionsClient} questions - questions client. * @property {() => WSClient} ws - Agama WebSocket client. @@ -60,11 +54,8 @@ const createClient = (url) => { const client = new HTTPClient(url); const l10n = new L10nClient(client); // TODO: unify with the manager client - const product = new ProductClient(client); const manager = new ManagerClient(client); - // const monitor = new Monitor(address, MANAGER_SERVICE); const network = new NetworkClient(client); - const software = new SoftwareClient(client); const storage = new StorageClient(client); const questions = new QuestionsClient(client); @@ -73,11 +64,8 @@ const createClient = (url) => { return { l10n, - product, manager, - // monitor, network, - software, storage, questions, isConnected, @@ -94,4 +82,4 @@ const createDefaultClient = async () => { return createClient(httpUrl); }; -export { createClient, createDefaultClient, phase }; +export { createClient, createDefaultClient }; diff --git a/web/src/client/manager.js b/web/src/client/manager.js index 2c4a50113b..313cf46f6b 100644 --- a/web/src/client/manager.js +++ b/web/src/client/manager.js @@ -21,16 +21,10 @@ // @ts-check -import { WithStatus } from "./mixins"; - -const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; - /** - * Manager base client - * - * @ignore + * Client to interact with the Agama manager service */ -class ManagerBaseClient { +class ManagerClient { /** * @param {import("./http").HTTPClient} client - HTTP client. */ @@ -60,24 +54,6 @@ class ManagerBaseClient { return this.client.post("/manager/install", {}); } - /** - * Checks whether it is possible to start the installation - * - * It might happen that there are some validation errors. In that case, - * it is not possible to proceed with the installation. - * - * @return {Promise} - */ - async canInstall() { - const response = await this.client.get("/manager/installer"); - if (!response.ok) { - console.error("Failed to get installer config: ", response); - return false; - } - const installer = await response.json(); - return installer.canInstall; - } - /** * Returns the binary content of the YaST logs file * @@ -93,61 +69,12 @@ class ManagerBaseClient { return response; } - /** - * Return the installer status - * - * @return {Promise} - */ - async getPhase() { - const response = await this.client.get("/manager/installer"); - if (!response.ok) { - console.error("Failed to get installer config: ", response); - return 0; - } - const installer = await response.json(); - return installer.phase; - } - - /** - * Register a callback to run when the "CurrentInstallationPhase" changes - * - * @param {function} handler - callback function - * @return {import ("./dbus").RemoveFn} function to disable the callback - */ - onPhaseChange(handler) { - return this.client.onEvent("InstallationPhaseChanged", ({ phase }) => { - if (phase) { - handler(phase); - } - }); - } - /** * Runs cleanup when installation is done */ finishInstallation() { return this.client.post("/manager/finish", {}); } - - /** - * Returns whether Iguana is used on the system - * - * @return {Promise} - */ - async useIguana() { - const response = await this.client.get("/manager/installer"); - if (!response.ok) { - console.error("Failed to get installer config: ", response); - return false; - } - const installer = await response.json(); - return installer.iguana; - } } -/** - * Client to interact with the Agama manager service - */ -class ManagerClient extends WithStatus(ManagerBaseClient, "/manager/status", MANAGER_SERVICE) {} - export { ManagerClient }; diff --git a/web/src/client/manager.test.js b/web/src/client/manager.test.js index ab5cd66ae0..3e1363978e 100644 --- a/web/src/client/manager.test.js +++ b/web/src/client/manager.test.js @@ -46,13 +46,6 @@ jest.mock("./http", () => { let client; -const installerStatus = { - phase: 1, - busy: [], - iguana: false, - canInstall: true, -}; - beforeEach(() => { client = new ManagerClient(new HTTPClient(new URL("http://localhost"))); }); @@ -64,17 +57,6 @@ describe("#startProbing", () => { }); }); -describe("#getPhase", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(installerStatus); - }); - - it("resolves to the current phase", () => { - const phase = client.getPhase(); - expect(phase).resolves.toEqual(1); - }); -}); - describe("#startInstallation", () => { it("starts the installation", async () => { await client.startInstallation(); @@ -93,30 +75,6 @@ describe("#rebootSystem", () => { }); }); -describe("#canInstall", () => { - describe("when the system can be installed", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue(installerStatus); - }); - - it("returns true", async () => { - const install = await client.canInstall(); - expect(install).toEqual(true); - }); - }); - - describe("when the system cannot be installed", () => { - beforeEach(() => { - mockJsonFn.mockResolvedValue({ ...installerStatus, canInstall: false }); - }); - - it("returns false", async () => { - const install = await client.canInstall(); - expect(install).toEqual(false); - }); - }); -}); - describe.skip("#fetchLogs", () => { // beforeEach(() => { // managerProxy.CollectLogs = jest.fn(() => "/tmp/y2log-hWBn95.tar.xz"); diff --git a/web/src/client/monitor.js b/web/src/client/monitor.js deleted file mode 100644 index a298f64f17..0000000000 --- a/web/src/client/monitor.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) [2022] 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. - */ - -// @ts-check - -import DBusClient from "./dbus"; - -const NAME_OWNER_CHANGED = { - interface: "org.freedesktop.DBus", - member: "NameOwnerChanged", -}; - -/** - * Monitor a D-Bus service - */ -class Monitor { - /** - * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. - * @param {string} serviceName - name of the service to monitor - */ - constructor(address, serviceName) { - this.serviceName = serviceName; - this.client = new DBusClient("org.freedesktop.DBus", address); - } - - /** - * Registers a callback to be executed when the D-Bus service connection changes - * - * @param {() => void} handler - function to execute when the client gets - * disconnected. - * @return {() => void} function to deregister the callbacks. - */ - onDisconnect(handler) { - return this.client.onSignal(NAME_OWNER_CHANGED, (_path, _interface, _signal, args) => { - const [service, , newOwner] = args; - if (service === this.serviceName && newOwner === "") { - handler(); - } - }); - } -} - -export { Monitor }; diff --git a/web/src/client/software.js b/web/src/client/software.js deleted file mode 100644 index 1e886bad56..0000000000 --- a/web/src/client/software.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) [2022-2023] 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. - */ - -// @ts-check - -import { WithStatus } from "./mixins"; - -const SOFTWARE_SERVICE = "org.opensuse.Agama.Software1"; - -/** - * Software client - * - * @ignore - */ -class SoftwareBaseClient { - /** - * @param {string|undefined} address - D-Bus address; if it is undefined, it uses the system bus. - */ - /** - * @param {import("./http").HTTPClient} client - HTTP client. - */ - constructor(client) { - this.client = client; - } -} - -/** - * Manages software and product configuration. - */ -class SoftwareClient extends WithStatus(SoftwareBaseClient, "/software/status", SOFTWARE_SERVICE) {} - -class ProductClient { - /** - * @param {import("./http").HTTPClient} client - HTTP client. - */ - constructor(client) { - this.client = client; - } -} - -export { ProductClient, SoftwareClient }; diff --git a/web/src/components/core/Installation.jsx b/web/src/components/core/Installation.jsx index abd7504560..6cf7532681 100644 --- a/web/src/components/core/Installation.jsx +++ b/web/src/components/core/Installation.jsx @@ -21,10 +21,9 @@ import React from "react"; import { InstallationProgress, InstallationFinished } from "~/components/core"; -import { IDLE } from "~/client/status"; -function Installation({ status }) { - return status === IDLE ? : ; +function Installation({ isBusy }) { + return isBusy ? : ; } export default Installation; diff --git a/web/src/components/core/InstallationFinished.jsx b/web/src/components/core/InstallationFinished.jsx index 4f37f8a57f..2b02d6935e 100644 --- a/web/src/components/core/InstallationFinished.jsx +++ b/web/src/components/core/InstallationFinished.jsx @@ -42,6 +42,7 @@ import { EncryptionMethods } from "~/client/storage"; import { _ } from "~/i18n"; import { useInstallerClient } from "~/context/installer"; import alignmentStyles from "@patternfly/react-styles/css/utilities/Alignment/alignment"; +import { useInstallerStatus } from "~/queries/status"; const TpmHint = () => { const [isExpanded, setIsExpanded] = useState(false); @@ -74,19 +75,17 @@ const SuccessIcon = () => - {usingIguana + {useIguana ? _("At this point you can power off the machine.") : _( "At this point you can reboot the machine to log in to the new system.", @@ -127,7 +126,7 @@ function InstallationFinished() { diff --git a/web/src/components/core/InstallationFinished.test.jsx b/web/src/components/core/InstallationFinished.test.jsx index c6901c4981..51a3d27f89 100644 --- a/web/src/components/core/InstallationFinished.test.jsx +++ b/web/src/components/core/InstallationFinished.test.jsx @@ -25,9 +25,13 @@ import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { createClient } from "~/client"; import { EncryptionMethods } from "~/client/storage"; - import InstallationFinished from "./InstallationFinished"; +jest.mock("~/queries/status", () => ({ + ...jest.requireActual("~/queries/status"), + useInstallerStatus: () => ({ isBusy: false, useIguana: false, phase: 2, canInstall: false }), +})); + jest.mock("~/client"); jest.mock("~/components/core/InstallerOptions", () => () =>
Installer Options
); diff --git a/web/src/components/overview/SoftwareSection.test.tsx b/web/src/components/overview/SoftwareSection.test.tsx index eb02e06e78..d476b71799 100644 --- a/web/src/components/overview/SoftwareSection.test.tsx +++ b/web/src/components/overview/SoftwareSection.test.tsx @@ -32,7 +32,7 @@ let mockTestingProposal: SoftwareProposal; jest.mock("~/queries/software", () => ({ usePatterns: () => mockTestingPatterns, useProposal: () => mockTestingProposal, - useProposalChanges: jest.fn(), + useProposalChanges: () => jest.fn(), })); describe("SoftwareSection", () => { diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.jsx index 9b1c93e2b1..d654f7b5ac 100644 --- a/web/src/components/product/ProductSelectionProgress.jsx +++ b/web/src/components/product/ProductSelectionProgress.jsx @@ -27,6 +27,7 @@ import { Page, ProgressReport } from "~/components/core"; import { IDLE } from "~/client/status"; import { useInstallerClient } from "~/context/installer"; import { PATHS } from "~/router"; +import { useInstallerStatus } from "~/queries/status"; /** * @component @@ -35,15 +36,9 @@ import { PATHS } from "~/router"; */ function ProductSelectionProgress() { const { selectedProduct } = useProduct({ suspense: true }); - const { manager } = useInstallerClient(); - const [status, setStatus] = useState(); + const { isBusy } = useInstallerStatus({ suspense: true }); - useEffect(() => { - manager.getStatus().then(setStatus); - return manager.onStatusChange(setStatus); - }, [manager, setStatus]); - - if (status === IDLE) return ; + if (!isBusy) return ; return ( diff --git a/web/src/context/installer.jsx b/web/src/context/installer.jsx index 0b4b4c8246..2cbec5ad18 100644 --- a/web/src/context/installer.jsx +++ b/web/src/context/installer.jsx @@ -30,8 +30,6 @@ const InstallerClientContext = React.createContext(null); const InstallerClientStatusContext = React.createContext({ connected: false, error: false, - phase: undefined, - status: undefined, }); /** @@ -80,8 +78,6 @@ function InstallerClientProvider({ children, client = null }) { const [value, setValue] = useState(client); const [connected, setConnected] = useState(false); const [error, setError] = useState(false); - const [status, setStatus] = useState(undefined); - const [phase, setPhase] = useState(undefined); useEffect(() => { const connectClient = async () => { @@ -104,31 +100,6 @@ function InstallerClientProvider({ children, client = null }) { if (!value) connectClient(); }, [setValue, value]); - useEffect(() => { - if (value) { - return value.manager.onPhaseChange(setPhase); - } - }, [value, setPhase]); - - useEffect(() => { - if (value) { - return value.manager.onStatusChange(setStatus); - } - }, [value, setStatus]); - - useEffect(() => { - const loadPhase = async () => { - const initialPhase = await value.manager.getPhase(); - const initialStatus = await value.manager.getStatus(); - setPhase(initialPhase); - setStatus(initialStatus); - }; - - if (value) { - loadPhase().catch(console.error); - } - }, [value, setPhase, setStatus]); - useEffect(() => { if (!value) return; @@ -145,7 +116,7 @@ function InstallerClientProvider({ children, client = null }) { return ( - + {children} diff --git a/web/src/context/installer.test.jsx b/web/src/context/installer.test.jsx index 0e025f2a82..0c190518ef 100644 --- a/web/src/context/installer.test.jsx +++ b/web/src/context/installer.test.jsx @@ -24,20 +24,16 @@ import { act, screen } from "@testing-library/react"; import { createDefaultClient } from "~/client"; import { plainRender, createCallbackMock } from "~/test-utils"; import { InstallerClientProvider, useInstallerClientStatus } from "./installer"; -import { STARTUP } from "~/client/phase"; -import { BUSY } from "~/client/status"; jest.mock("~/client"); // Helper component to check the client status. const ClientStatus = () => { - const { connected, phase, status } = useInstallerClientStatus(); + const { connected } = useInstallerClientStatus(); return (
  • {`connected: ${connected}`}
  • -
  • {`phase: ${phase}`}
  • -
  • {`status: ${status}`}
); }; @@ -48,12 +44,6 @@ describe("installer context", () => { return { onConnect: jest.fn(), onDisconnect: jest.fn(), - manager: { - getPhase: jest.fn().mockResolvedValue(STARTUP), - getStatus: jest.fn().mockResolvedValue(BUSY), - onPhaseChange: jest.fn(), - onStatusChange: jest.fn(), - }, }; }); }); @@ -65,7 +55,5 @@ describe("installer context", () => { , ); await screen.findByText("connected: false"); - await screen.findByText("phase: 0"); - await screen.findByText("status: 1"); }); }); diff --git a/web/src/context/installerL10n.test.jsx b/web/src/context/installerL10n.test.jsx index 7b47d3f140..50af75fa4b 100644 --- a/web/src/context/installerL10n.test.jsx +++ b/web/src/context/installerL10n.test.jsx @@ -35,12 +35,6 @@ const setUILocaleFn = jest.fn().mockResolvedValue(); const client = { onConnect: jest.fn(), onDisconnect: jest.fn(), - manager: { - getPhase: jest.fn(), - getStatus: jest.fn(), - onPhaseChange: jest.fn(), - onStatusChange: jest.fn(), - }, l10n: { getUILocale: getUILocaleFn, setUILocale: setUILocaleFn, diff --git a/web/src/queries/status.ts b/web/src/queries/status.ts new file mode 100644 index 0000000000..d5e391e8ae --- /dev/null +++ b/web/src/queries/status.ts @@ -0,0 +1,91 @@ +/* + * 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 { useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import React from "react"; +import { useInstallerClient } from "~/context/installer"; +import { InstallerStatus } from "~/types/status"; + +const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; + +/** + * Returns a query for retrieving the installer status + */ +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 }; + }), +}); + +/** + * Hook that returns the installer status + * + * @param options - Query options + */ +const useInstallerStatus = (options?: QueryHookOptions): InstallerStatus | undefined => { + const query = statusQuery(); + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func(query); + return data; +}; + +/** + * Hook that registers a useEffect to listen for status changes + * + * It listens for all status changes but updates the query only + * if it is already cached. + */ +const useInstallerStatusChanges = () => { + const client = useInstallerClient(); + const queryClient = useQueryClient(); + React.useEffect(() => { + if (!client) return; + + return client.ws().onEvent((event) => { + const { type } = event; + const data = queryClient.getQueryData(["status"]) as object; + if (!data) { + return; + } + + if (type === "InstallationPhaseChanged") { + const { phase } = event; + queryClient.setQueryData(["status"], { ...data, phase }); + } + + if (type === "ServiceStatusChanged" && event.service === MANAGER_SERVICE) { + const { status } = event; + queryClient.setQueryData(["status"], { ...data, busy: status === 1 }); + } + + if (type === "IssuesChanged") { + queryClient.invalidateQueries({ queryKey: ["status"] }); + } + }); + }); +}; + +export { useInstallerStatus, useInstallerStatusChanges }; diff --git a/web/src/queries/users.ts b/web/src/queries/users.ts index 2404810de8..8946268478 100644 --- a/web/src/queries/users.ts +++ b/web/src/queries/users.ts @@ -151,7 +151,6 @@ const useRootUserChanges = () => { if (!client) return; return client.ws().onEvent((event) => { - console.log("event.type", event.type); if (event.type === "RootChanged") { const { password, sshkey } = event; queryClient.setQueryData(["users", "root"], (oldRoot: RootUser) => { diff --git a/web/src/test-utils.js b/web/src/test-utils.js index 8452491f88..b296837e10 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -97,14 +97,6 @@ const Providers = ({ children, withL10n }) => { client.manager = {}; } - client.manager = { - getPhase: noop, - getStatus: noop, - onPhaseChange: noop, - onStatusChange: noop, - ...client.manager, - }; - if (withL10n) { return ( diff --git a/web/src/client/phase.js b/web/src/types/status.ts similarity index 57% rename from web/src/client/phase.js rename to web/src/types/status.ts index 5024a61448..9174911c84 100644 --- a/web/src/client/phase.js +++ b/web/src/types/status.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2024] SUSE LLC * * All Rights Reserved. * @@ -19,12 +19,28 @@ * find current contact information at www.suse.com. */ -export const STARTUP = 0; -export const CONFIG = 1; -export const INSTALL = 2; +/* + * Enum that represents the installation phase + */ +enum InstallationPhase { + Startup = 0, + Config = 1, + Install = 2, +} -export default { - STARTUP, - CONFIG, - INSTALL, +/* + * 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 };