From f3c634b6cb021c6202cc949f4d5d55a96784cf5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 5 Nov 2025 15:49:40 +0000 Subject: [PATCH 1/9] Add solve_storage_model endpoint --- rust/agama-manager/src/message.rs | 15 +++++++++++++ rust/agama-manager/src/service.rs | 14 ++++++++++++ rust/agama-server/src/server/web.rs | 27 +++++++++++++++++++++-- rust/agama-server/src/web/docs/config.rs | 1 + rust/agama-utils/src/api.rs | 1 + rust/agama-utils/src/api/query.rs | 28 ++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 rust/agama-utils/src/api/query.rs diff --git a/rust/agama-manager/src/message.rs b/rust/agama-manager/src/message.rs index 83676f56a6..ed78bc03f5 100644 --- a/rust/agama-manager/src/message.rs +++ b/rust/agama-manager/src/message.rs @@ -141,3 +141,18 @@ impl SetStorageModel { impl Message for SetStorageModel { type Reply = (); } + +#[derive(Clone)] +pub struct SolveStorageModel { + pub model: Value, +} + +impl SolveStorageModel { + pub fn new(model: Value) -> Self { + Self { model } + } +} + +impl Message for SolveStorageModel { + type Reply = Option; +} diff --git a/rust/agama-manager/src/service.rs b/rust/agama-manager/src/service.rs index 29e03e6dfc..8b6c37120a 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -291,3 +291,17 @@ impl MessageHandler for Service { .await?) } } + +#[async_trait] +impl MessageHandler for Service { + /// It solves the storage model. + async fn handle( + &mut self, + message: message::SolveStorageModel, + ) -> Result, Error> { + Ok(self + .storage + .call(storage::message::SolveConfigModel::new(message.model)) + .await?) + } +} diff --git a/rust/agama-server/src/server/web.rs b/rust/agama-server/src/server/web.rs index 389b686cc7..2c7dcc8de5 100644 --- a/rust/agama-server/src/server/web.rs +++ b/rust/agama-server/src/server/web.rs @@ -26,14 +26,14 @@ use agama_manager::{self as manager, message}; use agama_utils::{ actor::Handler, api::{ - event, + event, query, question::{Question, QuestionSpec, UpdateQuestion}, Action, Config, IssueMap, Patch, Status, SystemInfo, }, question, }; use axum::{ - extract::State, + extract::{Query, State}, response::{IntoResponse, Response}, routing::{get, post}, Json, Router, @@ -109,6 +109,7 @@ pub async fn server_service( "/private/storage_model", get(get_storage_model).put(set_storage_model), ) + .route("/private/solve_storage_model", get(solve_storage_model)) .with_state(state)) } @@ -378,6 +379,28 @@ async fn set_storage_model( Ok(()) } +/// Solves a storage config model. +#[utoipa::path( + get, + path = "/private/solve_storage_model", + context_path = "/api/v2", + params(query::SolveStorageModel), + responses( + (status = 200, description = "Solve the storage model", body = String), + (status = 400, description = "Not possible to solve the storage model") + ) +)] +async fn solve_storage_model( + State(state): State, + Query(params): Query, +) -> Result>, Error> { + let solved_model = state + .manager + .call(message::SolveStorageModel::new(params.model)) + .await?; + Ok(Json(solved_model)) +} + fn to_option_response(value: Option) -> Response { match value { Some(inner) => Json(inner).into_response(), diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index ef0c136a18..1b218aa7f1 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -182,6 +182,7 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .schema_from::() .schema_from::() .schema_from::() + .schema_from::() .schema_from::() .build() } diff --git a/rust/agama-utils/src/api.rs b/rust/agama-utils/src/api.rs index 89ccd79dcc..567e6807d9 100644 --- a/rust/agama-utils/src/api.rs +++ b/rust/agama-utils/src/api.rs @@ -52,5 +52,6 @@ mod action; pub use action::Action; pub mod l10n; +pub mod query; pub mod question; pub mod storage; diff --git a/rust/agama-utils/src/api/query.rs b/rust/agama-utils/src/api/query.rs new file mode 100644 index 0000000000..7bcadf2e8b --- /dev/null +++ b/rust/agama-utils/src/api/query.rs @@ -0,0 +1,28 @@ +// 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. + +use serde::Deserialize; +use serde_json::Value; + +#[derive(Deserialize, utoipa::IntoParams, utoipa::ToSchema)] +pub struct SolveStorageModel { + /// Serialized storage model. + pub model: Value, +} From 0dc8bc97b46282df95e40e1f15fdff27cfefe9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 7 Nov 2025 15:06:00 +0000 Subject: [PATCH 2/9] Initial adaptation to the new API --- web/src/App.tsx | 5 +- web/src/api.ts | 105 +++++ web/src/api/{api.ts => action.ts} | 44 +- web/src/{types => api}/config.ts | 5 +- web/src/api/hostname.ts | 2 +- web/src/{types/system.ts => api/issue.ts} | 8 +- web/src/api/issues.ts | 2 +- web/src/{types => api/l10n}/proposal.ts | 6 +- web/src/{types/l10n.ts => api/l10n/system.ts} | 25 +- web/src/api/manager.ts | 2 +- web/src/api/network.ts | 2 +- web/src/api/progress.ts | 2 +- web/src/api/proposal.ts | 32 ++ web/src/api/{storage/types.ts => question.ts} | 8 +- web/src/api/questions.ts | 2 +- web/src/api/software.ts | 2 +- web/src/api/status.ts | 7 +- web/src/api/storage.ts | 92 +--- web/src/api/storage/{types => }/config.ts | 0 web/src/api/storage/dasd.ts | 2 +- web/src/api/storage/devices.ts | 2 +- web/src/api/storage/iscsi.ts | 2 +- web/src/api/storage/{types => }/model.ts | 0 web/src/api/storage/proposal.ts | 1 - web/src/api/storage/system.ts | 1 - web/src/api/storage/types/checks.ts | 2 +- web/src/api/storage/types/openapi.ts | 438 ------------------ web/src/api/storage/zfcp.ts | 2 +- web/src/api/system.ts | 32 ++ web/src/api/users.ts | 2 +- .../core/InstallationFinished.test.tsx | 2 +- .../components/core/InstallationFinished.tsx | 9 +- .../components/core/InstallerOptions.test.tsx | 2 +- web/src/components/core/InstallerOptions.tsx | 8 +- .../l10n/KeyboardSelection.test.tsx | 2 +- web/src/components/l10n/KeyboardSelection.tsx | 7 +- web/src/components/l10n/L10nPage.test.tsx | 5 +- web/src/components/l10n/L10nPage.tsx | 3 +- .../components/l10n/LocaleSelection.test.tsx | 2 +- web/src/components/l10n/LocaleSelection.tsx | 7 +- .../l10n/TimezoneSelection.test.tsx | 2 +- web/src/components/l10n/TimezoneSelection.tsx | 9 +- .../components/overview/L10nSection.test.tsx | 2 +- web/src/components/overview/L10nSection.tsx | 5 +- .../components/overview/StorageSection.tsx | 13 +- .../questions/LuksActivationQuestion.test.tsx | 2 +- .../questions/QuestionWithPassword.test.tsx | 2 +- web/src/components/storage/BootSelection.tsx | 5 +- web/src/components/storage/ConfigEditor.tsx | 7 +- .../components/storage/ConfigEditorMenu.tsx | 9 +- .../storage/EncryptionSettingsPage.tsx | 4 +- .../storage/FormattableDevicePage.tsx | 19 +- .../components/storage/LogicalVolumePage.tsx | 20 +- web/src/components/storage/PartitionPage.tsx | 28 +- web/src/components/storage/PartitionsMenu.tsx | 2 +- .../components/storage/ProposalFailedInfo.tsx | 4 +- web/src/components/storage/ProposalPage.tsx | 25 +- .../storage/ProposalResultSection.tsx | 3 +- .../storage/ProposalTransactionalInfo.tsx | 4 +- .../storage/SpacePolicySelection.tsx | 6 +- .../storage/UnsupportedModelInfo.tsx | 4 +- web/src/components/storage/utils.ts | 3 +- web/src/context/installerL10n.tsx | 41 +- web/src/helpers/l10n.ts | 30 ++ web/src/helpers/storage/system.ts | 36 ++ web/src/hooks/api.ts | 146 ++++++ web/src/hooks/l10n.ts | 39 ++ web/src/hooks/storage/api-model.ts | 61 --- web/src/hooks/storage/boot.ts | 18 +- web/src/hooks/storage/config.ts | 43 ++ web/src/hooks/storage/drive.ts | 18 +- web/src/hooks/storage/filesystem.ts | 13 +- web/src/hooks/storage/logical-volume.ts | 18 +- web/src/hooks/storage/md-raid.ts | 18 +- web/src/hooks/storage/model.ts | 40 +- web/src/hooks/storage/partition.ts | 18 +- web/src/hooks/storage/product.ts | 60 --- web/src/hooks/storage/proposal.ts | 39 ++ web/src/hooks/storage/space-policy.ts | 8 +- web/src/hooks/storage/system.ts | 216 ++++++--- web/src/hooks/storage/volume-group.ts | 23 +- web/src/{api => }/http.ts | 0 web/src/queries/proposal.ts | 57 --- web/src/queries/storage.ts | 299 ------------ web/src/queries/storage/config-model.ts | 37 +- web/src/queries/storage/dasd.ts | 6 +- web/src/queries/system.ts | 95 ---- web/src/test-utils.tsx | 19 +- 88 files changed, 931 insertions(+), 1527 deletions(-) create mode 100644 web/src/api.ts rename web/src/api/{api.ts => action.ts} (56%) rename web/src/{types => api}/config.ts (90%) rename web/src/{types/system.ts => api/issue.ts} (88%) rename web/src/{types => api/l10n}/proposal.ts (91%) rename web/src/{types/l10n.ts => api/l10n/system.ts} (76%) create mode 100644 web/src/api/proposal.ts rename web/src/api/{storage/types.ts => question.ts} (83%) rename web/src/api/storage/{types => }/config.ts (100%) rename web/src/api/storage/{types => }/model.ts (100%) delete mode 100644 web/src/api/storage/types/openapi.ts create mode 100644 web/src/api/system.ts create mode 100644 web/src/helpers/l10n.ts create mode 100644 web/src/helpers/storage/system.ts create mode 100644 web/src/hooks/api.ts create mode 100644 web/src/hooks/l10n.ts delete mode 100644 web/src/hooks/storage/api-model.ts create mode 100644 web/src/hooks/storage/config.ts delete mode 100644 web/src/hooks/storage/product.ts create mode 100644 web/src/hooks/storage/proposal.ts rename web/src/{api => }/http.ts (100%) delete mode 100644 web/src/queries/proposal.ts delete mode 100644 web/src/queries/storage.ts delete mode 100644 web/src/queries/system.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index 258cf9883b..b6f4464d5a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -24,11 +24,9 @@ import React, { useEffect } from "react"; import { Navigate, Outlet, useLocation } from "react-router-dom"; import { Loading } from "~/components/layout"; import { useProduct, useProductChanges } from "~/queries/software"; -import { useProposalChanges } from "~/queries/proposal"; -import { useSystemChanges } from "~/queries/system"; +import { useSystemChanges, useProposalChanges } from "~/hooks/api"; import { useIssuesChanges } from "~/queries/issues"; import { useInstallerStatus, useInstallerStatusChanges } from "~/queries/status"; -import { useDeprecatedChanges } from "~/queries/storage"; import { ROOT, PRODUCT } from "~/routes/paths"; import { InstallationPhase } from "~/types/status"; import { useQueryClient } from "@tanstack/react-query"; @@ -43,7 +41,6 @@ function App() { useProductChanges(); useIssuesChanges(); useInstallerStatusChanges(); - useDeprecatedChanges(); const location = useLocation(); const { isBusy, phase } = useInstallerStatus({ suspense: true }); diff --git a/web/src/api.ts b/web/src/api.ts new file mode 100644 index 0000000000..7c07e5f05c --- /dev/null +++ b/web/src/api.ts @@ -0,0 +1,105 @@ +/* + * 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 { get, patch, post, put } from "~/http"; +import { apiModel } from "~/api/storage"; +import { Config } from "~/api/config"; +import { Issue } from "~/api/issue"; +import { Proposal } from "~/api/proposal"; +import { Question } from "~/api/question"; +import { Status } from "~/api/status"; +import { System } from "~/api/system"; +import { + Action, + L10nSystemConfig, + configureL10n, + activateStorage, + probeStorage, +} from "~/api/action"; +import { AxiosResponse } from "axios"; +import { Job } from "~/types/job"; + +type Response = Promise; + +const getStatus = (): Promise => get("/api/v2/status"); + +const getConfig = (): Promise => get("/api/v2/config"); + +const getExtendedConfig = (): Promise => get("/api/v2/extended_config"); + +const getSystem = (): Promise => get("/api/v2/system"); + +const getProposal = (): Promise => get("/api/v2/proposal"); + +const getIssues = (): Promise => get("/api/v2/issues"); + +const getQuestions = (): Promise => get("/api/v2/questions"); + +const getStorageModel = (): Promise => get("/api/v2/private/storage_model"); + +const solveStorageModel = (model: apiModel.Config): Promise => { + const json = encodeURIComponent(JSON.stringify(model)); + return get(`/api/v2/private/solve_storage_model?model=${json}`); +}; + +const putConfig = (config: Config): Response => put("/api/v2/config", config); + +const putStorageModel = (model: apiModel.Config) => put("/api/v2/private/storage_model", model); + +const patchConfig = (config: Config) => patch("/api/v2/config", { update: config }); + +const postAction = (action: Action) => post("/api/v2/action", action); + +const configureL10nAction = (config: L10nSystemConfig) => postAction(configureL10n(config)); + +const activateStorageAction = () => postAction(activateStorage()); + +const probeStorageAction = () => postAction(probeStorage()); + +/** + * @todo Adapt jobs to the new API. + */ +const getStorageJobs = (): Promise => get("/api/storage/jobs"); + +export { + getStatus, + getConfig, + getExtendedConfig, + getSystem, + getProposal, + getIssues, + getQuestions, + getStorageModel, + solveStorageModel, + putConfig, + putStorageModel, + patchConfig, + configureL10nAction, + activateStorageAction, + probeStorageAction, + getStorageJobs, +}; + +export type { Response, System, Config, Proposal }; +export * as system from "~/api/system"; +export * as config from "~/api/config"; +export * as proposal from "~/api/proposal"; diff --git a/web/src/api/api.ts b/web/src/api/action.ts similarity index 56% rename from web/src/api/api.ts rename to web/src/api/action.ts index 87911ea4ec..5b36cfb563 100644 --- a/web/src/api/api.ts +++ b/web/src/api/action.ts @@ -20,28 +20,30 @@ * find current contact information at www.suse.com. */ -import { get, patch, post } from "~/api/http"; -import { Config } from "~/types/config"; -import { Proposal } from "~/types/proposal"; -import { System } from "~/types/system"; +type Action = ConfigureL10n | ActivateStorage | ProbeStorage; -/** - * Returns the system config - */ -const fetchSystem = (): Promise => get("/api/v2/system"); +type ConfigureL10n = { + configureL10n: L10nSystemConfig; +}; -/** - * Returns the proposal - */ -const fetchProposal = (): Promise => get("/api/v2/proposal"); +type L10nSystemConfig = { + locale?: string; + keymap?: string; +}; -/** - * Updates configuration - */ -const updateConfig = (config: Config) => patch("/api/v2/config", { update: config }); -/** - * Triggers an action - */ -const trigger = (action) => post("/api/v2/action", action); +type ActivateStorage = { + activateStorage: null; +}; + +type ProbeStorage = { + probeStorage: null; +}; + +const configureL10n = (config: L10nSystemConfig): ConfigureL10n => ({ configureL10n: config }); + +const activateStorage = (): ActivateStorage => ({ activateStorage: null }); + +const probeStorage = (): ProbeStorage => ({ probeStorage: null }); -export { fetchSystem, fetchProposal, updateConfig, trigger }; +export { configureL10n, activateStorage, probeStorage }; +export type { Action, L10nSystemConfig }; diff --git a/web/src/types/config.ts b/web/src/api/config.ts similarity index 90% rename from web/src/types/config.ts rename to web/src/api/config.ts index f7248c72cf..fc050d8899 100644 --- a/web/src/types/config.ts +++ b/web/src/api/config.ts @@ -20,10 +20,11 @@ * find current contact information at www.suse.com. */ -import { Localization } from "./l10n"; +import * as storage from "~/api/storage/config"; type Config = { - l10n?: Localization; + storage?: storage.Config; }; +export { storage }; export type { Config }; diff --git a/web/src/api/hostname.ts b/web/src/api/hostname.ts index dfac34b69e..902b160359 100644 --- a/web/src/api/hostname.ts +++ b/web/src/api/hostname.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get, put } from "~/api/http"; +import { get, put } from "~/http"; import { Hostname } from "~/types/hostname"; /** diff --git a/web/src/types/system.ts b/web/src/api/issue.ts similarity index 88% rename from web/src/types/system.ts rename to web/src/api/issue.ts index 60fb1f35c1..41d5691f46 100644 --- a/web/src/types/system.ts +++ b/web/src/api/issue.ts @@ -20,10 +20,6 @@ * find current contact information at www.suse.com. */ -import { Localization } from "./l10n"; +type Issue = object; -type System = { - l10n?: Localization; -}; - -export type { System }; +export type { Issue }; diff --git a/web/src/api/issues.ts b/web/src/api/issues.ts index fee355836f..888febd137 100644 --- a/web/src/api/issues.ts +++ b/web/src/api/issues.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get } from "~/api/http"; +import { get } from "~/http"; import { Issue, IssuesMap, IssuesScope } from "~/types/issues"; /** diff --git a/web/src/types/proposal.ts b/web/src/api/l10n/proposal.ts similarity index 91% rename from web/src/types/proposal.ts rename to web/src/api/l10n/proposal.ts index 1eacf176f9..e7ab01974f 100644 --- a/web/src/types/proposal.ts +++ b/web/src/api/l10n/proposal.ts @@ -20,10 +20,6 @@ * find current contact information at www.suse.com. */ -import { Localization } from "./l10n"; - -type Proposal = { - l10n?: Localization; -}; +type Proposal = object; export type { Proposal }; diff --git a/web/src/types/l10n.ts b/web/src/api/l10n/system.ts similarity index 76% rename from web/src/types/l10n.ts rename to web/src/api/l10n/system.ts index fbc9bc4c24..98cfde5a4b 100644 --- a/web/src/types/l10n.ts +++ b/web/src/api/l10n/system.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2025] SUSE LLC * * All Rights Reserved. * @@ -65,7 +65,7 @@ type Timezone = { utcOffset: number; }; -type Localization = { +type System = { locales?: Locale[]; keymaps?: Keymap[]; timezones?: Timezone[]; @@ -74,23 +74,4 @@ type Localization = { timezone?: string; }; -type LocaleConfig = { - /** - * Selected locale for installation (e.g, "en_US.UTF-8") - */ - locale?: string; - /** - * List of locales to install (e.g., ["en_US.UTF-8"]). - */ - locales?: string[]; - /** - * Selected keymap for installation (e.g., "en"). - */ - keymap?: string; - /** - * Selected timezone for installation (e.g., "Atlantic/Canary"). - */ - timezone?: string; -}; - -export type { Keymap, Locale, Timezone, LocaleConfig, Localization }; +export type { System, Keymap, Locale, Timezone }; diff --git a/web/src/api/manager.ts b/web/src/api/manager.ts index d02d1f6d83..61653c8408 100644 --- a/web/src/api/manager.ts +++ b/web/src/api/manager.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get, post } from "~/api/http"; +import { get, post } from "~/http"; /** * Starts the probing process. diff --git a/web/src/api/network.ts b/web/src/api/network.ts index ec919ca3d9..a6fd2b9fe1 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { del, get, post, put } from "~/api/http"; +import { del, get, post, put } from "~/http"; import { APIAccessPoint, APIConnection, APIDevice, NetworkGeneralState } from "~/types/network"; /** diff --git a/web/src/api/progress.ts b/web/src/api/progress.ts index e94538a6fe..d62095f0e4 100644 --- a/web/src/api/progress.ts +++ b/web/src/api/progress.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get } from "~/api/http"; +import { get } from "~/http"; import { APIProgress, Progress } from "~/types/progress"; /** diff --git a/web/src/api/proposal.ts b/web/src/api/proposal.ts new file mode 100644 index 0000000000..9e7fd99496 --- /dev/null +++ b/web/src/api/proposal.ts @@ -0,0 +1,32 @@ +/* + * 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 * as l10n from "~/api/l10n/proposal"; +import * as storage from "~/api/storage/proposal"; + +type Proposal = { + l10n?: l10n.Proposal; + storage?: storage.Proposal; +}; + +export { l10n, storage }; +export type { Proposal }; diff --git a/web/src/api/storage/types.ts b/web/src/api/question.ts similarity index 83% rename from web/src/api/storage/types.ts rename to web/src/api/question.ts index 09ad88d304..57db1df22b 100644 --- a/web/src/api/storage/types.ts +++ b/web/src/api/question.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024-2025] SUSE LLC + * Copyright (c) [2025] SUSE LLC * * All Rights Reserved. * @@ -20,6 +20,6 @@ * find current contact information at www.suse.com. */ -export * from "./types/openapi"; -export * as config from "./types/config"; -export * as apiModel from "./types/model"; +type Question = object; + +export type { Question }; diff --git a/web/src/api/questions.ts b/web/src/api/questions.ts index 80680a3bf7..4b78e0232e 100644 --- a/web/src/api/questions.ts +++ b/web/src/api/questions.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get, patch } from "~/api/http"; +import { get, patch } from "~/http"; import { Question } from "~/types/questions"; /** diff --git a/web/src/api/software.ts b/web/src/api/software.ts index ca9efc52dc..0f6210fed6 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -34,7 +34,7 @@ import { SoftwareConfig, SoftwareProposal, } from "~/types/software"; -import { get, patch, post, put } from "~/api/http"; +import { get, patch, post, put } from "~/http"; /** * Returns the software configuration diff --git a/web/src/api/status.ts b/web/src/api/status.ts index dd4931b481..f7d0f7c65e 100644 --- a/web/src/api/status.ts +++ b/web/src/api/status.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get } from "~/api/http"; +import { get } from "~/http"; import { InstallerStatus } from "~/types/status"; /** @@ -31,4 +31,9 @@ const fetchInstallerStatus = async (): Promise => { return { phase, isBusy, useIguana, canInstall }; }; +// TODO: remove export { fetchInstallerStatus }; + +type Status = object; + +export type { Status }; diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts index 76bc8cd2d7..dccba2ae09 100644 --- a/web/src/api/storage.ts +++ b/web/src/api/storage.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024-2025] SUSE LLC + * Copyright (c) [2025] SUSE LLC * * All Rights Reserved. * @@ -20,90 +20,6 @@ * find current contact information at www.suse.com. */ -import { get, post, put } from "~/api/http"; -import { Job } from "~/types/job"; -import { Action, config, apiModel, ProductParams, Volume } from "~/api/storage/types"; - -/** - * Starts the storage probing process. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const probe = (): Promise => post("/api/storage/probe"); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const reprobe = (): Promise => post("/api/storage/reprobe"); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const reactivate = (): Promise => post("/api/storage/reactivate"); - -const fetchConfig = (): Promise => - get("/api/storage/config").then((config) => config.storage ?? null); - -const fetchConfigModel = (): Promise => - get("/api/storage/config_model"); - -const setConfig = (config: config.Config) => put("/api/storage/config", { storage: config }); - -const resetConfig = () => put("/api/storage/config/reset", {}); - -const setConfigModel = (model: apiModel.Config) => put("/api/storage/config_model", model); - -const solveConfigModel = (model: apiModel.Config): Promise => { - const serializedModel = encodeURIComponent(JSON.stringify(model)); - return get(`/api/storage/config_model/solve?model=${serializedModel}`); -}; - -const fetchAvailableDrives = (): Promise => get(`/api/storage/devices/available_drives`); - -const fetchCandidateDrives = (): Promise => get(`/api/storage/devices/candidate_drives`); - -const fetchAvailableMdRaids = (): Promise => - get(`/api/storage/devices/available_md_raids`); - -const fetchCandidateMdRaids = (): Promise => - get(`/api/storage/devices/candidate_md_raids`); - -const fetchProductParams = (): Promise => get("/api/storage/product/params"); - -const fetchVolume = (mountPath: string): Promise => { - const path = encodeURIComponent(mountPath); - return get(`/api/storage/product/volume_for?mount_path=${path}`); -}; - -const fetchVolumes = (mountPaths: string[]): Promise => - Promise.all(mountPaths.map(fetchVolume)); - -const fetchActions = (): Promise => get("/api/storage/devices/actions"); - -/** - * Returns the list of jobs - */ -const fetchStorageJobs = (): Promise => get("/api/storage/jobs"); - -/** - * Returns the job with given id or undefined - */ -const findStorageJob = (id: string): Promise => - fetchStorageJobs().then((jobs: Job[]) => jobs.find((value) => value.id === id)); - -export { - probe, - reprobe, - reactivate, - fetchConfig, - fetchConfigModel, - setConfig, - resetConfig, - setConfigModel, - solveConfigModel, - fetchAvailableDrives, - fetchCandidateDrives, - fetchAvailableMdRaids, - fetchCandidateMdRaids, - fetchProductParams, - fetchVolume, - fetchVolumes, - fetchActions, - fetchStorageJobs, - findStorageJob, -}; +export * as config from "~/api/storage/config"; +export * as apiModel from "~/api/storage/model"; +export * as system from "~/api/storage/system"; diff --git a/web/src/api/storage/types/config.ts b/web/src/api/storage/config.ts similarity index 100% rename from web/src/api/storage/types/config.ts rename to web/src/api/storage/config.ts diff --git a/web/src/api/storage/dasd.ts b/web/src/api/storage/dasd.ts index b3962d26ec..26c5767a1e 100644 --- a/web/src/api/storage/dasd.ts +++ b/web/src/api/storage/dasd.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { post, get, put } from "~/api/http"; +import { post, get, put } from "~/http"; import { DASDDevice } from "~/types/dasd"; /** diff --git a/web/src/api/storage/devices.ts b/web/src/api/storage/devices.ts index ea1fbe4fe1..b6a6d5fd63 100644 --- a/web/src/api/storage/devices.ts +++ b/web/src/api/storage/devices.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { get } from "~/api/http"; +import { get } from "~/http"; import { Component, Device, diff --git a/web/src/api/storage/iscsi.ts b/web/src/api/storage/iscsi.ts index fb808a4187..6c10d4dfc1 100644 --- a/web/src/api/storage/iscsi.ts +++ b/web/src/api/storage/iscsi.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { del, get, patch, post } from "~/api/http"; +import { del, get, patch, post } from "~/http"; import { ISCSIInitiator, ISCSINode } from "~/api/storage/types"; const ISCSI_NODES_NAMESPACE = "/api/storage/iscsi/nodes"; diff --git a/web/src/api/storage/types/model.ts b/web/src/api/storage/model.ts similarity index 100% rename from web/src/api/storage/types/model.ts rename to web/src/api/storage/model.ts diff --git a/web/src/api/storage/proposal.ts b/web/src/api/storage/proposal.ts index 0973d1931d..423e6b8bbc 100644 --- a/web/src/api/storage/proposal.ts +++ b/web/src/api/storage/proposal.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, diff --git a/web/src/api/storage/system.ts b/web/src/api/storage/system.ts index 88078a978e..d71d396564 100644 --- a/web/src/api/storage/system.ts +++ b/web/src/api/storage/system.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, diff --git a/web/src/api/storage/types/checks.ts b/web/src/api/storage/types/checks.ts index 0b373aa1e3..cf3dab7393 100644 --- a/web/src/api/storage/types/checks.ts +++ b/web/src/api/storage/types/checks.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import * as config from "./config"; +import * as config from "../config"; // Type guards. diff --git a/web/src/api/storage/types/openapi.ts b/web/src/api/storage/types/openapi.ts deleted file mode 100644 index aee104ea76..0000000000 --- a/web/src/api/storage/types/openapi.ts +++ /dev/null @@ -1,438 +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. - */ - -// This file is auto-generated by @hey-api/openapi-ts - -/** - * Represents a single change action done to storage - */ -export type Action = { - delete: boolean; - device: DeviceSid; - resize: boolean; - subvol: boolean; - text: string; -}; - -export type BlockDevice = { - active: boolean; - encrypted: boolean; - shrinking: ShrinkingInfo; - size: DeviceSize; - start: number; - systems: Array; - udevIds: Array; - udevPaths: Array; -}; - -export type Component = { - deviceNames: Array; - devices: Array; - type: string; -}; - -/** - * Information about system device created by composition to reflect different devices on system - */ -export type Device = { - blockDevice?: BlockDevice | null; - component?: Component | null; - deviceInfo: DeviceInfo; - drive?: Drive | null; - filesystem?: Filesystem | null; - lvmLv?: LvmLv | null; - lvmVg?: LvmVg | null; - md?: Md | null; - multipath?: Multipath | null; - partition?: Partition | null; - partitionTable?: PartitionTable | null; - raid?: Raid | null; -}; - -export type DeviceInfo = { - description: string; - name: string; - sid: DeviceSid; -}; - -export type DeviceSid = number; - -export type DeviceSize = number; - -export type DiscoverParams = { - /** - * iSCSI server address. - */ - address: string; - options?: ISCSIAuth; - /** - * iSCSI service port. - */ - port: number; -}; - -export type Drive = { - bus: string; - busId: string; - driver: Array; - info: DriveInfo; - model: string; - transport: string; - type: string; - vendor: string; -}; - -export type DriveInfo = { - dellBOSS: boolean; - sdCard: boolean; -}; - -export type Filesystem = { - label: string; - mountPath: string; - sid: DeviceSid; - type: string; -}; - -export type ISCSIAuth = { - /** - * Password for authentication by target. - */ - password?: string | null; - /** - * Password for authentication by initiator. - */ - reverse_password?: string | null; - /** - * Username for authentication by initiator. - */ - reverse_username?: string | null; - /** - * Username for authentication by target. - */ - username?: string | null; -}; - -export type ISCSIInitiator = { - ibft: boolean; - name: string; -}; - -/** - * ISCSI node - */ -export type ISCSINode = { - /** - * Target IP address (in string-like form). - */ - address: string; - /** - * Whether the node is connected (there is a session). - */ - connected: boolean; - /** - * Whether the node was initiated by iBFT - */ - ibft: boolean; - /** - * Artificial ID to match it against the D-Bus backend. - */ - id: number; - /** - * Interface name. - */ - interface: string; - /** - * Target port. - */ - port: number; - /** - * Startup status (TODO: document better) - */ - startup: string; - /** - * Target name. - */ - target: string; -}; - -export type InitiatorParams = { - /** - * iSCSI initiator name. - */ - name: string; -}; - -export type LoginParams = ISCSIAuth & { - /** - * Startup value. - */ - startup: string; -}; - -export type LoginResult = "Success" | "InvalidStartup" | "Failed"; - -export type LvmLv = { - volumeGroup: DeviceSid; -}; - -export type LvmVg = { - logicalVolumes: Array; - physicalVolumes: Array; - size: DeviceSize; -}; - -export type Md = { - devices: Array; - level: string; - uuid: string; -}; - -export type Multipath = { - wires: Array; -}; - -export type NodeParams = { - /** - * Startup value. - */ - startup: string; -}; - -export type Partition = { - device: DeviceSid; - efi: boolean; -}; - -export type PartitionTable = { - partitions: Array; - type: string; - unusedSlots: Array; -}; - -export type PingResponse = { - /** - * API status - */ - status: string; -}; - -export type ProductParams = { - /** - * Encryption methods allowed by the product. - */ - encryptionMethods: Array; - /** - * Mount points defined by the product. - */ - mountPoints: Array; -}; - -/** - * Represents a proposal configuration - */ -export type ProposalSettings = { - bootDevice: string; - configureBoot: boolean; - defaultBootDevice: string; - encryptionMethod: string; - encryptionPBKDFunction: string; - encryptionPassword: string; - spaceActions: Array; - spacePolicy: string; - target: ProposalTarget; - targetDevice?: string | null; - targetPVDevices?: Array | null; - volumes: Array; -}; - -/** - * Represents a proposal patch -> change of proposal configuration that can be partial - */ -export type ProposalSettingsPatch = { - bootDevice?: string | null; - configureBoot?: boolean | null; - encryptionMethod?: string | null; - encryptionPBKDFunction?: string | null; - encryptionPassword?: string | null; - spaceActions?: Array | null; - spacePolicy?: string | null; - target?: ProposalTarget | null; - targetDevice?: string | null; - targetPVDevices?: Array | null; - volumes?: Array | null; -}; - -export type ProposalTarget = "disk" | "newLvmVg" | "reusedLvmVg"; - -export type Raid = { - devices: Array; -}; - -export type ShrinkingInfo = - | { - supported: DeviceSize; - } - | { - unsupported: Array; - }; - -export type SpaceAction = "force_delete" | "resize" | "keep"; - -export type SpaceActionSettings = { - action: SpaceAction; - device: string; -}; - -export type UnusedSlot = { - size: DeviceSize; - start: number; -}; - -/** - * Represents a single volume - */ -export type Volume = { - autoSize: boolean; - fsType: string; - maxSize?: DeviceSize | null; - minSize?: DeviceSize | null; - mountOptions: Array; - mountPath: string; - outline?: VolumeOutline | null; - snapshots: boolean; - target: VolumeTarget; - targetDevice?: string | null; - transactional?: boolean | null; -}; - -/** - * Represents volume outline aka requirements for volume - */ -export type VolumeOutline = { - adjustByRam: boolean; - fsTypes: Array; - /** - * whether it is required - */ - required: boolean; - sizeRelevantVolumes: Array; - snapshotsAffectSizes: boolean; - snapshotsConfigurable: boolean; - supportAutoSize: boolean; -}; - -/** - * Represents value for target key of Volume - * It is snake cased when serializing to be compatible with yast2-storage-ng. - */ -export type VolumeTarget = "default" | "new_partition" | "new_vg" | "device" | "filesystem"; - -export type DevicesDirtyResponse = boolean; - -export type StagingDevicesResponse = Array; - -export type SystemDevicesResponse = Array; - -export type DiscoverData = { - requestBody: DiscoverParams; -}; - -export type DiscoverResponse = void; - -export type InitiatorResponse = ISCSIInitiator; - -export type UpdateInitiatorData = { - requestBody: InitiatorParams; -}; - -export type UpdateInitiatorResponse = void; - -export type NodesResponse = Array; - -export type UpdateNodeData = { - /** - * iSCSI artificial ID. - */ - id: number; - requestBody: NodeParams; -}; - -export type UpdateNodeResponse = NodeParams; - -export type DeleteNodeData = { - /** - * iSCSI artificial ID. - */ - id: number; -}; - -export type DeleteNodeResponse = void; - -export type LoginNodeData = { - /** - * iSCSI artificial ID. - */ - id: number; - requestBody: LoginParams; -}; - -export type LoginNodeResponse = void; - -export type LogoutNodeData = { - /** - * iSCSI artificial ID. - */ - id: number; -}; - -export type LogoutNodeResponse = void; - -export type StorageProbeResponse = unknown; - -export type ProductParamsResponse = ProductParams; - -export type VolumeForData = { - /** - * Mount path of the volume (empty for an arbitrary volume). - */ - mountPath: string; -}; - -export type VolumeForResponse = Volume; - -export type ActionsResponse = Array; - -export type GetProposalSettingsResponse = ProposalSettings; - -export type SetProposalSettingsData = { - /** - * Proposal settings - */ - requestBody: ProposalSettingsPatch; -}; - -export type SetProposalSettingsResponse = boolean; - -export type UsableDevicesResponse = Array; - -export type PingResponse2 = PingResponse; diff --git a/web/src/api/storage/zfcp.ts b/web/src/api/storage/zfcp.ts index 0d04169d5e..72dccb0719 100644 --- a/web/src/api/storage/zfcp.ts +++ b/web/src/api/storage/zfcp.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { post, get } from "~/api/http"; +import { post, get } from "~/http"; import { ZFCPDisk, ZFCPController, ZFCPConfig } from "~/types/zfcp"; /** diff --git a/web/src/api/system.ts b/web/src/api/system.ts new file mode 100644 index 0000000000..a7325b498b --- /dev/null +++ b/web/src/api/system.ts @@ -0,0 +1,32 @@ +/* + * 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 * as l10n from "~/api/l10n/system"; +import * as storage from "~/api/storage/system"; + +type System = { + l10n?: l10n.System; + storage?: storage.System; +}; + +export { l10n, storage }; +export type { System }; diff --git a/web/src/api/users.ts b/web/src/api/users.ts index 25af9b2ed0..ae8db6e116 100644 --- a/web/src/api/users.ts +++ b/web/src/api/users.ts @@ -21,7 +21,7 @@ */ import { AxiosResponse } from "axios"; -import { del, get, patch, post, put } from "~/api/http"; +import { del, get, patch, post, put } from "~/http"; import { FirstUser, PasswordCheckResult, RootUser } from "~/types/users"; /** diff --git a/web/src/components/core/InstallationFinished.test.tsx b/web/src/components/core/InstallationFinished.test.tsx index 91432a6ed8..eaa4bfec1b 100644 --- a/web/src/components/core/InstallationFinished.test.tsx +++ b/web/src/components/core/InstallationFinished.test.tsx @@ -25,7 +25,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import InstallationFinished from "./InstallationFinished"; -import { Encryption } from "~/api/storage/types/config"; +import { Encryption } from "~/api/storage/config"; jest.mock("~/queries/status", () => ({ ...jest.requireActual("~/queries/status"), diff --git a/web/src/components/core/InstallationFinished.tsx b/web/src/components/core/InstallationFinished.tsx index 619f4e5eed..72ce092501 100644 --- a/web/src/components/core/InstallationFinished.tsx +++ b/web/src/components/core/InstallationFinished.tsx @@ -41,7 +41,7 @@ import { Navigate, useNavigate } from "react-router-dom"; import { Icon } from "~/components/layout"; import alignmentStyles from "@patternfly/react-styles/css/utilities/Alignment/alignment"; import { useInstallerStatus } from "~/queries/status"; -import { useConfig } from "~/queries/storage"; +import { useExtendedConfig } from "~/hooks/api"; import { finishInstallation } from "~/api/manager"; import { InstallationPhase } from "~/types/status"; import { ROOT as PATHS } from "~/routes/paths"; @@ -83,11 +83,8 @@ function usingTpm(config): boolean { return null; } - const { guided, drives = [], volumeGroups = [] } = config; + const { drives = [], volumeGroups = [] } = config; - if (guided !== undefined) { - return guided.encryption?.method === "tpm_fde"; - } const devices = [ ...drives, ...drives.flatMap((d) => d.partitions || []), @@ -100,7 +97,7 @@ function usingTpm(config): boolean { } function InstallationFinished() { - const config = useConfig(); + const config = useExtendedConfig(); const { phase, useIguana } = useInstallerStatus({ suspense: true }); const navigate = useNavigate(); diff --git a/web/src/components/core/InstallerOptions.test.tsx b/web/src/components/core/InstallerOptions.test.tsx index 335df6a12d..7347fb0b82 100644 --- a/web/src/components/core/InstallerOptions.test.tsx +++ b/web/src/components/core/InstallerOptions.test.tsx @@ -28,7 +28,7 @@ import * as utils from "~/utils"; import { PRODUCT, ROOT } from "~/routes/paths"; import InstallerOptions, { InstallerOptionsProps } from "./InstallerOptions"; import { Product } from "~/types/software"; -import { Keymap, Locale } from "~/types/l10n"; +import { Keymap, Locale } from "~/api/system"; let phase: InstallationPhase; let isBusy: boolean; diff --git a/web/src/components/core/InstallerOptions.tsx b/web/src/components/core/InstallerOptions.tsx index 3b97d0c31c..34b680bcc2 100644 --- a/web/src/components/core/InstallerOptions.tsx +++ b/web/src/components/core/InstallerOptions.tsx @@ -47,7 +47,7 @@ import { } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { Icon } from "~/components/layout"; -import { Keymap, Locale } from "~/types/l10n"; +import { Keymap, Locale } from "~/api/l10n/system"; import { InstallationPhase } from "~/types/status"; import { useInstallerL10n } from "~/context/installerL10n"; import { useInstallerStatus } from "~/queries/status"; @@ -56,8 +56,8 @@ import { _ } from "~/i18n"; import supportedLanguages from "~/languages.json"; import { PRODUCT, ROOT, L10N } from "~/routes/paths"; import { useProduct } from "~/queries/software"; -import { useSystem } from "~/queries/system"; -import { updateConfig } from "~/api/api"; +import { useSystem } from "~/hooks/api"; +import { patchConfig } from "~/api"; /** * Props for select inputs @@ -593,7 +593,7 @@ export default function InstallerOptions({ if (variant !== "keyboard") systemL10n.locale = systemLocale?.id; if (variant !== "language" && localConnection()) systemL10n.keymap = formState.keymap; - updateConfig({ l10n: systemL10n }); + patchConfig({ l10n: systemL10n }); }; const close = () => { diff --git a/web/src/components/l10n/KeyboardSelection.test.tsx b/web/src/components/l10n/KeyboardSelection.test.tsx index 3c5ec93c03..9cda3ecb94 100644 --- a/web/src/components/l10n/KeyboardSelection.test.tsx +++ b/web/src/components/l10n/KeyboardSelection.test.tsx @@ -25,7 +25,7 @@ import KeyboardSelection from "./KeyboardSelection"; import userEvent from "@testing-library/user-event"; import { screen } from "@testing-library/react"; import { mockNavigateFn, installerRender } from "~/test-utils"; -import { Keymap } from "~/types/l10n"; +import { Keymap } from "~/api/system"; const keymaps: Keymap[] = [ { id: "us", name: "English" }, diff --git a/web/src/components/l10n/KeyboardSelection.tsx b/web/src/components/l10n/KeyboardSelection.tsx index 6b34273243..0dc4020ec5 100644 --- a/web/src/components/l10n/KeyboardSelection.tsx +++ b/web/src/components/l10n/KeyboardSelection.tsx @@ -24,9 +24,8 @@ import React, { useState } from "react"; import { Content, Flex, Form, FormGroup, Radio } from "@patternfly/react-core"; import { useNavigate } from "react-router-dom"; import { ListSearch, Page } from "~/components/core"; -import { updateConfig } from "~/api/api"; -import { useSystem } from "~/queries/system"; -import { useProposal } from "~/queries/proposal"; +import { patchConfig } from "~/api"; +import { useSystem, useProposal } from "~/hooks/api"; import { _ } from "~/i18n"; // TODO: Add documentation @@ -51,7 +50,7 @@ export default function KeyboardSelection() { const onSubmit = async (e: React.SyntheticEvent) => { e.preventDefault(); // FIXME: udpate when new API is ready - updateConfig({ l10n: { keymap: selected } }); + patchConfig({ l10n: { keymap: selected } }); navigate(-1); }; diff --git a/web/src/components/l10n/L10nPage.test.tsx b/web/src/components/l10n/L10nPage.test.tsx index a4d25c230f..173c50277c 100644 --- a/web/src/components/l10n/L10nPage.test.tsx +++ b/web/src/components/l10n/L10nPage.test.tsx @@ -24,9 +24,8 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import L10nPage from "~/components/l10n/L10nPage"; -import { Keymap, Locale, Timezone } from "~/types/l10n"; -import { System } from "~/types/system"; -import { Proposal } from "~/types/proposal"; +import { System, Keymap, Locale, Timezone } from "~/api/system"; +import { Proposal } from "~/api/proposal"; let mockSystemData: System; let mockProposedData: Proposal; diff --git a/web/src/components/l10n/L10nPage.tsx b/web/src/components/l10n/L10nPage.tsx index 534b4a0911..2d17e18330 100644 --- a/web/src/components/l10n/L10nPage.tsx +++ b/web/src/components/l10n/L10nPage.tsx @@ -25,8 +25,7 @@ import { Button, Content, Grid, GridItem } from "@patternfly/react-core"; import { InstallerOptions, Link, Page } from "~/components/core"; import { L10N as PATHS } from "~/routes/paths"; import { localConnection } from "~/utils"; -import { useProposal } from "~/queries/proposal"; -import { useSystem } from "~/queries/system"; +import { useSystem, useProposal } from "~/hooks/api"; import { _ } from "~/i18n"; const InstallerL10nSettingsInfo = () => { diff --git a/web/src/components/l10n/LocaleSelection.test.tsx b/web/src/components/l10n/LocaleSelection.test.tsx index 0bf485e541..5b917b56c8 100644 --- a/web/src/components/l10n/LocaleSelection.test.tsx +++ b/web/src/components/l10n/LocaleSelection.test.tsx @@ -25,7 +25,7 @@ import LocaleSelection from "./LocaleSelection"; import userEvent from "@testing-library/user-event"; import { screen } from "@testing-library/react"; import { mockNavigateFn, installerRender } from "~/test-utils"; -import { Locale } from "~/types/l10n"; +import { Locale } from "~/api/system"; const locales: Locale[] = [ { id: "en_US.UTF-8", name: "English", territory: "United States" }, diff --git a/web/src/components/l10n/LocaleSelection.tsx b/web/src/components/l10n/LocaleSelection.tsx index 3243a83969..14088dea83 100644 --- a/web/src/components/l10n/LocaleSelection.tsx +++ b/web/src/components/l10n/LocaleSelection.tsx @@ -24,9 +24,8 @@ import React, { useState } from "react"; import { Content, Flex, Form, FormGroup, Radio } from "@patternfly/react-core"; import { useNavigate } from "react-router-dom"; import { ListSearch, Page } from "~/components/core"; -import { updateConfig } from "~/api/api"; -import { useSystem } from "~/queries/system"; -import { useProposal } from "~/queries/proposal"; +import { patchConfig } from "~/api"; +import { useSystem, useProposal } from "~/hooks/api"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { _ } from "~/i18n"; @@ -47,7 +46,7 @@ export default function LocaleSelection() { const onSubmit = async (e: React.SyntheticEvent) => { e.preventDefault(); - updateConfig({ l10n: { locale: selected } }); + patchConfig({ l10n: { locale: selected } }); navigate(-1); }; diff --git a/web/src/components/l10n/TimezoneSelection.test.tsx b/web/src/components/l10n/TimezoneSelection.test.tsx index 72cfb9b57c..6603881a9f 100644 --- a/web/src/components/l10n/TimezoneSelection.test.tsx +++ b/web/src/components/l10n/TimezoneSelection.test.tsx @@ -25,7 +25,7 @@ import TimezoneSelection from "./TimezoneSelection"; import userEvent from "@testing-library/user-event"; import { screen } from "@testing-library/react"; import { mockNavigateFn, installerRender } from "~/test-utils"; -import { Timezone } from "~/types/l10n"; +import { Timezone } from "~/api/system"; jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
ProductRegistrationAlert Mock
diff --git a/web/src/components/l10n/TimezoneSelection.tsx b/web/src/components/l10n/TimezoneSelection.tsx index 5656a8e795..eec0954aa1 100644 --- a/web/src/components/l10n/TimezoneSelection.tsx +++ b/web/src/components/l10n/TimezoneSelection.tsx @@ -24,10 +24,9 @@ import React, { useState } from "react"; import { Content, Flex, Form, FormGroup, Radio } from "@patternfly/react-core"; import { useNavigate } from "react-router-dom"; import { ListSearch, Page } from "~/components/core"; -import { Timezone } from "~/types/l10n"; -import { updateConfig } from "~/api/api"; -import { useSystem } from "~/queries/system"; -import { useProposal } from "~/queries/proposal"; +import { Timezone } from "~/api/l10n/system"; +import { patchConfig } from "~/api"; +import { useSystem, useProposal } from "~/hooks/api"; import { timezoneTime } from "~/utils"; import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { _ } from "~/i18n"; @@ -83,7 +82,7 @@ export default function TimezoneSelection() { const onSubmit = async (e: React.SyntheticEvent) => { e.preventDefault(); - updateConfig({ l10n: { timezone: selected } }); + patchConfig({ l10n: { timezone: selected } }); navigate(-1); }; diff --git a/web/src/components/overview/L10nSection.test.tsx b/web/src/components/overview/L10nSection.test.tsx index d0e9e040ae..20e542368e 100644 --- a/web/src/components/overview/L10nSection.test.tsx +++ b/web/src/components/overview/L10nSection.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { L10nSection } from "~/components/overview"; -import { Locale } from "~/types/l10n"; +import { Locale } from "~/api/system"; const locales: Locale[] = [ { id: "en_US.UTF-8", name: "English", territory: "United States" }, diff --git a/web/src/components/overview/L10nSection.tsx b/web/src/components/overview/L10nSection.tsx index 9fe2a48c69..806a0ab80c 100644 --- a/web/src/components/overview/L10nSection.tsx +++ b/web/src/components/overview/L10nSection.tsx @@ -22,10 +22,9 @@ import React from "react"; import { Content } from "@patternfly/react-core"; -import { useProposal } from "~/queries/proposal"; -import { useSystem } from "~/queries/system"; +import { useSystem, useProposal } from "~/hooks/api"; import { _ } from "~/i18n"; -import { Locale } from "~/types/l10n"; +import { Locale } from "~/api/l10n/system"; export default function L10nSection() { const { l10n: l10nProposal } = useProposal(); diff --git a/web/src/components/overview/StorageSection.tsx b/web/src/components/overview/StorageSection.tsx index 919d4fe24d..28cf116d59 100644 --- a/web/src/components/overview/StorageSection.tsx +++ b/web/src/components/overview/StorageSection.tsx @@ -23,21 +23,20 @@ import React from "react"; import { Content } from "@patternfly/react-core"; import { deviceLabel } from "~/components/storage/utils"; -import { useDevices } from "~/queries/storage"; -import { useAvailableDevices } from "~/hooks/storage/system"; +import { useAvailableDevices, useDevices } from "~/hooks/storage/system"; import { useConfigModel } from "~/queries/storage/config-model"; import { useSystemErrors } from "~/queries/issues"; -import { StorageDevice } from "~/types/storage"; -import { apiModel } from "~/api/storage/types"; +import { storage } from "~/api/system"; +import { apiModel } from "~/api/storage"; import { _ } from "~/i18n"; -const findDriveDevice = (drive: apiModel.Drive, devices: StorageDevice[]) => +const findDriveDevice = (drive: apiModel.Drive, devices: storage.Device[]) => devices.find((d) => d.name === drive.name); const NoDeviceSummary = () => _("No device selected yet"); const SingleDiskSummary = ({ drive }: { drive: apiModel.Drive }) => { - const devices = useDevices("system", { suspense: true }); + const devices = useDevices({ suspense: true }); const device = findDriveDevice(drive, devices); const options = { // TRANSLATORS: %s will be replaced by the device name and its size, @@ -81,7 +80,7 @@ const MultipleDisksSummary = ({ drives }: { drives: apiModel.Drive[] }): string }; const ModelSummary = ({ model }: { model: apiModel.Config }): React.ReactNode => { - const devices = useDevices("system", { suspense: true }); + const devices = useDevices({ suspense: true }); const drives = model?.drives || []; const existDevice = (name: string) => devices.some((d) => d.name === name); const noDrive = drives.length === 0 || drives.some((d) => !existDevice(d.name)); diff --git a/web/src/components/questions/LuksActivationQuestion.test.tsx b/web/src/components/questions/LuksActivationQuestion.test.tsx index fe65d32c72..496a235a4d 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.tsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -27,7 +27,7 @@ import { AnswerCallback, Question, FieldType } from "~/types/questions"; import { InstallationPhase } from "~/types/status"; import { Product } from "~/types/software"; import LuksActivationQuestion from "~/components/questions/LuksActivationQuestion"; -import { Locale, Keymap } from "~/types/l10n"; +import { Locale, Keymap } from "~/api/system"; let question: Question; const questionMock: Question = { diff --git a/web/src/components/questions/QuestionWithPassword.test.tsx b/web/src/components/questions/QuestionWithPassword.test.tsx index 60a2961f83..2e1bfc9cdc 100644 --- a/web/src/components/questions/QuestionWithPassword.test.tsx +++ b/web/src/components/questions/QuestionWithPassword.test.tsx @@ -27,7 +27,7 @@ import { Question, FieldType } from "~/types/questions"; import { Product } from "~/types/software"; import { InstallationPhase } from "~/types/status"; import QuestionWithPassword from "~/components/questions/QuestionWithPassword"; -import { Locale, Keymap } from "~/types/l10n"; +import { Locale, Keymap } from "~/api/system"; const answerFn = jest.fn(); const question: Question = { diff --git a/web/src/components/storage/BootSelection.tsx b/web/src/components/storage/BootSelection.tsx index d167d584b4..f6201c7dba 100644 --- a/web/src/components/storage/BootSelection.tsx +++ b/web/src/components/storage/BootSelection.tsx @@ -27,11 +27,10 @@ import { DevicesFormSelect } from "~/components/storage"; import { Page, SubtleContent } from "~/components/core"; import { deviceLabel } from "~/components/storage/utils"; import { StorageDevice } from "~/types/storage"; -import { useCandidateDevices } from "~/hooks/storage/system"; +import { useCandidateDevices, useDevices } from "~/hooks/storage/system"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; -import { useDevices } from "~/queries/storage"; import { useModel } from "~/hooks/storage/model"; import { useSetBootDevice, @@ -69,7 +68,7 @@ type BootSelectionState = { export default function BootSelectionDialog() { const [state, setState] = useState({ load: false }); const navigate = useNavigate(); - const devices = useDevices("system"); + const devices = useDevices(); const model = useModel({ suspense: true }); const candidateDevices = filteredCandidates(useCandidateDevices(), model); const setBootDevice = useSetBootDevice(); diff --git a/web/src/components/storage/ConfigEditor.tsx b/web/src/components/storage/ConfigEditor.tsx index 74a5a0c529..2e1fb4b663 100644 --- a/web/src/components/storage/ConfigEditor.tsx +++ b/web/src/components/storage/ConfigEditor.tsx @@ -26,12 +26,13 @@ import Text from "~/components/core/Text"; import DriveEditor from "~/components/storage/DriveEditor"; import VolumeGroupEditor from "~/components/storage/VolumeGroupEditor"; import MdRaidEditor from "~/components/storage/MdRaidEditor"; -import { useDevices, useResetConfigMutation } from "~/queries/storage"; +import { useDevices } from "~/hooks/storage/system"; +import { useResetConfig } from "~/hooks/storage/config"; import { useModel } from "~/hooks/storage/model"; import { _ } from "~/i18n"; const NoDevicesConfiguredAlert = () => { - const { mutate: reset } = useResetConfigMutation(); + const reset = useResetConfig(); const title = _("No devices configured yet"); // TRANSLATORS: %s will be replaced by a "reset to default" button const body = _( @@ -72,7 +73,7 @@ const NoDevicesConfiguredAlert = () => { */ export default function ConfigEditor() { const model = useModel(); - const devices = useDevices("system", { suspense: true }); + const devices = useDevices({ suspense: true }); const drives = model.drives; const mdRaids = model.mdRaids; const volumeGroups = model.volumeGroups; diff --git a/web/src/components/storage/ConfigEditorMenu.tsx b/web/src/components/storage/ConfigEditorMenu.tsx index 0437893e66..665d82269a 100644 --- a/web/src/components/storage/ConfigEditorMenu.tsx +++ b/web/src/components/storage/ConfigEditorMenu.tsx @@ -31,8 +31,8 @@ import { DropdownItem, Divider, } from "@patternfly/react-core"; -import { useResetConfigMutation } from "~/queries/storage"; -import { useReactivateSystem } from "~/hooks/storage/system"; +import { useResetConfig } from "~/hooks/storage/config"; +import { activateStorageAction } from "~/api"; import { STORAGE as PATHS } from "~/routes/paths"; import { useZFCPSupported } from "~/queries/storage/zfcp"; import { useDASDSupported } from "~/queries/storage/dasd"; @@ -41,8 +41,7 @@ export default function ConfigEditorMenu() { const navigate = useNavigate(); const isZFCPSupported = useZFCPSupported(); const isDASDSupported = useDASDSupported(); - const { mutate: reset } = useResetConfigMutation(); - const reactivate = useReactivateSystem(); + const reset = useResetConfig(); const [isOpen, setIsOpen] = useState(false); const toggle = () => setIsOpen(!isOpen); @@ -106,7 +105,7 @@ export default function ConfigEditorMenu() { )} {_("Rescan devices")} diff --git a/web/src/components/storage/EncryptionSettingsPage.tsx b/web/src/components/storage/EncryptionSettingsPage.tsx index 8709533a38..ead660ba71 100644 --- a/web/src/components/storage/EncryptionSettingsPage.tsx +++ b/web/src/components/storage/EncryptionSettingsPage.tsx @@ -25,9 +25,9 @@ import { useNavigate } from "react-router-dom"; import { ActionGroup, Alert, Checkbox, Content, Form } from "@patternfly/react-core"; import { NestedContent, Page, PasswordAndConfirmationInput } from "~/components/core"; import PasswordCheck from "~/components/users/PasswordCheck"; -import { useEncryptionMethods } from "~/queries/storage"; +import { useEncryptionMethods } from "~/hooks/storage/system"; import { useEncryption } from "~/queries/storage/config-model"; -import { apiModel } from "~/api/storage/types"; +import { apiModel } from "~/api/storage"; import { isEmpty } from "radashi"; import { _ } from "~/i18n"; diff --git a/web/src/components/storage/FormattableDevicePage.tsx b/web/src/components/storage/FormattableDevicePage.tsx index b93e42b72f..b3f2b1747f 100644 --- a/web/src/components/storage/FormattableDevicePage.tsx +++ b/web/src/components/storage/FormattableDevicePage.tsx @@ -48,15 +48,14 @@ import { import { Page, SelectWrapper as Select } from "~/components/core/"; import { SelectWrapperProps as SelectProps } from "~/components/core/SelectWrapper"; import SelectTypeaheadCreatable from "~/components/core/SelectTypeaheadCreatable"; -import { useMissingMountPaths, useVolume } from "~/hooks/storage/product"; import { useAddFilesystem } from "~/hooks/storage/filesystem"; -import { useModel } from "~/hooks/storage/model"; -import { useDevices } from "~/queries/storage"; -import { data, model, StorageDevice } from "~/types/storage"; +import { useModel, useMissingMountPaths } from "~/hooks/storage/model"; +import { useDevices, useVolumeTemplate } from "~/hooks/storage/system"; +import { data, model } from "~/types/storage"; import { deviceBaseName, filesystemLabel } from "~/components/storage/utils"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { apiModel } from "~/api/storage/types"; +import { apiModel, system } from "~/api/storage"; import { STORAGE as PATHS } from "~/routes/paths"; import { unique } from "radashi"; import { compact } from "~/utils"; @@ -144,9 +143,9 @@ function useDeviceModel(): DeviceModel { return model[list].at(listIndex); } -function useDevice(): StorageDevice { +function useDevice(): system.Device { const deviceModel = useDeviceModel(); - const devices = useDevices("system", { suspense: true }); + const devices = useDevices({ suspense: true }); return devices.find((d) => d.name === deviceModel.name); } @@ -156,7 +155,7 @@ function useCurrentFilesystem(): string | null { } function useDefaultFilesystem(mountPoint: string): string { - const volume = useVolume(mountPoint, { suspense: true }); + const volume = useVolumeTemplate(mountPoint, { suspense: true }); return volume.mountPath === "/" && volume.snapshots ? BTRFS_SNAPSHOTS : volume.fsType; } @@ -173,7 +172,7 @@ function useUnusedMountPoints(): string[] { } function useUsableFilesystems(mountPoint: string): string[] { - const volume = useVolume(mountPoint); + const volume = useVolumeTemplate(mountPoint); const defaultFilesystem = useDefaultFilesystem(mountPoint); const usableFilesystems = React.useMemo(() => { @@ -292,7 +291,7 @@ type FilesystemOptionsProps = { function FilesystemOptions({ mountPoint }: FilesystemOptionsProps): React.ReactNode { const device = useDevice(); - const volume = useVolume(mountPoint); + const volume = useVolumeTemplate(mountPoint); const defaultFilesystem = useDefaultFilesystem(mountPoint); const usableFilesystems = useUsableFilesystems(mountPoint); const currentFilesystem = useCurrentFilesystem(); diff --git a/web/src/components/storage/LogicalVolumePage.tsx b/web/src/components/storage/LogicalVolumePage.tsx index f948904ecb..c78a38b54e 100644 --- a/web/src/components/storage/LogicalVolumePage.tsx +++ b/web/src/components/storage/LogicalVolumePage.tsx @@ -51,14 +51,14 @@ import { SelectWrapperProps as SelectProps } from "~/components/core/SelectWrapp import SelectTypeaheadCreatable from "~/components/core/SelectTypeaheadCreatable"; import AutoSizeText from "~/components/storage/AutoSizeText"; import { deviceSize, filesystemLabel, parseToBytes } from "~/components/storage/utils"; -import { useApiModel, useSolvedApiModel } from "~/hooks/storage/api-model"; -import { useModel } from "~/hooks/storage/model"; -import { useMissingMountPaths, useVolume } from "~/hooks/storage/product"; +import { useSolvedStorageModel, useStorageModel } from "~/hooks/api"; +import { useModel, useMissingMountPaths } from "~/hooks/storage/model"; +import { useVolumeTemplate } from "~/hooks/storage/system"; import { useVolumeGroup } from "~/hooks/storage/volume-group"; import { useAddLogicalVolume, useEditLogicalVolume } from "~/hooks/storage/logical-volume"; import { addLogicalVolume, editLogicalVolume } from "~/helpers/storage/logical-volume"; import { buildLogicalVolumeName } from "~/helpers/storage/api-model"; -import { apiModel } from "~/api/storage/types"; +import { apiModel } from "~/api/storage"; import { data } from "~/types/storage"; import { STORAGE as PATHS } from "~/routes/paths"; import { unique } from "radashi"; @@ -172,7 +172,7 @@ function toFormValue(logicalVolume: apiModel.LogicalVolume): FormValue { } function useDefaultFilesystem(mountPoint: string): string { - const volume = useVolume(mountPoint, { suspense: true }); + const volume = useVolumeTemplate(mountPoint, { suspense: true }); return volume.mountPath === "/" && volume.snapshots ? BTRFS_SNAPSHOTS : volume.fsType; } @@ -200,7 +200,7 @@ function useUnusedMountPoints(): string[] { } function useUsableFilesystems(mountPoint: string): string[] { - const volume = useVolume(mountPoint); + const volume = useVolumeTemplate(mountPoint); const defaultFilesystem = useDefaultFilesystem(mountPoint); const usableFilesystems = useMemo(() => { @@ -339,7 +339,7 @@ function useErrors(value: FormValue): ErrorsHandler { function useSolvedModel(value: FormValue): apiModel.Config | null { const { id: vgName, logicalVolumeId: mountPath } = useParams(); - const apiModel = useApiModel(); + const apiModel = useStorageModel(); const { getError } = useErrors(value); const mountPointError = getError("mountPoint"); const data = toData(value); @@ -358,7 +358,7 @@ function useSolvedModel(value: FormValue): apiModel.Config | null { } } - const solvedModel = useSolvedApiModel(sparseModel); + const solvedModel = useSolvedStorageModel(sparseModel); return solvedModel; } @@ -476,7 +476,7 @@ type FilesystemOptionsProps = { function FilesystemOptions({ mountPoint }: FilesystemOptionsProps): React.ReactNode { const defaultFilesystem = useDefaultFilesystem(mountPoint); const usableFilesystems = useUsableFilesystems(mountPoint); - const volume = useVolume(mountPoint); + const volume = useVolumeTemplate(mountPoint); const defaultOptText = mountPoint !== NO_VALUE && volume.mountPath @@ -561,7 +561,7 @@ type AutoSizeInfoProps = { }; function AutoSizeInfo({ value }: AutoSizeInfoProps): React.ReactNode { - const volume = useVolume(value.mountPoint); + const volume = useVolumeTemplate(value.mountPoint); const logicalVolume = useSolvedLogicalVolume(value); const size = logicalVolume?.size; diff --git a/web/src/components/storage/PartitionPage.tsx b/web/src/components/storage/PartitionPage.tsx index 5e0d039c46..8c2d2d722a 100644 --- a/web/src/components/storage/PartitionPage.tsx +++ b/web/src/components/storage/PartitionPage.tsx @@ -51,20 +51,18 @@ import SizeModeSelect, { SizeMode, SizeRange } from "~/components/storage/SizeMo import AlertOutOfSync from "~/components/core/AlertOutOfSync"; import ResourceNotFound from "~/components/core/ResourceNotFound"; import { useAddPartition, useEditPartition } from "~/hooks/storage/partition"; -import { useMissingMountPaths } from "~/hooks/storage/product"; -import { useModel } from "~/hooks/storage/model"; +import { useModel, useMissingMountPaths } from "~/hooks/storage/model"; import { addPartition as addPartitionHelper, editPartition as editPartitionHelper, } from "~/helpers/storage/partition"; -import { useDevices, useVolume } from "~/queries/storage"; +import { useDevices, useVolumeTemplate } from "~/hooks/storage/system"; import { useConfigModel, useSolvedConfigModel } from "~/queries/storage/config-model"; import { findDevice } from "~/helpers/storage/api-model"; -import { StorageDevice } from "~/types/storage"; import { deviceSize, deviceLabel, filesystemLabel, parseToBytes } from "~/components/storage/utils"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { apiModel } from "~/api/storage/types"; +import { apiModel, system } from "~/api/storage"; import { STORAGE as PATHS, STORAGE } from "~/routes/paths"; import { isUndefined, unique } from "radashi"; import { compact } from "~/utils"; @@ -195,19 +193,19 @@ function useModelDevice() { return model[list].at(listIndex); } -function useDevice(): StorageDevice { +function useDevice(): system.Device { const modelDevice = useModelDevice(); - const devices = useDevices("system", { suspense: true }); + const devices = useDevices({ suspense: true }); return devices.find((d) => d.name === modelDevice.name); } -function usePartition(target: string): StorageDevice | null { +function usePartition(target: string): system.Device | null { const device = useDevice(); if (target === NEW_PARTITION) return null; const partitions = device.partitionTable?.partitions || []; - return partitions.find((p: StorageDevice) => p.name === target); + return partitions.find((p: system.Device) => p.name === target); } function usePartitionFilesystem(target: string): string | null { @@ -216,7 +214,7 @@ function usePartitionFilesystem(target: string): string | null { } function useDefaultFilesystem(mountPoint: string): string { - const volume = useVolume(mountPoint); + const volume = useVolumeTemplate(mountPoint); return volume.mountPath === "/" && volume.snapshots ? BTRFS_SNAPSHOTS : volume.fsType; } @@ -247,7 +245,7 @@ function useUnusedMountPoints(): string[] { } /** Unused partitions. Includes the currently used partition when editing (if any). */ -function useUnusedPartitions(): StorageDevice[] { +function useUnusedPartitions(): system.Device[] { const device = useDevice(); const allPartitions = device.partitionTable?.partitions || []; const initialPartitionConfig = useInitialPartitionConfig(); @@ -260,7 +258,7 @@ function useUnusedPartitions(): StorageDevice[] { } function useUsableFilesystems(mountPoint: string): string[] { - const volume = useVolume(mountPoint); + const volume = useVolumeTemplate(mountPoint); const defaultFilesystem = useDefaultFilesystem(mountPoint); const usableFilesystems = React.useMemo(() => { @@ -502,7 +500,7 @@ function TargetOptionLabel({ value }: TargetOptionLabelProps): React.ReactNode { } type PartitionDescriptionProps = { - partition: StorageDevice; + partition: system.Device; }; function PartitionDescription({ partition }: PartitionDescriptionProps): React.ReactNode { @@ -572,7 +570,7 @@ type FilesystemOptionsProps = { }; function FilesystemOptions({ mountPoint, target }: FilesystemOptionsProps): React.ReactNode { - const volume = useVolume(mountPoint); + const volume = useVolumeTemplate(mountPoint); const defaultFilesystem = useDefaultFilesystem(mountPoint); const usableFilesystems = useUsableFilesystems(mountPoint); const partitionFilesystem = usePartitionFilesystem(target); @@ -673,7 +671,7 @@ type AutoSizeInfoProps = { }; function AutoSizeInfo({ value }: AutoSizeInfoProps): React.ReactNode { - const volume = useVolume(value.mountPoint); + const volume = useVolumeTemplate(value.mountPoint); const solvedPartitionConfig = useSolvedPartitionConfig(value); const size = solvedPartitionConfig?.size; diff --git a/web/src/components/storage/PartitionsMenu.tsx b/web/src/components/storage/PartitionsMenu.tsx index 6421f3fede..10056f6a46 100644 --- a/web/src/components/storage/PartitionsMenu.tsx +++ b/web/src/components/storage/PartitionsMenu.tsx @@ -27,7 +27,7 @@ import Text from "~/components/core/Text"; import MenuButton from "~/components/core/MenuButton"; import MenuHeader from "~/components/core/MenuHeader"; import MountPathMenuItem from "~/components/storage/MountPathMenuItem"; -import { Partition } from "~/api/storage/types/model"; +import { Partition } from "~/api/storage/model"; import { STORAGE as PATHS } from "~/routes/paths"; import { useDeletePartition } from "~/hooks/storage/partition"; import * as driveUtils from "~/components/storage/utils/drive"; diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index 8620d65752..29c4d47a0f 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -23,14 +23,14 @@ import React from "react"; import { Alert, Content } from "@patternfly/react-core"; import { IssueSeverity } from "~/types/issues"; -import { useApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; import { useIssues, useConfigErrors } from "~/queries/issues"; import * as partitionUtils from "~/components/storage/utils/partition"; import { _, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; const Description = () => { - const model = useApiModel({ suspense: true }); + const model = useStorageModel({ suspense: true }); const partitions = model.drives.flatMap((d) => d.partitions || []); const logicalVolumes = model.volumeGroups.flatMap((vg) => vg.logicalVolumes || []); diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index ef1f65ef86..2c8df575a0 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -35,7 +35,7 @@ import { ListItem, } from "@patternfly/react-core"; import { Page, Link } from "~/components/core/"; -import { Icon, Loading } from "~/components/layout"; +import { Icon } from "~/components/layout"; import ConfigEditor from "./ConfigEditor"; import ConfigEditorMenu from "./ConfigEditorMenu"; import ConfigureDeviceMenu from "./ConfigureDeviceMenu"; @@ -46,12 +46,7 @@ import ProposalResultSection from "./ProposalResultSection"; import ProposalTransactionalInfo from "./ProposalTransactionalInfo"; import UnsupportedModelInfo from "./UnsupportedModelInfo"; import { useAvailableDevices } from "~/hooks/storage/system"; -import { - useResetConfigMutation, - useDeprecated, - useDeprecatedChanges, - useReprobeMutation, -} from "~/queries/storage"; +import { useResetConfig } from "~/hooks/storage/config"; import { useConfigModel } from "~/queries/storage/config-model"; import { useZFCPSupported } from "~/queries/storage/zfcp"; import { useDASDSupported } from "~/queries/storage/dasd"; @@ -63,7 +58,7 @@ import { useNavigate } from "react-router-dom"; function InvalidConfigEmptyState(): React.ReactNode { const errors = useConfigErrors("storage"); - const { mutate: reset } = useResetConfigMutation(); + const reset = useResetConfig(); return ( { - if (isDeprecated) reprobe().catch(console.log); - }, [isDeprecated, reprobe]); React.useEffect(() => { if (progress && !progress.finished) navigate(PATHS.progress); @@ -259,9 +247,8 @@ export default function ProposalPage(): React.ReactNode { {_("Storage")} - {isDeprecated && } - {!isDeprecated && !showSections && } - {!isDeprecated && showSections && } + {!showSections && } + {showSections && } ); diff --git a/web/src/components/storage/ProposalResultSection.tsx b/web/src/components/storage/ProposalResultSection.tsx index 6c97fcc5f9..61803f86e7 100644 --- a/web/src/components/storage/ProposalResultSection.tsx +++ b/web/src/components/storage/ProposalResultSection.tsx @@ -27,7 +27,8 @@ import DevicesManager from "~/components/storage/DevicesManager"; import ProposalResultTable from "~/components/storage/ProposalResultTable"; import { ProposalActionsDialog } from "~/components/storage"; import { _, n_, formatList } from "~/i18n"; -import { useActions, useDevices } from "~/queries/storage"; +import { useDevices } from "~/hooks/storage/system"; +import { useActions } from "~/hooks/storage/proposal"; import { sprintf } from "sprintf-js"; /** diff --git a/web/src/components/storage/ProposalTransactionalInfo.tsx b/web/src/components/storage/ProposalTransactionalInfo.tsx index a0fcf86e7e..e112b1776f 100644 --- a/web/src/components/storage/ProposalTransactionalInfo.tsx +++ b/web/src/components/storage/ProposalTransactionalInfo.tsx @@ -25,7 +25,7 @@ import { Alert } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { useProduct } from "~/queries/software"; -import { useVolumes } from "~/queries/storage"; +import { useVolumeTemplates } from "~/hooks/storage/system"; import { isTransactionalSystem } from "~/components/storage/utils"; /** @@ -36,7 +36,7 @@ import { isTransactionalSystem } from "~/components/storage/utils"; */ export default function ProposalTransactionalInfo() { const { selectedProduct } = useProduct({ suspense: true }); - const volumes = useVolumes(); + const volumes = useVolumeTemplates(); if (!isTransactionalSystem(volumes)) return; diff --git a/web/src/components/storage/SpacePolicySelection.tsx b/web/src/components/storage/SpacePolicySelection.tsx index 37d7ed02cc..c26b0f6035 100644 --- a/web/src/components/storage/SpacePolicySelection.tsx +++ b/web/src/components/storage/SpacePolicySelection.tsx @@ -28,8 +28,8 @@ import { SpaceActionsTable } from "~/components/storage"; import { deviceChildren } from "~/components/storage/utils"; import { _ } from "~/i18n"; import { PartitionSlot, SpacePolicyAction, StorageDevice } from "~/types/storage"; -import { apiModel } from "~/api/storage/types"; -import { useDevices } from "~/queries/storage"; +import { apiModel } from "~/api/storage"; +import { useDevices } from "~/hooks/storage/system"; import { useModel } from "~/hooks/storage/model"; import { useSetSpacePolicy } from "~/hooks/storage/space-policy"; import { toStorageDevice } from "./device-utils"; @@ -50,7 +50,7 @@ export default function SpacePolicySelection() { const { list, listIndex } = useParams(); const model = useModel({ suspense: true }); const deviceModel = model[list][listIndex]; - const devices = useDevices("system", { suspense: true }); + const devices = useDevices({ suspense: true }); const device = devices.find((d) => d.name === deviceModel.name); const children = deviceChildren(device); const setSpacePolicy = useSetSpacePolicy(); diff --git a/web/src/components/storage/UnsupportedModelInfo.tsx b/web/src/components/storage/UnsupportedModelInfo.tsx index 3fa20a2c27..a3d23b5c79 100644 --- a/web/src/components/storage/UnsupportedModelInfo.tsx +++ b/web/src/components/storage/UnsupportedModelInfo.tsx @@ -24,14 +24,14 @@ import React from "react"; import { Alert, Button, Content, Stack, StackItem } from "@patternfly/react-core"; import { _ } from "~/i18n"; import { useConfigModel } from "~/queries/storage/config-model"; -import { useResetConfigMutation } from "~/queries/storage"; +import { useResetConfig } from "~/hooks/storage/config"; /** * Info about unsupported model. */ export default function UnsupportedModelInfo(): React.ReactNode { const model = useConfigModel({ suspense: true }); - const { mutate: reset } = useResetConfigMutation(); + const reset = useResetConfig(); if (model) return null; diff --git a/web/src/components/storage/utils.ts b/web/src/components/storage/utils.ts index b0f2d47302..eefa151500 100644 --- a/web/src/components/storage/utils.ts +++ b/web/src/components/storage/utils.ts @@ -30,7 +30,8 @@ import xbytes from "xbytes"; import { _, N_ } from "~/i18n"; import { PartitionSlot, StorageDevice, model } from "~/types/storage"; -import { apiModel, Volume } from "~/api/storage/types"; +import { Volume } from "~/api/storage/system"; +import { apiModel } from "~/api/storage"; import { sprintf } from "sprintf-js"; /** diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index 1dc2afa005..6913c83efd 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -24,8 +24,8 @@ import React, { useCallback, useEffect, useState } from "react"; import { locationReload, setLocationSearch } from "~/utils"; import agama from "~/agama"; import supportedLanguages from "~/languages.json"; -import { fetchSystem, trigger } from "~/api/api"; -import { System } from "~/types/system"; +import { useSystem } from "~/hooks/l10n"; +import { configureL10nAction } from "~/api"; const L10nContext = React.createContext(null); @@ -132,16 +132,6 @@ function languageToLocale(language: string): string { return `${locale}.UTF-8`; } -/** - * Returns the language tag from the backend. - * - * @return Language tag from the backend locale. - */ -async function languageFromBackend(fetchConfig): Promise { - const config = await fetchConfig(); - return languageFromLocale(config?.l10n?.locale); -} - /** * Returns the first supported language from the given list. * @@ -228,31 +218,29 @@ async function loadTranslations(locale: string) { * * @param props * @param [props.children] - Content to display within the wrapper. - * @param [props.fetchConfigFn] - Function to retrieve l10n settings. * * @see useInstallerL10n */ function InstallerL10nProvider({ initialLanguage, - fetchConfigFn, children, }: { initialLanguage?: string; - fetchConfigFn?: () => Promise; children?: React.ReactNode; }) { - const fetchConfig = fetchConfigFn || fetchSystem; + const system = useSystem(); const [language, setLanguage] = useState(initialLanguage); const [keymap, setKeymap] = useState(undefined); - // FIXME: NEW-API: sync and updateConfig with new API once it's ready. + const locale = system?.locale; + const backendLanguage = locale ? languageFromLocale(locale) : null; + const syncBackendLanguage = useCallback(async () => { - const backendLanguage = await languageFromBackend(fetchConfig); - if (backendLanguage === language) return; + if (!backendLanguage || backendLanguage === language) return; // FIXME: fallback to en-US if the language is not supported. - await trigger({ configureL10n: { language: languageToLocale(language) } }); - }, [fetchConfig, language]); + await configureL10nAction({ locale: languageToLocale(language) }); + }, [language, backendLanguage]); const changeLanguage = useCallback( async (lang?: string) => { @@ -269,7 +257,7 @@ function InstallerL10nProvider({ wanted, wanted?.split("-")[0], // fallback to the language (e.g., "es" for "es-AR") agamaLanguage(), - await languageFromBackend(fetchConfig), + backendLanguage, ].filter((l) => l); const newLanguage = findSupportedLanguage(candidateLanguages) || "en-US"; const mustReload = storeAgamaLanguage(newLanguage); @@ -284,13 +272,13 @@ function InstallerL10nProvider({ await loadTranslations(newLanguage); } }, - [fetchConfig, setLanguage], + [backendLanguage, setLanguage], ); const changeKeymap = useCallback( async (id: string) => { setKeymap(id); - await trigger({ configureL10n: { keymap: id } }); + await configureL10nAction({ keymap: id }); }, [setKeymap], ); @@ -301,13 +289,12 @@ function InstallerL10nProvider({ useEffect(() => { if (!language) return; - // syncBackendLanguage(); }, [language, syncBackendLanguage]); useEffect(() => { - fetchConfig().then((c) => setKeymap(c?.l10n?.keymap)); - }, [setKeymap, fetchConfig]); + setKeymap(system?.keymap); + }, [setKeymap, system]); const value = { language, changeLanguage, keymap, changeKeymap }; diff --git a/web/src/helpers/l10n.ts b/web/src/helpers/l10n.ts new file mode 100644 index 0000000000..237d42a031 --- /dev/null +++ b/web/src/helpers/l10n.ts @@ -0,0 +1,30 @@ +/* + * 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 { tzOffset } from "@date-fns/tz/tzOffset"; +import { Timezone } from "~/api/l10n/system"; + +function timezoneOffset(timezone: Timezone) { + return tzOffset(timezone.id, new Date()); +} + +export { timezoneOffset }; diff --git a/web/src/helpers/storage/system.ts b/web/src/helpers/storage/system.ts new file mode 100644 index 0000000000..cad6e40ef2 --- /dev/null +++ b/web/src/helpers/storage/system.ts @@ -0,0 +1,36 @@ +/* + * 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 { System, Device } from "~/api/storage/system"; + +function findDevice(system: System, sid: number): Device | undefined { + const device = system.devices.find((d) => d.sid === sid); + if (device === undefined) console.warn("Device not found:", sid); + + return device; +} + +function findDevices(system: System, sids: number[]): Device[] { + return sids.map((sid) => findDevice(system, sid)).filter((d) => d); +} + +export { findDevice, findDevices }; diff --git a/web/src/hooks/api.ts b/web/src/hooks/api.ts new file mode 100644 index 0000000000..77f322e875 --- /dev/null +++ b/web/src/hooks/api.ts @@ -0,0 +1,146 @@ +/* + * 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 from "react"; +import { useQuery, useSuspenseQuery, useQueryClient } from "@tanstack/react-query"; +import { + getSystem, + getProposal, + getExtendedConfig, + solveStorageModel, + getStorageModel, +} from "~/api"; +import { useInstallerClient } from "~/context/installer"; +import { System } from "~/api/system"; +import { Proposal } from "~/api/proposal"; +import { Config } from "~/api/config"; +import { apiModel } from "~/api/storage"; +import { QueryHookOptions } from "~/types/queries"; + +const systemQuery = () => ({ + queryKey: ["system"], + queryFn: getSystem, +}); + +function useSystem(options?: QueryHookOptions): System | null { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func(systemQuery()); + return data; +} + +function useSystemChanges() { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + + React.useEffect(() => { + if (!client) return; + + // TODO: replace the scope instead of invalidating the query. + return client.onEvent((event) => { + if (event.type === "SystemChanged") { + queryClient.invalidateQueries({ queryKey: ["system"] }); + } + }); + }, [client, queryClient]); +} + +const proposalQuery = () => { + return { + queryKey: ["proposal"], + queryFn: getProposal, + }; +}; + +function useProposal(options?: QueryHookOptions): Proposal | null { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func(proposalQuery()); + return data; +} + +function useProposalChanges() { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + + React.useEffect(() => { + if (!client) return; + + // TODO: replace the scope instead of invalidating the query. + return client.onEvent((event) => { + if (event.type === "ProposalChanged") { + queryClient.invalidateQueries({ queryKey: ["extendedConfig"] }); + queryClient.invalidateQueries({ queryKey: ["storageModel"] }); + queryClient.invalidateQueries({ queryKey: ["proposal"] }); + } + }); + }, [client, queryClient]); +} + +const extendedConfigQuery = () => ({ + queryKey: ["extendedConfig"], + queryFn: getExtendedConfig, +}); + +function useExtendedConfig(options?: QueryHookOptions): Config | null { + const query = extendedConfigQuery(); + const func = options?.suspense ? useSuspenseQuery : useQuery; + return func(query)?.data; +} + +const storageModelQuery = () => ({ + queryKey: ["storageModel"], + queryFn: getStorageModel, +}); + +function useStorageModel(options?: QueryHookOptions): apiModel.Config | null { + const query = storageModelQuery(); + const func = options?.suspense ? useSuspenseQuery : useQuery; + return func(query)?.data; +} + +const solvedStorageModelQuery = (apiModel?: apiModel.Config) => ({ + queryKey: ["solvedStorageModel", JSON.stringify(apiModel)], + queryFn: () => (apiModel ? solveStorageModel(apiModel) : Promise.resolve(null)), + staleTime: Infinity, +}); + +function useSolvedStorageModel( + model?: apiModel.Config, + options?: QueryHookOptions, +): apiModel.Config | null { + const query = solvedStorageModelQuery(model); + const func = options?.suspense ? useSuspenseQuery : useQuery; + return func(query)?.data; +} + +export { + systemQuery, + proposalQuery, + extendedConfigQuery, + storageModelQuery, + useSystem, + useSystemChanges, + useProposal, + useProposalChanges, + useExtendedConfig, + useStorageModel, + useSolvedStorageModel, +}; diff --git a/web/src/hooks/l10n.ts b/web/src/hooks/l10n.ts new file mode 100644 index 0000000000..2b062d49c2 --- /dev/null +++ b/web/src/hooks/l10n.ts @@ -0,0 +1,39 @@ +/* + * 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 { useSuspenseQuery, useQuery } from "@tanstack/react-query"; +import { System, l10n } from "~/api/system"; +import { QueryHookOptions } from "~/types/queries"; +import { systemQuery } from "~/hooks/api"; + +const selectSystem = (data: System | null): l10n.System | null => data?.l10n; + +function useSystem(options?: QueryHookOptions): l10n.System | null { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectSystem, + }); + return data; +} + +export { useSystem }; diff --git a/web/src/hooks/storage/api-model.ts b/web/src/hooks/storage/api-model.ts deleted file mode 100644 index 0610caef21..0000000000 --- a/web/src/hooks/storage/api-model.ts +++ /dev/null @@ -1,61 +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 { useQuery, useSuspenseQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { apiModel } from "~/api/storage/types"; -import { apiModelQuery, solveApiModelQuery } from "~/queries/storage"; -import { QueryHookOptions } from "~/types/queries"; -import { setConfigModel } from "~/api/storage"; - -function useApiModel(options?: QueryHookOptions): apiModel.Config | null { - const query = apiModelQuery; - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func(query); - return data || null; -} - -/** @todo Use a hash key from the model object as id for the query. */ -function useSolvedApiModel( - model?: apiModel.Config, - options?: QueryHookOptions, -): apiModel.Config | null { - const query = solveApiModelQuery(model); - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func(query); - return data; -} - -type UpdateApiModelFn = (apiModel: apiModel.Config) => void; - -function useUpdateApiModel(): UpdateApiModelFn { - const queryClient = useQueryClient(); - const query = { - mutationFn: (apiModel: apiModel.Config) => setConfigModel(apiModel), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage"] }), - }; - - const { mutate } = useMutation(query); - return mutate; -} - -export { useApiModel, useSolvedApiModel, useUpdateApiModel }; -export type { UpdateApiModelFn }; diff --git a/web/src/hooks/storage/boot.ts b/web/src/hooks/storage/boot.ts index 9c59da44d3..aa9a2c3d73 100644 --- a/web/src/hooks/storage/boot.ts +++ b/web/src/hooks/storage/boot.ts @@ -21,7 +21,8 @@ */ import { useModel } from "~/hooks/storage/model"; -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { QueryHookOptions } from "~/types/queries"; import { setBootDevice, setDefaultBootDevice, disableBootConfig } from "~/helpers/storage/boot"; @@ -29,27 +30,24 @@ type SetBootDeviceFn = (deviceName: string) => void; function useSetBootDevice(options?: QueryHookOptions): SetBootDeviceFn { const model = useModel(options); - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); - return (deviceName: string) => updateApiModel(setBootDevice(model, apiModel, deviceName)); + const apiModel = useStorageModel(options); + return (deviceName: string) => putStorageModel(setBootDevice(model, apiModel, deviceName)); } type SetDefaultBootDeviceFn = () => void; function useSetDefaultBootDevice(options?: QueryHookOptions): SetDefaultBootDeviceFn { const model = useModel(options); - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); - return () => updateApiModel(setDefaultBootDevice(model, apiModel)); + const apiModel = useStorageModel(options); + return () => putStorageModel(setDefaultBootDevice(model, apiModel)); } type DisableBootConfigFn = () => void; function useDisableBootConfig(options?: QueryHookOptions): DisableBootConfigFn { const model = useModel(options); - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); - return () => updateApiModel(disableBootConfig(model, apiModel)); + const apiModel = useStorageModel(options); + return () => putStorageModel(disableBootConfig(model, apiModel)); } export { useSetBootDevice, useSetDefaultBootDevice, useDisableBootConfig }; diff --git a/web/src/hooks/storage/config.ts b/web/src/hooks/storage/config.ts new file mode 100644 index 0000000000..fba08ca78d --- /dev/null +++ b/web/src/hooks/storage/config.ts @@ -0,0 +1,43 @@ +/* + * 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 { useSuspenseQuery, useQuery } from "@tanstack/react-query"; +import { QueryHookOptions } from "~/types/queries"; +import { extendedConfigQuery } from "~/hooks/api"; +import { putConfig, Response } from "~/api"; +import { Config } from "~/api/config"; + +const resetConfig = (data: Config | null): Config => (!data ? {} : { ...data, storage: null }); + +type ResetConfigFn = () => Response; + +function useResetConfig(options?: QueryHookOptions): ResetConfigFn { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...extendedConfigQuery(), + select: resetConfig, + }); + return () => putConfig(data); +} + +export { useResetConfig }; +export type { ResetConfigFn }; diff --git a/web/src/hooks/storage/drive.ts b/web/src/hooks/storage/drive.ts index bf7f4d8624..b83c56e73b 100644 --- a/web/src/hooks/storage/drive.ts +++ b/web/src/hooks/storage/drive.ts @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { addDrive, deleteDrive, switchToDrive } from "~/helpers/storage/drive"; import { QueryHookOptions } from "~/types/queries"; import { model, data } from "~/types/storage"; @@ -35,30 +36,27 @@ function useDrive(name: string, options?: QueryHookOptions): model.Drive | null type AddDriveFn = (data: data.Drive) => void; function useAddDrive(options?: QueryHookOptions): AddDriveFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (data: data.Drive) => { - updateApiModel(addDrive(apiModel, data)); + putStorageModel(addDrive(apiModel, data)); }; } type DeleteDriveFn = (name: string) => void; function useDeleteDrive(options?: QueryHookOptions): DeleteDriveFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (name: string) => { - updateApiModel(deleteDrive(apiModel, name)); + putStorageModel(deleteDrive(apiModel, name)); }; } type SwitchToDriveFn = (oldName: string, drive: data.Drive) => void; function useSwitchToDrive(options?: QueryHookOptions): SwitchToDriveFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (oldName: string, drive: data.Drive) => { - updateApiModel(switchToDrive(apiModel, oldName, drive)); + putStorageModel(switchToDrive(apiModel, oldName, drive)); }; } diff --git a/web/src/hooks/storage/filesystem.ts b/web/src/hooks/storage/filesystem.ts index b674f2fb05..6f2213919f 100644 --- a/web/src/hooks/storage/filesystem.ts +++ b/web/src/hooks/storage/filesystem.ts @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { configureFilesystem } from "~/helpers/storage/filesystem"; import { QueryHookOptions } from "~/types/queries"; import { data } from "~/types/storage"; @@ -28,20 +29,18 @@ import { data } from "~/types/storage"; type AddFilesystemFn = (list: string, index: number, data: data.Formattable) => void; function useAddFilesystem(options?: QueryHookOptions): AddFilesystemFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (list: string, index: number, data: data.Formattable) => { - updateApiModel(configureFilesystem(apiModel, list, index, data)); + putStorageModel(configureFilesystem(apiModel, list, index, data)); }; } type DeleteFilesystemFn = (list: string, index: number) => void; function useDeleteFilesystem(options?: QueryHookOptions): DeleteFilesystemFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (list: string, index: number) => { - updateApiModel(configureFilesystem(apiModel, list, index, {})); + putStorageModel(configureFilesystem(apiModel, list, index, {})); }; } diff --git a/web/src/hooks/storage/logical-volume.ts b/web/src/hooks/storage/logical-volume.ts index f5ce5c9715..febbbab3f9 100644 --- a/web/src/hooks/storage/logical-volume.ts +++ b/web/src/hooks/storage/logical-volume.ts @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { QueryHookOptions } from "~/types/queries"; import { data } from "~/types/storage"; import { @@ -32,30 +33,27 @@ import { type AddLogicalVolumeFn = (vgName: string, data: data.LogicalVolume) => void; function useAddLogicalVolume(options?: QueryHookOptions): AddLogicalVolumeFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (vgName: string, data: data.LogicalVolume) => { - updateApiModel(addLogicalVolume(apiModel, vgName, data)); + putStorageModel(addLogicalVolume(apiModel, vgName, data)); }; } type EditLogicalVolumeFn = (vgName: string, mountPath: string, data: data.LogicalVolume) => void; function useEditLogicalVolume(options?: QueryHookOptions): EditLogicalVolumeFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (vgName: string, mountPath: string, data: data.LogicalVolume) => { - updateApiModel(editLogicalVolume(apiModel, vgName, mountPath, data)); + putStorageModel(editLogicalVolume(apiModel, vgName, mountPath, data)); }; } type DeleteLogicalVolumeFn = (vgName: string, mountPath: string) => void; function useDeleteLogicalVolume(options?: QueryHookOptions): DeleteLogicalVolumeFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (vgName: string, mountPath: string) => - updateApiModel(deleteLogicalVolume(apiModel, vgName, mountPath)); + putStorageModel(deleteLogicalVolume(apiModel, vgName, mountPath)); } export { useAddLogicalVolume, useEditLogicalVolume, useDeleteLogicalVolume }; diff --git a/web/src/hooks/storage/md-raid.ts b/web/src/hooks/storage/md-raid.ts index 2c387cc1d0..8a8066588d 100644 --- a/web/src/hooks/storage/md-raid.ts +++ b/web/src/hooks/storage/md-raid.ts @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { addReusedMdRaid, deleteMdRaid, switchToMdRaid } from "~/helpers/storage/md-raid"; import { QueryHookOptions } from "~/types/queries"; import { data } from "~/types/storage"; @@ -28,30 +29,27 @@ import { data } from "~/types/storage"; type AddReusedMdRaidFn = (data: data.MdRaid) => void; function useAddReusedMdRaid(options?: QueryHookOptions): AddReusedMdRaidFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (data: data.MdRaid) => { - updateApiModel(addReusedMdRaid(apiModel, data)); + putStorageModel(addReusedMdRaid(apiModel, data)); }; } type DeleteMdRaidFn = (name: string) => void; function useDeleteMdRaid(options?: QueryHookOptions): DeleteMdRaidFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (name: string) => { - updateApiModel(deleteMdRaid(apiModel, name)); + putStorageModel(deleteMdRaid(apiModel, name)); }; } type SwitchToMdRaidFn = (oldName: string, raid: data.MdRaid) => void; function useSwitchToMdRaid(options?: QueryHookOptions): SwitchToMdRaidFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (oldName: string, raid: data.MdRaid) => { - updateApiModel(switchToMdRaid(apiModel, oldName, raid)); + putStorageModel(switchToMdRaid(apiModel, oldName, raid)); }; } diff --git a/web/src/hooks/storage/model.ts b/web/src/hooks/storage/model.ts index fcf27af15b..756ba5f101 100644 --- a/web/src/hooks/storage/model.ts +++ b/web/src/hooks/storage/model.ts @@ -20,20 +20,42 @@ * find current contact information at www.suse.com. */ -import { useMemo } from "react"; -import { useApiModel } from "~/hooks/storage/api-model"; +import { useCallback } from "react"; +import { useSuspenseQuery, useQuery } from "@tanstack/react-query"; +import { storageModelQuery } from "~/hooks/api"; +import { useSystem } from "~/hooks/storage/system"; +import { apiModel } from "~/api/storage"; import { buildModel } from "~/helpers/storage/model"; import { QueryHookOptions } from "~/types/queries"; import { model } from "~/types/storage"; -function useModel(options?: QueryHookOptions): model.Model | null { - const apiModel = useApiModel(options); +const modelFromData = (data: apiModel.Config | null): model.Model | null => + data ? buildModel(data) : null; - const model = useMemo((): model.Model | null => { - return apiModel ? buildModel(apiModel) : null; - }, [apiModel]); +function useModel(options?: QueryHookOptions): model.Model | null { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...storageModelQuery(), + select: modelFromData, + }); + return data; +} - return model; +function useMissingMountPaths(options?: QueryHookOptions): string[] { + const productMountPoints = useSystem()?.productMountPoints; + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...storageModelQuery(), + select: useCallback( + (data: apiModel.Config | null): string[] => { + const model = modelFromData(data); + const currentMountPaths = model?.getMountPaths() || []; + return (productMountPoints || []).filter((p) => !currentMountPaths.includes(p)); + }, + [productMountPoints], + ), + }); + return data; } -export { useModel }; +export { useModel, useMissingMountPaths }; diff --git a/web/src/hooks/storage/partition.ts b/web/src/hooks/storage/partition.ts index 291ae68a30..84bca69956 100644 --- a/web/src/hooks/storage/partition.ts +++ b/web/src/hooks/storage/partition.ts @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { QueryHookOptions } from "~/types/queries"; import { data } from "~/types/storage"; import { addPartition, editPartition, deletePartition } from "~/helpers/storage/partition"; @@ -32,10 +33,9 @@ type AddPartitionFn = ( ) => void; function useAddPartition(options?: QueryHookOptions): AddPartitionFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (list: "drives" | "mdRaids", listIndex: number | string, data: data.Partition) => { - updateApiModel(addPartition(apiModel, list, listIndex, data)); + putStorageModel(addPartition(apiModel, list, listIndex, data)); }; } @@ -47,15 +47,14 @@ type EditPartitionFn = ( ) => void; function useEditPartition(options?: QueryHookOptions): EditPartitionFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return ( list: "drives" | "mdRaids", listIndex: number | string, mountPath: string, data: data.Partition, ) => { - updateApiModel(editPartition(apiModel, list, listIndex, mountPath, data)); + putStorageModel(editPartition(apiModel, list, listIndex, mountPath, data)); }; } @@ -66,10 +65,9 @@ type DeletePartitionFn = ( ) => void; function useDeletePartition(options?: QueryHookOptions): DeletePartitionFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (list: "drives" | "mdRaids", listIndex: number | string, mountPath: string) => - updateApiModel(deletePartition(apiModel, list, listIndex, mountPath)); + putStorageModel(deletePartition(apiModel, list, listIndex, mountPath)); } export { useAddPartition, useEditPartition, useDeletePartition }; diff --git a/web/src/hooks/storage/product.ts b/web/src/hooks/storage/product.ts deleted file mode 100644 index 54f7ce425c..0000000000 --- a/web/src/hooks/storage/product.ts +++ /dev/null @@ -1,60 +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 { useMemo } from "react"; -import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; -import { QueryHookOptions } from "~/types/queries"; -import { ProductParams, Volume } from "~/api/storage/types"; -import { productParamsQuery, volumeQuery } from "~/queries/storage"; -import { useModel } from "~/hooks/storage/model"; - -function useProductParams(options?: QueryHookOptions): ProductParams { - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func(productParamsQuery); - return data; -} - -function useMissingMountPaths(options?: QueryHookOptions): string[] { - const productParams = useProductParams(options); - const model = useModel(); - - const missingMountPaths = useMemo(() => { - const currentMountPaths = model?.getMountPaths() || []; - return (productParams?.mountPoints || []).filter((p) => !currentMountPaths.includes(p)); - }, [productParams, model]); - - return missingMountPaths; -} - -function useVolume(mountPath: string, options?: QueryHookOptions): Volume { - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { mountPoints } = useProductParams(options); - - // The query returns a volume with the given mount path, but we need the "generic" volume without - // mount path for an arbitrary mount path. Take it into account while refactoring the backend side - // in order to report all the volumes in a single call (e.g., as part of the product params). - if (!mountPoints.includes(mountPath)) mountPath = ""; - const { data } = func(volumeQuery(mountPath)); - return data; -} - -export { useProductParams, useMissingMountPaths, useVolume }; diff --git a/web/src/hooks/storage/proposal.ts b/web/src/hooks/storage/proposal.ts new file mode 100644 index 0000000000..8bda265b1c --- /dev/null +++ b/web/src/hooks/storage/proposal.ts @@ -0,0 +1,39 @@ +/* + * 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 { useSuspenseQuery, useQuery } from "@tanstack/react-query"; +import { Proposal, storage } from "~/api/proposal"; +import { QueryHookOptions } from "~/types/queries"; +import { proposalQuery } from "~/hooks/api"; + +const selectActions = (data: Proposal | null): storage.Action[] => data?.storage?.actions || []; + +function useActions(options?: QueryHookOptions): storage.Action[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...proposalQuery(), + select: selectActions, + }); + return data; +} + +export { useActions }; diff --git a/web/src/hooks/storage/space-policy.ts b/web/src/hooks/storage/space-policy.ts index 463ee3c3ab..802357a709 100644 --- a/web/src/hooks/storage/space-policy.ts +++ b/web/src/hooks/storage/space-policy.ts @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { QueryHookOptions } from "~/types/queries"; import { data } from "~/types/storage"; import { setSpacePolicy } from "~/helpers/storage/space-policy"; @@ -28,10 +29,9 @@ import { setSpacePolicy } from "~/helpers/storage/space-policy"; type setSpacePolicyFn = (list: string, listIndex: number | string, data: data.SpacePolicy) => void; function useSetSpacePolicy(options?: QueryHookOptions): setSpacePolicyFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (list: string, listIndex: number | string, data: data.SpacePolicy) => { - updateApiModel(setSpacePolicy(apiModel, list, listIndex, data)); + putStorageModel(setSpacePolicy(apiModel, list, listIndex, data)); }; } diff --git a/web/src/hooks/storage/system.ts b/web/src/hooks/storage/system.ts index 436ff3d078..5d4d223a5b 100644 --- a/web/src/hooks/storage/system.ts +++ b/web/src/hooks/storage/system.ts @@ -20,120 +20,186 @@ * find current contact information at www.suse.com. */ -import { useSuspenseQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { useMemo } from "react"; -import { - useDevices, - availableDrivesQuery, - candidateDrivesQuery, - availableMdRaidsQuery, - candidateMdRaidsQuery, -} from "~/queries/storage"; -import { reactivate } from "~/api/storage"; -import { StorageDevice } from "~/types/storage"; - -function findDevice(devices: StorageDevice[], sid: number): StorageDevice | undefined { - const device = devices.find((d) => d.sid === sid); - if (device === undefined) console.warn("Device not found:", sid); - - return device; +import { useCallback } from "react"; +import { useSuspenseQuery, useQuery } from "@tanstack/react-query"; +import { System, storage } from "~/api/system"; +import { QueryHookOptions } from "~/types/queries"; +import { systemQuery } from "~/hooks/api"; +import { findDevices } from "~/helpers/storage/system"; + +const selectSystem = (data: System | null): storage.System => data?.storage; + +function useSystem(options?: QueryHookOptions): storage.System { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectSystem, + }); + return data; +} + +const selectEncryptionMethods = (data: System | null): storage.EncryptionMethod[] => + data?.storage?.encryptionMethods || []; + +function useEncryptionMethods(options?: QueryHookOptions): storage.EncryptionMethod[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectEncryptionMethods, + }); + return data; +} + +const enum DeviceGroup { + AvailableDrives = "availableDrives", + CandidateDrives = "candidateDrives", + AvailableMdRaids = "availableMdRaids", + CandidateMdRaids = "candidateMdRaids", +} + +function selectDeviceGroups(data: System | null, groups: DeviceGroup[]): storage.Device[] { + if (!data?.storage) return []; + const sids = groups.flatMap((g) => data.storage[g]); + return findDevices(data.storage, sids); } +const selectAvailableDrives = (data: System | null) => + selectDeviceGroups(data, [DeviceGroup.AvailableDrives]); + /** * Hook that returns the list of available drives for installation. */ -const useAvailableDrives = (): StorageDevice[] => { - const devices = useDevices("system", { suspense: true }); - const { data: sids } = useSuspenseQuery(availableDrivesQuery()); +function useAvailableDrives(options?: QueryHookOptions): storage.Device[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectAvailableDrives, + }); + return data; +} - return useMemo(() => { - return sids.map((sid: number) => findDevice(devices, sid)).filter((d) => d); - }, [devices, sids]); -}; +const selectCandidateDrives = (data: System | null) => + selectDeviceGroups(data, [DeviceGroup.CandidateDrives]); /** * Hook that returns the list of candidate drives for installation. */ -const useCandidateDrives = (): StorageDevice[] => { - const devices = useDevices("system", { suspense: true }); - const { data: sids } = useSuspenseQuery(candidateDrivesQuery()); +function useCandidateDrives(options?: QueryHookOptions): storage.Device[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectCandidateDrives, + }); + return data; +} - return useMemo(() => { - return sids.map((sid: number) => findDevice(devices, sid)).filter((d) => d); - }, [devices, sids]); -}; +const selectAvailableMdRaids = (data: System | null) => + selectDeviceGroups(data, [DeviceGroup.AvailableMdRaids]); /** * Hook that returns the list of available MD RAIDs for installation. */ -const useAvailableMdRaids = (): StorageDevice[] => { - const devices = useDevices("system", { suspense: true }); - const { data: sids } = useSuspenseQuery(availableMdRaidsQuery()); +function useAvailableMdRaids(options?: QueryHookOptions): storage.Device[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectAvailableMdRaids, + }); + return data; +} - return useMemo(() => { - return sids.map((sid: number) => findDevice(devices, sid)).filter((d) => d); - }, [devices, sids]); -}; +const selectCandidateMdRaids = (data: System | null) => + selectDeviceGroups(data, [DeviceGroup.CandidateMdRaids]); /** * Hook that returns the list of available MD RAIDs for installation. */ -const useCandidateMdRaids = (): StorageDevice[] => { - const devices = useDevices("system", { suspense: true }); - const { data: sids } = useSuspenseQuery(candidateMdRaidsQuery()); +function useCandidateMdRaids(options?: QueryHookOptions): storage.Device[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectCandidateMdRaids, + }); + return data; +} - return useMemo(() => { - return sids.map((sid: number) => findDevice(devices, sid)).filter((d) => d); - }, [devices, sids]); -}; +const selectAvailableDevices = (data: System | null) => + selectDeviceGroups(data, [DeviceGroup.AvailableDrives, DeviceGroup.AvailableMdRaids]); /** * Hook that returns the list of available devices for installation. */ -const useAvailableDevices = (): StorageDevice[] => { - const availableDrives = useAvailableDrives(); - const availableMdRaids = useAvailableMdRaids(); - - return useMemo( - () => [...availableDrives, ...availableMdRaids], - [availableDrives, availableMdRaids], - ); -}; +function useAvailableDevices(options?: QueryHookOptions): storage.Device[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectAvailableDevices, + }); + return data; +} + +const selectCandidateDevices = (data: System | null) => + selectDeviceGroups(data, [DeviceGroup.CandidateDrives, DeviceGroup.CandidateMdRaids]); /** * Hook that returns the list of candidate devices for installation. */ -const useCandidateDevices = (): StorageDevice[] => { - const candidateDrives = useCandidateDrives(); - const candidateMdRaids = useCandidateMdRaids(); - - return useMemo( - () => [...candidateMdRaids, ...candidateDrives], - [candidateDrives, candidateMdRaids], - ); -}; +function useCandidateDevices(options?: QueryHookOptions): storage.Device[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectCandidateDevices, + }); + return data; +} -type ReactivateSystemFn = () => void; +const selectDevices = (data: System | null): storage.Device[] => data?.storage?.devices || []; -function useReactivateSystem(): ReactivateSystemFn { - const queryClient = useQueryClient(); - const query = { - mutationFn: reactivate, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage"] }), - }; +function useDevices(options?: QueryHookOptions): storage.Device[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectDevices, + }); + return data; +} - const { mutate } = useMutation(query); - return mutate; +const selectVolumeTemplates = (data: System | null): storage.Volume[] => + data?.storage?.volumeTemplates || []; + +function useVolumeTemplates(options?: QueryHookOptions): storage.Volume[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectVolumeTemplates, + }); + return data; +} + +const selectVolumeTemplate = (data: System | null, mountPath: string): storage.Volume | null => { + const volumes = data?.storage?.volumeTemplates || []; + return volumes.find((v) => v.mountPath === mountPath); +}; + +function useVolumeTemplate(mountPath: string, options?: QueryHookOptions): storage.Volume | null { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: useCallback((data) => selectVolumeTemplate(data, mountPath), [mountPath]), + }); + return data; } export { + useSystem, + useEncryptionMethods, useAvailableDrives, useCandidateDrives, useAvailableMdRaids, useCandidateMdRaids, useAvailableDevices, useCandidateDevices, - useReactivateSystem, + useDevices, + useVolumeTemplates, + useVolumeTemplate, }; - -export type { ReactivateSystemFn }; diff --git a/web/src/hooks/storage/volume-group.ts b/web/src/hooks/storage/volume-group.ts index 03f23a43fa..6055cdcd56 100644 --- a/web/src/hooks/storage/volume-group.ts +++ b/web/src/hooks/storage/volume-group.ts @@ -20,7 +20,8 @@ * find current contact information at www.suse.com. */ -import { useApiModel, useUpdateApiModel } from "~/hooks/storage/api-model"; +import { useStorageModel } from "~/hooks/api"; +import { putStorageModel } from "~/api"; import { addVolumeGroup, editVolumeGroup, @@ -41,30 +42,27 @@ function useVolumeGroup(vgName: string, options?: QueryHookOptions): model.Volum type AddVolumeGroupFn = (data: data.VolumeGroup, moveContent: boolean) => void; function useAddVolumeGroup(options?: QueryHookOptions): AddVolumeGroupFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (data: data.VolumeGroup, moveContent: boolean) => { - updateApiModel(addVolumeGroup(apiModel, data, moveContent)); + putStorageModel(addVolumeGroup(apiModel, data, moveContent)); }; } type EditVolumeGroupFn = (vgName: string, data: data.VolumeGroup) => void; function useEditVolumeGroup(options?: QueryHookOptions): EditVolumeGroupFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (vgName: string, data: data.VolumeGroup) => { - updateApiModel(editVolumeGroup(apiModel, vgName, data)); + putStorageModel(editVolumeGroup(apiModel, vgName, data)); }; } type DeleteVolumeGroupFn = (vgName: string, moveToDrive: boolean) => void; function useDeleteVolumeGroup(options?: QueryHookOptions): DeleteVolumeGroupFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (vgName: string, moveToDrive: boolean) => { - updateApiModel( + putStorageModel( moveToDrive ? volumeGroupToPartitions(apiModel, vgName) : deleteVolumeGroup(apiModel, vgName), ); }; @@ -73,10 +71,9 @@ function useDeleteVolumeGroup(options?: QueryHookOptions): DeleteVolumeGroupFn { type ConvertToVolumeGroupFn = (driveName: string) => void; function useConvertToVolumeGroup(options?: QueryHookOptions): ConvertToVolumeGroupFn { - const apiModel = useApiModel(options); - const updateApiModel = useUpdateApiModel(); + const apiModel = useStorageModel(options); return (driveName: string) => { - updateApiModel(deviceToVolumeGroup(apiModel, driveName)); + putStorageModel(deviceToVolumeGroup(apiModel, driveName)); }; } diff --git a/web/src/api/http.ts b/web/src/http.ts similarity index 100% rename from web/src/api/http.ts rename to web/src/http.ts diff --git a/web/src/queries/proposal.ts b/web/src/queries/proposal.ts deleted file mode 100644 index c83df902f2..0000000000 --- a/web/src/queries/proposal.ts +++ /dev/null @@ -1,57 +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 from "react"; -import { useSuspenseQuery, useQueryClient } from "@tanstack/react-query"; -import { useInstallerClient } from "~/context/installer"; -import { fetchProposal } from "~/api/api"; - -/** - * Returns a query for retrieving the proposal - */ -const proposalQuery = () => { - return { - queryKey: ["proposal"], - queryFn: fetchProposal, - }; -}; - -const useProposal = () => { - const { data: config } = useSuspenseQuery(proposalQuery()); - return config; -}; - -const useProposalChanges = () => { - const queryClient = useQueryClient(); - const client = useInstallerClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "ProposalChanged" && event.scope === "localization") { - queryClient.invalidateQueries({ queryKey: ["proposal"] }); - } - }); - }, [client, queryClient]); -}; -export { useProposal, useProposalChanges }; diff --git a/web/src/queries/storage.ts b/web/src/queries/storage.ts deleted file mode 100644 index 27683f431c..0000000000 --- a/web/src/queries/storage.ts +++ /dev/null @@ -1,299 +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 { useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; -import React from "react"; -import { - fetchConfig, - setConfig, - resetConfig, - fetchConfigModel, - solveConfigModel, - fetchActions, - fetchVolume, - fetchVolumes, - fetchProductParams, - fetchAvailableDrives, - fetchCandidateDrives, - fetchAvailableMdRaids, - fetchCandidateMdRaids, - reprobe, -} from "~/api/storage"; -import { fetchDevices, fetchDevicesDirty } from "~/api/storage/devices"; -import { useInstallerClient } from "~/context/installer"; -import { config, apiModel, ProductParams, Volume } from "~/api/storage/types"; -import { Action, StorageDevice } from "~/types/storage"; -import { QueryHookOptions } from "~/types/queries"; - -const configQuery = { - queryKey: ["storage", "config"], - queryFn: fetchConfig, - staleTime: Infinity, -}; - -const apiModelQuery = { - queryKey: ["storage", "apiModel"], - queryFn: fetchConfigModel, - staleTime: Infinity, -}; - -const solveApiModelQuery = (apiModel?: apiModel.Config) => ({ - queryKey: ["storage", "solveApiModel", JSON.stringify(apiModel)], - queryFn: () => (apiModel ? solveConfigModel(apiModel) : Promise.resolve(null)), - staleTime: Infinity, -}); - -const devicesQuery = (scope: "result" | "system") => ({ - queryKey: ["storage", "devices", scope], - queryFn: () => fetchDevices(scope), - staleTime: Infinity, -}); - -const availableDrivesQuery = () => ({ - queryKey: ["storage", "availableDrives"], - queryFn: fetchAvailableDrives, - staleTime: Infinity, -}); - -const candidateDrivesQuery = () => ({ - queryKey: ["storage", "candidateDrives"], - queryFn: fetchCandidateDrives, - staleTime: Infinity, -}); - -const availableMdRaidsQuery = () => ({ - queryKey: ["storage", "availableMdRaids"], - queryFn: fetchAvailableMdRaids, - staleTime: Infinity, -}); - -const candidateMdRaidsQuery = () => ({ - queryKey: ["storage", "candidateMdRaids"], - queryFn: fetchCandidateMdRaids, - staleTime: Infinity, -}); - -const productParamsQuery = { - queryKey: ["storage", "productParams"], - queryFn: fetchProductParams, - staleTime: Infinity, -}; - -const volumeQuery = (mountPath: string) => ({ - queryKey: ["storage", "volume", mountPath], - queryFn: () => fetchVolume(mountPath), - staleTime: Infinity, -}); - -const volumesQuery = (mountPaths: string[]) => ({ - queryKey: ["storage", "volumes"], - queryFn: () => fetchVolumes(mountPaths), - staleTime: Infinity, -}); - -const actionsQuery = { - queryKey: ["storage", "devices", "actions"], - queryFn: fetchActions, -}; - -const deprecatedQuery = { - queryKey: ["storage", "dirty"], - queryFn: fetchDevicesDirty, -}; - -/** - * Hook that returns the unsolved config. - */ -const useConfig = (options?: QueryHookOptions): config.Config => { - const query = configQuery; - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func(query); - return data; -}; - -/** - * Hook for setting a new config. - */ -const useConfigMutation = () => { - const queryClient = useQueryClient(); - const query = { - mutationFn: async (config: config.Config) => await setConfig(config), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage"] }), - }; - - return useMutation(query); -}; - -/** - * Hook for setting the default config. - */ -const useResetConfigMutation = () => { - const queryClient = useQueryClient(); - const query = { - mutationFn: async () => await resetConfig(), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage"] }), - }; - - return useMutation(query); -}; - -/** - * Hook that returns the list of storage devices for the given scope. - * - * @param scope - "system": devices in the current state of the system; "result": - * devices in the proposal ("stage") - */ -const useDevices = ( - scope: "result" | "system", - options?: QueryHookOptions, -): StorageDevice[] | undefined => { - const query = devicesQuery(scope); - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func(query); - return data; -}; - -/** - * @deprecated Use useProductParams from ~/hooks/storage/product. - * Hook that returns the product parameters (e.g., mount points). - */ -const useProductParams = (options?: QueryHookOptions): ProductParams => { - const func = options?.suspense ? useSuspenseQuery : useQuery; - const { data } = func(productParamsQuery); - return data; -}; - -/** - * Hook that returns the available encryption methods. - * - * @note The ids of the encryption methods reported by product params are different to the - * EncryptionMethod values. This should be fixed at the bakcend size. - */ -const useEncryptionMethods = (options?: QueryHookOptions): apiModel.EncryptionMethod[] => { - const productParams = useProductParams(options); - - const encryptionMethods = React.useMemo((): apiModel.EncryptionMethod[] => { - const conversions = { - luks1: "luks1", - luks2: "luks2", - pervasive_encryption: "pervasiveEncryption", - tpm_fde: "tpmFde", - protected_swap: "protectedSwap", - secure_swap: "secureSwap", - random_swap: "randomSwap", - }; - - const apiMethods = productParams?.encryptionMethods || []; - return apiMethods.map((v) => conversions[v] || "luks2"); - }, [productParams]); - - return encryptionMethods; -}; - -/** - * Hook that returns the volumes for the current product. - */ -const useVolumes = (): Volume[] => { - const product = useProductParams({ suspense: true }); - const mountPoints = ["", ...product.mountPoints]; - const { data } = useSuspenseQuery(volumesQuery(mountPoints)); - return data; -}; - -/** @deprecated Use useVolume from ~/hooks/storage/product. */ -function useVolume(mountPoint: string): Volume { - const volumes = useVolumes(); - const volume = volumes.find((v) => v.mountPath === mountPoint); - const defaultVolume = volumes.find((v) => v.mountPath === ""); - return volume || defaultVolume; -} - -/** - * Hook that returns the actions to perform in the storage devices. - */ -const useActions = (): Action[] => { - const { data } = useSuspenseQuery(actionsQuery); - return data; -}; - -/** - * Hook that returns whether the storage devices are "dirty". - */ -const useDeprecated = () => { - const { isPending, data } = useQuery(deprecatedQuery); - return isPending ? false : data; -}; - -/** - * Hook that listens for changes to the devices dirty property. - */ -const useDeprecatedChanges = () => { - const client = useInstallerClient(); - const queryClient = useQueryClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent(({ type, dirty: value }) => { - if (type === "DevicesDirty") { - queryClient.setQueryData(deprecatedQuery.queryKey, value); - } - }); - }); -}; - -/** - * Hook that reprobes the devices and recalculates the proposal using the current settings. - */ -const useReprobeMutation = () => { - const queryClient = useQueryClient(); - const query = { - mutationFn: async () => { - await reprobe(); - }, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage"] }), - }; - - return useMutation(query); -}; - -export { - productParamsQuery, - apiModelQuery, - availableDrivesQuery, - candidateDrivesQuery, - availableMdRaidsQuery, - candidateMdRaidsQuery, - solveApiModelQuery, - volumeQuery, - useConfig, - useConfigMutation, - useResetConfigMutation, - useDevices, - useEncryptionMethods, - useVolumes, - useVolume, - useActions, - useDeprecated, - useDeprecatedChanges, - useReprobeMutation, -}; diff --git a/web/src/queries/storage/config-model.ts b/web/src/queries/storage/config-model.ts index 610e2cafd9..6a359daaf8 100644 --- a/web/src/queries/storage/config-model.ts +++ b/web/src/queries/storage/config-model.ts @@ -22,11 +22,13 @@ /** @deprecated These hooks will be replaced by new hooks at ~/hooks/storage/ */ -import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; -import { setConfigModel, solveConfigModel } from "~/api/storage"; -import { apiModel, Volume } from "~/api/storage/types"; +import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { putStorageModel, solveStorageModel } from "~/api"; +import { apiModel } from "~/api/storage"; +import { Volume } from "~/api/storage/system"; import { QueryHookOptions } from "~/types/queries"; -import { apiModelQuery, useVolumes } from "~/queries/storage"; +import { storageModelQuery } from "~/hooks/api"; +import { useVolumeTemplates } from "~/hooks/storage/system"; function copyModel(model: apiModel.Config): apiModel.Config { return JSON.parse(JSON.stringify(model)); @@ -111,30 +113,17 @@ function unusedMountPaths(model: apiModel.Config, volumes: Volume[]): string[] { /** @deprecated Use useApiModel from ~/hooks/storage/api-model. */ export function useConfigModel(options?: QueryHookOptions): apiModel.Config { - const query = apiModelQuery; + const query = storageModelQuery(); const func = options?.suspense ? useSuspenseQuery : useQuery; const { data } = func(query); return data; } -/** - * Hook for setting a new config model. - */ -export function useConfigModelMutation() { - const queryClient = useQueryClient(); - const query = { - mutationFn: (model: apiModel.Config) => setConfigModel(model), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage"] }), - }; - - return useMutation(query); -} - /** @deprecated Use useSolvedApiModel from ~/hooks/storage/api-model. */ export function useSolvedConfigModel(model?: apiModel.Config): apiModel.Config | null { const query = useSuspenseQuery({ queryKey: ["storage", "solvedConfigModel", JSON.stringify(model)], - queryFn: () => (model ? solveConfigModel(model) : Promise.resolve(null)), + queryFn: () => (model ? solveStorageModel(model) : Promise.resolve(null)), staleTime: Infinity, }); @@ -149,13 +138,12 @@ export type EncryptionHook = { export function useEncryption(): EncryptionHook { const model = useConfigModel(); - const { mutate } = useConfigModelMutation(); return { encryption: model?.encryption, enable: (method: apiModel.EncryptionMethod, password: string) => - mutate(setEncryption(model, method, password)), - disable: () => mutate(disableEncryption(model)), + putStorageModel(setEncryption(model, method, password)), + disable: () => putStorageModel(disableEncryption(model)), }; } @@ -194,12 +182,11 @@ export type ModelHook = { */ export function useModel(): ModelHook { const model = useConfigModel(); - const { mutate } = useConfigModelMutation(); - const volumes = useVolumes(); + const volumes = useVolumeTemplates(); return { model, - addDrive: (driveName) => mutate(addDrive(model, driveName)), + addDrive: (driveName) => putStorageModel(addDrive(model, driveName)), usedMountPaths: model ? usedMountPaths(model) : [], unusedMountPaths: model ? unusedMountPaths(model, volumes) : [], }; diff --git a/web/src/queries/storage/dasd.ts b/web/src/queries/storage/dasd.ts index c75c2432f6..b42f731e2a 100644 --- a/web/src/queries/storage/dasd.ts +++ b/web/src/queries/storage/dasd.ts @@ -34,7 +34,7 @@ import { useInstallerClient } from "~/context/installer"; import React from "react"; import { hex } from "~/utils"; import { DASDDevice, FormatJob } from "~/types/dasd"; -import { fetchStorageJobs } from "~/api/storage"; +import { getStorageJobs } from "~/api"; /** * Returns a query for retrieving the dasd devices @@ -71,9 +71,7 @@ const useDASDSupported = (): boolean => { const dasdRunningFormatJobsQuery = () => ({ queryKey: ["dasd", "formatJobs", "running"], queryFn: () => - fetchStorageJobs().then((jobs) => - jobs.filter((j) => j.running).map(({ id }) => ({ jobId: id })), - ), + getStorageJobs().then((jobs) => jobs.filter((j) => j.running).map(({ id }) => ({ jobId: id }))), staleTime: 200, }); diff --git a/web/src/queries/system.ts b/web/src/queries/system.ts deleted file mode 100644 index 8d792c5c33..0000000000 --- a/web/src/queries/system.ts +++ /dev/null @@ -1,95 +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 from "react"; -import { tzOffset } from "@date-fns/tz/tzOffset"; -import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; -import { useInstallerClient } from "~/context/installer"; -import { fetchSystem } from "~/api/api"; -import { System } from "~/types/system"; - -const transformLocales = (locales) => - locales.map(({ id, language: name, territory }) => ({ id, name, territory })); - -const tranformKeymaps = (keymaps) => keymaps.map(({ id, description: name }) => ({ id, name })); - -const transformTimezones = (timezones) => - timezones.map(({ id, parts, country }) => { - const utcOffset = tzOffset(id, new Date()); - return { id, parts, country, utcOffset }; - }); - -/** - * Returns a query for retrieving the localization configuration - */ -const systemQuery = () => { - return { - queryKey: ["system"], - queryFn: fetchSystem, - - // FIXME: We previously had separate fetch functions (fetchLocales, - // fetchKeymaps, fetchTimezones) that each applied specific transformations to - // the raw API data, for example, adding `utcOffset` to timezones or - // changing keys to follow a consistent structure (e.g. `id` vs `code`). - // - // Now that we've consolidated these into a single "system" cache, instead of - // individual caches, those transformations are currently missing. While it's - // more efficient to fetch everything in one request, we may still want to apply - // those transformations only once. Ideally, this logic should live outside the - // React Query layer, in a dedicated "state layer" or transformation step, so - // that data remains normalized and consistently shaped for the rest of the app. - - select: (system: System) => ({ - ...system, - l10n: { - locales: transformLocales(system.l10n.locales), - keymaps: tranformKeymaps(system.l10n.keymaps), - timezones: transformTimezones(system.l10n.timezones), - locale: system.l10n.locale, - keypmap: system.l10n.keymap, - timezone: system.l10n.timezone, - }, - }), - }; -}; - -const useSystem = () => { - const { data: system } = useSuspenseQuery(systemQuery()); - return system; -}; - -const useSystemChanges = () => { - const queryClient = useQueryClient(); - const client = useInstallerClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "SystemChanged" && event.scope === "l10n") { - queryClient.invalidateQueries({ queryKey: ["system"] }); - } - }); - }, [client, queryClient]); -}; - -export { useSystem, useSystemChanges }; diff --git a/web/src/test-utils.tsx b/web/src/test-utils.tsx index 6df15d8af3..2b60cf66cf 100644 --- a/web/src/test-utils.tsx +++ b/web/src/test-utils.tsx @@ -119,18 +119,17 @@ const Providers = ({ children, withL10n }) => { } if (withL10n) { - const fetchConfig = async (): Promise => ({ - l10n: { - keymap: "us", - timezone: "Europe/Berlin", - locale: "en_US", - }, - }); + // FIXME + // const fetchConfig = async (): Promise => ({ + // l10n: { + // keymap: "us", + // timezone: "Europe/Berlin", + // locale: "en_US", + // }, + // }); return ( - - {children} - + {children} ); } From 889e415b82face686f13643895e00308c0cc3ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 7 Nov 2025 15:43:21 +0000 Subject: [PATCH 3/9] Rename type --- rust/share/device.storage.schema.json | 2 +- web/src/api/storage/proposal.ts | 8 ++++---- web/src/api/storage/system.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rust/share/device.storage.schema.json b/rust/share/device.storage.schema.json index 39ebbfcee6..07c0918860 100644 --- a/rust/share/device.storage.schema.json +++ b/rust/share/device.storage.schema.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", "$id": "https://github.com/openSUSE/agama/blob/master/rust/share/device.storage.schema.json", - "title": "Storage device", + "title": "Device", "description": "Schema to describe a device both in 'system' and 'proposal'.", "type": "object", "additionalProperties": false, diff --git a/web/src/api/storage/proposal.ts b/web/src/api/storage/proposal.ts index 423e6b8bbc..1b3bdf0c01 100644 --- a/web/src/api/storage/proposal.ts +++ b/web/src/api/storage/proposal.ts @@ -13,7 +13,7 @@ export interface Proposal { /** * Expected layout of the system after the commit phase. */ - devices?: StorageDevice[]; + devices?: Device[]; /** * Sorted list of actions to execute during the commit phase. */ @@ -22,7 +22,7 @@ export interface Proposal { /** * Schema to describe a device both in 'system' and 'proposal'. */ -export interface StorageDevice { +export interface Device { sid: number; name: string; description?: string; @@ -33,9 +33,9 @@ export interface StorageDevice { multipath?: Multipath; partitionTable?: PartitionTable; partition?: Partition; - partitions?: StorageDevice[]; + partitions?: Device[]; volumeGroup?: VolumeGroup; - logicalVolumes?: StorageDevice[]; + logicalVolumes?: Device[]; } export interface Block { start: number; diff --git a/web/src/api/storage/system.ts b/web/src/api/storage/system.ts index d71d396564..d35c77a155 100644 --- a/web/src/api/storage/system.ts +++ b/web/src/api/storage/system.ts @@ -13,7 +13,7 @@ export interface System { /** * All relevant devices on the system */ - devices?: StorageDevice[]; + devices?: Device[]; /** * SIDs of the available drives */ @@ -55,7 +55,7 @@ export interface System { /** * Schema to describe a device both in 'system' and 'proposal'. */ -export interface StorageDevice { +export interface Device { sid: number; name: string; description?: string; @@ -66,9 +66,9 @@ export interface StorageDevice { multipath?: Multipath; partitionTable?: PartitionTable; partition?: Partition; - partitions?: StorageDevice[]; + partitions?: Device[]; volumeGroup?: VolumeGroup; - logicalVolumes?: StorageDevice[]; + logicalVolumes?: Device[]; } export interface Block { start: number; From 9ab927fa52bad90810daaeae4417289d0442d310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 7 Nov 2025 16:24:21 +0000 Subject: [PATCH 4/9] Adapt questions --- web/src/api.ts | 11 ++- web/src/api/question.ts | 42 ++++++++- web/src/api/questions.ts | 44 ---------- .../questions/GenericQuestion.test.tsx | 2 +- .../components/questions/GenericQuestion.tsx | 2 +- .../LoadConfigRetryQuestion.test.tsx | 2 +- .../questions/LoadConfigRetryQuestion.tsx | 2 +- .../questions/LuksActivationQuestion.test.tsx | 2 +- .../questions/PackageErrorQuestion.test.tsx | 2 +- .../questions/PackageErrorQuestion.tsx | 2 +- .../questions/QuestionActions.test.tsx | 2 +- .../components/questions/QuestionActions.tsx | 2 +- .../questions/QuestionWithPassword.test.tsx | 2 +- .../questions/QuestionWithPassword.tsx | 2 +- .../components/questions/Questions.test.tsx | 2 +- web/src/components/questions/Questions.tsx | 10 +-- .../RegistrationCertificateQuestion.test.tsx | 2 +- .../RegistrationCertificateQuestion.tsx | 2 +- .../questions/UnsupportedAutoYaST.test.tsx | 2 +- .../questions/UnsupportedAutoYaST.tsx | 2 +- web/src/hooks/api.ts | 37 ++++++++ web/src/queries/questions.ts | 85 ------------------- web/src/types/questions.ts | 63 -------------- 23 files changed, 108 insertions(+), 216 deletions(-) delete mode 100644 web/src/api/questions.ts delete mode 100644 web/src/queries/questions.ts delete mode 100644 web/src/types/questions.ts diff --git a/web/src/api.ts b/web/src/api.ts index 7c07e5f05c..5274b06da3 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -52,7 +52,7 @@ const getProposal = (): Promise => get("/api/v2/proposal"); const getIssues = (): Promise => get("/api/v2/issues"); -const getQuestions = (): Promise => get("/api/v2/questions"); +const getQuestions = (): Promise => get("/api/v2/questions"); const getStorageModel = (): Promise => get("/api/v2/private/storage_model"); @@ -67,6 +67,14 @@ const putStorageModel = (model: apiModel.Config) => put("/api/v2/private/storage const patchConfig = (config: Config) => patch("/api/v2/config", { update: config }); +const patchQuestion = (question: Question): Response => { + const { + id, + answer: { action, value }, + } = question; + return patch(`/api/v2/questions`, { answer: { id, action, value } }); +}; + const postAction = (action: Action) => post("/api/v2/action", action); const configureL10nAction = (config: L10nSystemConfig) => postAction(configureL10n(config)); @@ -93,6 +101,7 @@ export { putConfig, putStorageModel, patchConfig, + patchQuestion, configureL10nAction, activateStorageAction, probeStorageAction, diff --git a/web/src/api/question.ts b/web/src/api/question.ts index 57db1df22b..0466470426 100644 --- a/web/src/api/question.ts +++ b/web/src/api/question.ts @@ -20,6 +20,44 @@ * find current contact information at www.suse.com. */ -type Question = object; +type Question = { + id: number; + text: string; + class: string; + field: SelectionField | Field; + actions: Action[]; + defaultAction?: string; + data?: { [key: string]: string }; + answer?: Answer; +}; -export type { Question }; +type Field = { + type: FieldType; +}; + +type SelectionField = { + type: FieldType.Select; + options: object; +}; + +type Action = { + id: string; + label: string; +}; + +type Answer = { + action: string; + value?: string; +}; + +enum FieldType { + None = "none", + Password = "password", + String = "string", + Select = "select", +} + +type AnswerCallback = (answeredQuestion: Question) => void; + +export { FieldType }; +export type { Question, Action, AnswerCallback }; diff --git a/web/src/api/questions.ts b/web/src/api/questions.ts deleted file mode 100644 index 4b78e0232e..0000000000 --- a/web/src/api/questions.ts +++ /dev/null @@ -1,44 +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 { get, patch } from "~/http"; -import { Question } from "~/types/questions"; - -/** - * Returns the list of questions - */ -const fetchQuestions = async (): Promise => await get("/api/v2/questions"); - -/** - * Update a questions' answer - * - * The answer is part of the Question object. - */ -const updateAnswer = async (question: Question): Promise => { - const { - id, - answer: { action, value }, - } = question; - await patch(`/api/v2/questions`, { answer: { id, action, value } }); -}; - -export { fetchQuestions, updateAnswer }; diff --git a/web/src/components/questions/GenericQuestion.test.tsx b/web/src/components/questions/GenericQuestion.test.tsx index 2c36b7e36c..98fb17edc9 100644 --- a/web/src/components/questions/GenericQuestion.test.tsx +++ b/web/src/components/questions/GenericQuestion.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { Question, FieldType } from "~/types/questions"; +import { Question, FieldType } from "~/api/question"; import GenericQuestion from "~/components/questions/GenericQuestion"; const question: Question = { diff --git a/web/src/components/questions/GenericQuestion.tsx b/web/src/components/questions/GenericQuestion.tsx index b4f0697ca2..6690473b20 100644 --- a/web/src/components/questions/GenericQuestion.tsx +++ b/web/src/components/questions/GenericQuestion.tsx @@ -23,7 +23,7 @@ import React from "react"; import { Content } from "@patternfly/react-core"; import { Popup } from "~/components/core"; -import { AnswerCallback, Question } from "~/types/questions"; +import { AnswerCallback, Question } from "~/api/question"; import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; diff --git a/web/src/components/questions/LoadConfigRetryQuestion.test.tsx b/web/src/components/questions/LoadConfigRetryQuestion.test.tsx index 5d464a046b..5a2d6c2c06 100644 --- a/web/src/components/questions/LoadConfigRetryQuestion.test.tsx +++ b/web/src/components/questions/LoadConfigRetryQuestion.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { Question, FieldType } from "~/types/questions"; +import { Question, FieldType } from "~/api/question"; import LoadConfigRetryQuestion from "~/components/questions/LoadConfigRetryQuestion"; const question: Question = { diff --git a/web/src/components/questions/LoadConfigRetryQuestion.tsx b/web/src/components/questions/LoadConfigRetryQuestion.tsx index d16c843a9e..37f8508eca 100644 --- a/web/src/components/questions/LoadConfigRetryQuestion.tsx +++ b/web/src/components/questions/LoadConfigRetryQuestion.tsx @@ -23,7 +23,7 @@ import React from "react"; import { Content, Stack } from "@patternfly/react-core"; import { NestedContent, Popup } from "~/components/core"; -import { AnswerCallback, Question } from "~/types/questions"; +import { AnswerCallback, Question } from "~/api/question"; import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; diff --git a/web/src/components/questions/LuksActivationQuestion.test.tsx b/web/src/components/questions/LuksActivationQuestion.test.tsx index 496a235a4d..3da73f7406 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.tsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import { AnswerCallback, Question, FieldType } from "~/types/questions"; +import { AnswerCallback, Question, FieldType } from "~/api/question"; import { InstallationPhase } from "~/types/status"; import { Product } from "~/types/software"; import LuksActivationQuestion from "~/components/questions/LuksActivationQuestion"; diff --git a/web/src/components/questions/PackageErrorQuestion.test.tsx b/web/src/components/questions/PackageErrorQuestion.test.tsx index 5598e453a9..92bcd080f0 100644 --- a/web/src/components/questions/PackageErrorQuestion.test.tsx +++ b/web/src/components/questions/PackageErrorQuestion.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { Question, FieldType } from "~/types/questions"; +import { Question, FieldType } from "~/api/question"; import PackageErrorQuestion from "~/components/questions/PackageErrorQuestion"; const answerFn = jest.fn(); diff --git a/web/src/components/questions/PackageErrorQuestion.tsx b/web/src/components/questions/PackageErrorQuestion.tsx index 5cdc0914ff..fb8804c459 100644 --- a/web/src/components/questions/PackageErrorQuestion.tsx +++ b/web/src/components/questions/PackageErrorQuestion.tsx @@ -24,7 +24,7 @@ import React from "react"; import { Content, Stack } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { Icon } from "~/components/layout"; -import { AnswerCallback, Question } from "~/types/questions"; +import { AnswerCallback, Question } from "~/api/question"; import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; diff --git a/web/src/components/questions/QuestionActions.test.tsx b/web/src/components/questions/QuestionActions.test.tsx index 168f0421e2..83dd8a0fc6 100644 --- a/web/src/components/questions/QuestionActions.test.tsx +++ b/web/src/components/questions/QuestionActions.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import { Question, FieldType } from "~/types/questions"; +import { Question, FieldType } from "~/api/question"; import QuestionActions from "~/components/questions/QuestionActions"; let defaultAction = "sure"; diff --git a/web/src/components/questions/QuestionActions.tsx b/web/src/components/questions/QuestionActions.tsx index 026d247071..a28eea97c2 100644 --- a/web/src/components/questions/QuestionActions.tsx +++ b/web/src/components/questions/QuestionActions.tsx @@ -23,7 +23,7 @@ import React from "react"; import { Popup } from "~/components/core"; import { fork } from "radashi"; -import { Action } from "~/types/questions"; +import { Action } from "~/api/question"; /** * A component for building a Question actions, using the defaultAction diff --git a/web/src/components/questions/QuestionWithPassword.test.tsx b/web/src/components/questions/QuestionWithPassword.test.tsx index 2e1bfc9cdc..5e857ebac3 100644 --- a/web/src/components/questions/QuestionWithPassword.test.tsx +++ b/web/src/components/questions/QuestionWithPassword.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; -import { Question, FieldType } from "~/types/questions"; +import { Question, FieldType } from "~/api/question"; import { Product } from "~/types/software"; import { InstallationPhase } from "~/types/status"; import QuestionWithPassword from "~/components/questions/QuestionWithPassword"; diff --git a/web/src/components/questions/QuestionWithPassword.tsx b/web/src/components/questions/QuestionWithPassword.tsx index 3811baf26c..addd1ddb99 100644 --- a/web/src/components/questions/QuestionWithPassword.tsx +++ b/web/src/components/questions/QuestionWithPassword.tsx @@ -24,7 +24,7 @@ import React, { useState } from "react"; import { Content, Form, FormGroup, Stack } from "@patternfly/react-core"; import { Icon } from "~/components/layout"; import { InstallerOptions, PasswordInput, Popup } from "~/components/core"; -import { AnswerCallback, Question } from "~/types/questions"; +import { AnswerCallback, Question } from "~/api/question"; import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; diff --git a/web/src/components/questions/Questions.test.tsx b/web/src/components/questions/Questions.test.tsx index 51e66474af..125017079a 100644 --- a/web/src/components/questions/Questions.test.tsx +++ b/web/src/components/questions/Questions.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { installerRender, plainRender } from "~/test-utils"; -import { Question, FieldType } from "~/types/questions"; +import { Question, FieldType } from "~/api/question"; import Questions from "~/components/questions/Questions"; import * as GenericQuestionComponent from "~/components/questions/GenericQuestion"; diff --git a/web/src/components/questions/Questions.tsx b/web/src/components/questions/Questions.tsx index 30c5f3ed42..7dd2aeddcd 100644 --- a/web/src/components/questions/Questions.tsx +++ b/web/src/components/questions/Questions.tsx @@ -28,19 +28,19 @@ import PackageErrorQuestion from "~/components/questions/PackageErrorQuestion"; import UnsupportedAutoYaST from "~/components/questions/UnsupportedAutoYaST"; import RegistrationCertificateQuestion from "~/components/questions/RegistrationCertificateQuestion"; import LoadConfigRetryQuestion from "~/components/questions/LoadConfigRetryQuestion"; -import { useQuestions, useQuestionsConfig, useQuestionsChanges } from "~/queries/questions"; -import { AnswerCallback, FieldType } from "~/types/questions"; +import { useQuestions, useQuestionsChanges } from "~/hooks/api"; +import { patchQuestion } from "~/api"; +import { AnswerCallback, FieldType } from "~/api/question"; export default function Questions(): React.ReactNode { useQuestionsChanges(); const allQuestions = useQuestions(); - const questionsConfig = useQuestionsConfig(); const pendingQuestions = allQuestions.filter((q) => !q.answer); if (pendingQuestions.length === 0) return null; - const answerQuestion: AnswerCallback = (answeredQuestion) => - questionsConfig.mutate(answeredQuestion); + const answerQuestion: AnswerCallback = async (answeredQuestion) => + await patchQuestion(answeredQuestion); // Renders the first pending question const [currentQuestion] = pendingQuestions; diff --git a/web/src/components/questions/RegistrationCertificateQuestion.test.tsx b/web/src/components/questions/RegistrationCertificateQuestion.test.tsx index 238e34a99f..3ce6932a52 100644 --- a/web/src/components/questions/RegistrationCertificateQuestion.test.tsx +++ b/web/src/components/questions/RegistrationCertificateQuestion.test.tsx @@ -23,7 +23,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { Question, FieldType } from "~/types/questions"; +import { Question, FieldType } from "~/api/question"; import RegistrationCertificateQuestion from "~/components/questions/RegistrationCertificateQuestion"; const question: Question = { diff --git a/web/src/components/questions/RegistrationCertificateQuestion.tsx b/web/src/components/questions/RegistrationCertificateQuestion.tsx index 785553cf61..daece4fe58 100644 --- a/web/src/components/questions/RegistrationCertificateQuestion.tsx +++ b/web/src/components/questions/RegistrationCertificateQuestion.tsx @@ -32,7 +32,7 @@ import { StackItem, } from "@patternfly/react-core"; import { Popup } from "~/components/core"; -import { AnswerCallback, Question } from "~/types/questions"; +import { AnswerCallback, Question } from "~/api/question"; import QuestionActions from "~/components/questions/QuestionActions"; import { _ } from "~/i18n"; diff --git a/web/src/components/questions/UnsupportedAutoYaST.test.tsx b/web/src/components/questions/UnsupportedAutoYaST.test.tsx index 82e63ccbdf..039b0c526e 100644 --- a/web/src/components/questions/UnsupportedAutoYaST.test.tsx +++ b/web/src/components/questions/UnsupportedAutoYaST.test.tsx @@ -22,7 +22,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { AnswerCallback, Question, FieldType } from "~/types/questions"; +import { AnswerCallback, Question, FieldType } from "~/api/question"; import UnsupportedAutoYaST from "~/components/questions/UnsupportedAutoYaST"; import { plainRender } from "~/test-utils"; diff --git a/web/src/components/questions/UnsupportedAutoYaST.tsx b/web/src/components/questions/UnsupportedAutoYaST.tsx index 21c62aaceb..227f3ccec9 100644 --- a/web/src/components/questions/UnsupportedAutoYaST.tsx +++ b/web/src/components/questions/UnsupportedAutoYaST.tsx @@ -30,7 +30,7 @@ import { ListVariant, Stack, } from "@patternfly/react-core"; -import { AnswerCallback, Question } from "~/types/questions"; +import { AnswerCallback, Question } from "~/api/question"; import { Page, Popup } from "~/components/core"; import QuestionActions from "~/components/questions/QuestionActions"; import { sprintf } from "sprintf-js"; diff --git a/web/src/hooks/api.ts b/web/src/hooks/api.ts index 77f322e875..8ea8c1c985 100644 --- a/web/src/hooks/api.ts +++ b/web/src/hooks/api.ts @@ -28,12 +28,14 @@ import { getExtendedConfig, solveStorageModel, getStorageModel, + getQuestions, } from "~/api"; import { useInstallerClient } from "~/context/installer"; import { System } from "~/api/system"; import { Proposal } from "~/api/proposal"; import { Config } from "~/api/config"; import { apiModel } from "~/api/storage"; +import { Question } from "~/api/question"; import { QueryHookOptions } from "~/types/queries"; const systemQuery = () => ({ @@ -105,6 +107,39 @@ function useExtendedConfig(options?: QueryHookOptions): Config | null { return func(query)?.data; } +const questionsQuery = () => ({ + queryKey: ["questions"], + queryFn: getQuestions, +}); + +const useQuestions = (options?: QueryHookOptions): Question[] => { + const func = options?.suspense ? useSuspenseQuery : useQuery; + return func(questionsQuery())?.data || []; +}; + +const useQuestionsChanges = () => { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + + React.useEffect(() => { + if (!client) return; + + return client.onEvent((event) => { + if (event.type === "QuestionAdded" || event.type === "QuestionAnswered") { + queryClient.invalidateQueries({ queryKey: ["questions"] }); + } + }); + }, [client, queryClient]); + + React.useEffect(() => { + if (!client) return; + + return client.onConnect(() => { + queryClient.invalidateQueries({ queryKey: ["questions"] }); + }); + }, [client, queryClient]); +}; + const storageModelQuery = () => ({ queryKey: ["storageModel"], queryFn: getStorageModel, @@ -141,6 +176,8 @@ export { useProposal, useProposalChanges, useExtendedConfig, + useQuestions, + useQuestionsChanges, useStorageModel, useSolvedStorageModel, }; diff --git a/web/src/queries/questions.ts b/web/src/queries/questions.ts deleted file mode 100644 index 20c25bc348..0000000000 --- a/web/src/queries/questions.ts +++ /dev/null @@ -1,85 +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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useInstallerClient } from "~/context/installer"; -import { Question } from "~/types/questions"; -import { fetchQuestions, updateAnswer } from "~/api/questions"; - -/** - * Query to retrieve questions - */ -const questionsQuery = () => ({ - queryKey: ["questions"], - queryFn: fetchQuestions, -}); - -/** - * Hook that builds a mutation given question, allowing to answer it - - * TODO: improve/simplify it once the backend API is improved. - */ -const useQuestionsConfig = () => { - const queryClient = useQueryClient(); - const query = { - mutationFn: (question: Question) => updateAnswer(question), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ["questions"] }), - }; - return useMutation(query); -}; - -/** - * Hook for listening questions changes and performing proper invalidations - */ -const useQuestionsChanges = () => { - const queryClient = useQueryClient(); - const client = useInstallerClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "QuestionAdded" || event.type === "QuestionAnswered") { - queryClient.invalidateQueries({ queryKey: ["questions"] }); - } - }); - }, [client, queryClient]); - - React.useEffect(() => { - if (!client) return; - - return client.onConnect(() => { - queryClient.invalidateQueries({ queryKey: ["questions"] }); - }); - }, [client, queryClient]); -}; - -/** - * Hook for retrieving available questions - */ -const useQuestions = () => { - const { data: questions, isPending } = useQuery(questionsQuery()); - return isPending ? [] : questions; -}; - -export { questionsQuery, useQuestions, useQuestionsConfig, useQuestionsChanges }; diff --git a/web/src/types/questions.ts b/web/src/types/questions.ts deleted file mode 100644 index d3f799e7a3..0000000000 --- a/web/src/types/questions.ts +++ /dev/null @@ -1,63 +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 Question = { - id: number; - text: string; - class: string; - field: SelectionField | Field; - actions: Action[]; - defaultAction?: string; - data?: { [key: string]: string }; - answer?: Answer; -}; - -type Field = { - type: FieldType; -}; - -type SelectionField = { - type: FieldType.Select; - options: object; -}; - -type Action = { - id: string; - label: string; -}; - -type Answer = { - action: string; - value?: string; -}; - -enum FieldType { - None = "none", - Password = "password", - String = "string", - Select = "select", -} - -type AnswerCallback = (answeredQuestion: Question) => void; - -export { FieldType }; -export type { Question, Action, AnswerCallback }; From 5d283ee037929fa458daf933214192c5f650f9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 10 Nov 2025 11:35:17 +0000 Subject: [PATCH 5/9] Adapt issues --- web/src/App.tsx | 3 +- web/src/api.ts | 4 +- web/src/api/hostname.ts | 2 + web/src/api/issue.ts | 76 +++++++- web/src/api/issues.ts | 37 ---- web/src/api/storage/dasd.ts | 2 + web/src/api/storage/devices.ts | 178 ------------------ web/src/api/storage/iscsi.ts | 81 +++++++- web/src/api/storage/types/checks.ts | 107 ----------- web/src/api/storage/zfcp.ts | 2 + .../components/core/InstallButton.test.tsx | 2 +- web/src/components/core/InstallButton.tsx | 5 +- web/src/components/core/InstallerOptions.tsx | 4 +- web/src/components/core/IssuesAlert.test.tsx | 2 +- web/src/components/core/IssuesAlert.tsx | 2 +- web/src/components/core/IssuesDrawer.test.tsx | 2 +- web/src/components/core/IssuesDrawer.tsx | 6 +- .../overview/StorageSection.test.tsx | 2 +- .../components/overview/StorageSection.tsx | 5 +- .../product/ProductRegistrationAlert.test.tsx | 6 +- .../product/ProductRegistrationAlert.tsx | 5 +- web/src/components/software/SoftwarePage.tsx | 4 +- .../components/storage/FixableConfigInfo.tsx | 4 +- .../storage/ProposalFailedInfo.test.tsx | 2 +- .../components/storage/ProposalFailedInfo.tsx | 9 +- .../components/storage/ProposalPage.test.tsx | 2 +- web/src/components/storage/ProposalPage.tsx | 10 +- web/src/components/users/UsersPage.tsx | 4 +- web/src/hooks/api.ts | 64 ++++++- web/src/hooks/storage/issues.ts | 52 +++++ web/src/hooks/storage/system.ts | 12 ++ web/src/queries/issues.ts | 95 ---------- web/src/types/issues.ts | 95 ---------- 33 files changed, 326 insertions(+), 560 deletions(-) delete mode 100644 web/src/api/issues.ts delete mode 100644 web/src/api/storage/devices.ts delete mode 100644 web/src/api/storage/types/checks.ts create mode 100644 web/src/hooks/storage/issues.ts delete mode 100644 web/src/queries/issues.ts delete mode 100644 web/src/types/issues.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index b6f4464d5a..7db7370ed3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -24,8 +24,7 @@ import React, { useEffect } from "react"; import { Navigate, Outlet, useLocation } from "react-router-dom"; import { Loading } from "~/components/layout"; import { useProduct, useProductChanges } from "~/queries/software"; -import { useSystemChanges, useProposalChanges } from "~/hooks/api"; -import { useIssuesChanges } from "~/queries/issues"; +import { useSystemChanges, useProposalChanges, useIssuesChanges } from "~/hooks/api"; import { useInstallerStatus, useInstallerStatusChanges } from "~/queries/status"; import { ROOT, PRODUCT } from "~/routes/paths"; import { InstallationPhase } from "~/types/status"; diff --git a/web/src/api.ts b/web/src/api.ts index 5274b06da3..9787c969c2 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -23,7 +23,7 @@ import { get, patch, post, put } from "~/http"; import { apiModel } from "~/api/storage"; import { Config } from "~/api/config"; -import { Issue } from "~/api/issue"; +import { IssuesMap } from "~/api/issue"; import { Proposal } from "~/api/proposal"; import { Question } from "~/api/question"; import { Status } from "~/api/status"; @@ -50,7 +50,7 @@ const getSystem = (): Promise => get("/api/v2/system"); const getProposal = (): Promise => get("/api/v2/proposal"); -const getIssues = (): Promise => get("/api/v2/issues"); +const getIssues = (): Promise => get("/api/v2/issues"); const getQuestions = (): Promise => get("/api/v2/questions"); diff --git a/web/src/api/hostname.ts b/web/src/api/hostname.ts index 902b160359..d119ebaf0b 100644 --- a/web/src/api/hostname.ts +++ b/web/src/api/hostname.ts @@ -20,6 +20,8 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { get, put } from "~/http"; import { Hostname } from "~/types/hostname"; diff --git a/web/src/api/issue.ts b/web/src/api/issue.ts index 41d5691f46..5efb26ecd9 100644 --- a/web/src/api/issue.ts +++ b/web/src/api/issue.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2025] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -20,6 +20,76 @@ * find current contact information at www.suse.com. */ -type Issue = object; +/** + * Known scopes for issues. + */ +type IssuesScope = "localization" | "product" | "software" | "storage" | "users" | "iscsi"; + +/** + * Source of the issue + * + * Which is the origin of the issue (the system, the configuration or unknown). + */ +enum IssueSource { + /** Unknown source (it is kind of a fallback value) */ + Unknown = "unknown", + /** An unexpected situation in the system (e.g., missing device). */ + System = "system", + /** Wrong or incomplete configuration (e.g., an authentication mechanism is not set) */ + Config = "config", +} + +/** + * Issue severity + * + * It indicates how severe the problem is. + */ +enum IssueSeverity { + /** Just a warning, the installation can start */ + Warn = "warn", + /** An important problem that makes the installation not possible */ + Error = "error", +} + +/** + * Pre-installation issue as they come from the API. + */ +type ApiIssue = { + /** Issue description */ + description: string; + /** Issue kind **/ + kind: string; + /** Issue details */ + details?: string; + /** Where the issue comes from */ + source: IssueSource; + /** How severe is the issue */ + severity: IssueSeverity; +}; + +/** + * Issues grouped by scope as they come from the API. + */ +type IssuesMap = { + localization?: ApiIssue[]; + software?: ApiIssue[]; + product?: ApiIssue[]; + storage?: ApiIssue[]; + iscsi?: ApiIssue[]; + users?: ApiIssue[]; +}; + +/** + * Pre-installation issue augmented with the scope. + */ +type Issue = ApiIssue & { scope: IssuesScope }; + +/** + * Validation error + */ +type ValidationError = { + message: string; +}; -export type { Issue }; +export { IssueSource, IssueSeverity }; +export type { ApiIssue, IssuesMap, IssuesScope, Issue, ValidationError }; diff --git a/web/src/api/issues.ts b/web/src/api/issues.ts deleted file mode 100644 index 888febd137..0000000000 --- a/web/src/api/issues.ts +++ /dev/null @@ -1,37 +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 { get } from "~/http"; -import { Issue, IssuesMap, IssuesScope } from "~/types/issues"; - -/** - * Return the issues of the given scope. - */ -const fetchIssues = async (): Promise => { - const issues = (await get(`/api/v2/issues`)) as IssuesMap; - return Object.keys(issues).reduce((all: Issue[], key: IssuesScope) => { - const scoped = issues[key].map((i) => ({ ...i, scope: key })); - return all.concat(scoped); - }, []); -}; - -export { fetchIssues }; diff --git a/web/src/api/storage/dasd.ts b/web/src/api/storage/dasd.ts index 26c5767a1e..d9317970a3 100644 --- a/web/src/api/storage/dasd.ts +++ b/web/src/api/storage/dasd.ts @@ -20,6 +20,8 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { post, get, put } from "~/http"; import { DASDDevice } from "~/types/dasd"; diff --git a/web/src/api/storage/devices.ts b/web/src/api/storage/devices.ts deleted file mode 100644 index b6a6d5fd63..0000000000 --- a/web/src/api/storage/devices.ts +++ /dev/null @@ -1,178 +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 { get } from "~/http"; -import { - Component, - Device, - DevicesDirtyResponse, - Drive, - Filesystem, - LvmVg, - Md, - Multipath, - Partition, - PartitionTable, - Raid, -} from "./types"; -import { StorageDevice } from "~/types/storage"; - -/** - * @fixme Use a transformation instead of building the devices as part of the fetch function, see - * https://tkdodo.eu/blog/react-query-data-transformations. - * - * Returns the list of devices in the given scope - * - * @param scope - "system": devices in the current state of the system; "result": - * devices in the proposal ("stage") - */ -const fetchDevices = async (scope: "result" | "system") => { - const buildDevice = (jsonDevice: Device, jsonDevices: Device[]) => { - const buildDefaultDevice = (): StorageDevice => { - return { - sid: 0, - name: "", - description: "", - isDrive: false, - type: "drive", - }; - }; - - const buildCollectionFromNames = (names: string[]): StorageDevice[] => { - return names.map((name) => ({ ...buildDefaultDevice(), name })); - }; - - const buildCollection = (sids: number[], jsonDevices: Device[]): StorageDevice[] => { - if (sids === null || sids === undefined) return []; - - // Some devices might not be found because they are not exported, for example, the members of - // a BIOS RAID, see bsc#1237803. - return sids - .map((sid) => jsonDevices.find((dev) => dev.deviceInfo?.sid === sid)) - .filter((jsonDevice) => jsonDevice) - .map((jsonDevice) => buildDevice(jsonDevice, jsonDevices)); - }; - - const addDriveInfo = (device: StorageDevice, info: Drive) => { - device.isDrive = true; - device.type = info.type; - device.vendor = info.vendor; - device.model = info.model; - device.driver = info.driver; - device.bus = info.bus; - device.busId = info.busId; - device.transport = info.transport; - device.sdCard = info.info.sdCard; - device.dellBOSS = info.info.dellBOSS; - }; - - const addRaidInfo = (device: StorageDevice, info: Raid) => { - device.devices = buildCollectionFromNames(info.devices); - }; - - const addMultipathInfo = (device: StorageDevice, info: Multipath) => { - device.wires = buildCollectionFromNames(info.wires); - }; - - const addMDInfo = (device: StorageDevice, info: Md) => { - device.type = "md"; - device.level = info.level; - device.uuid = info.uuid; - device.devices = buildCollection(info.devices, jsonDevices); - }; - - const addPartitionInfo = (device: StorageDevice, info: Partition) => { - device.type = "partition"; - device.isEFI = info.efi; - }; - - const addVgInfo = (device: StorageDevice, info: LvmVg) => { - device.type = "lvmVg"; - device.size = info.size; - device.physicalVolumes = buildCollection(info.physicalVolumes, jsonDevices); - device.logicalVolumes = buildCollection(info.logicalVolumes, jsonDevices); - }; - - const addLvInfo = (device: StorageDevice) => { - device.type = "lvmLv"; - }; - - const addPTableInfo = (device: StorageDevice, tableInfo: PartitionTable) => { - const partitions = buildCollection(tableInfo.partitions, jsonDevices); - device.partitionTable = { - type: tableInfo.type, - partitions, - unpartitionedSize: device.size - partitions.reduce((s, p) => s + p.size, 0), - unusedSlots: tableInfo.unusedSlots.map((s) => Object.assign({}, s)), - }; - }; - - const addFilesystemInfo = (device: StorageDevice, filesystemInfo: Filesystem) => { - const buildMountPath = (path: string) => (path.length > 0 ? path : undefined); - const buildLabel = (label: string) => (label.length > 0 ? label : undefined); - device.filesystem = { - sid: filesystemInfo.sid, - type: filesystemInfo.type, - mountPath: buildMountPath(filesystemInfo.mountPath), - label: buildLabel(filesystemInfo.label), - }; - }; - - const addComponentInfo = (device: StorageDevice, info: Component) => { - device.component = { - type: info.type, - deviceNames: info.deviceNames, - }; - }; - - const device = buildDefaultDevice(); - - const process = (jsonProperty: string, method: Function) => { - const info = jsonDevice[jsonProperty]; - if (info === undefined || info === null) return; - - method(device, info); - }; - - process("deviceInfo", Object.assign); - process("drive", addDriveInfo); - process("raid", addRaidInfo); - process("multipath", addMultipathInfo); - process("md", addMDInfo); - process("blockDevice", Object.assign); - process("partition", addPartitionInfo); - process("lvmVg", addVgInfo); - process("lvmLv", addLvInfo); - process("partitionTable", addPTableInfo); - process("filesystem", addFilesystemInfo); - process("component", addComponentInfo); - - return device; - }; - - const jsonDevices: Device[] = await get(`/api/storage/devices/${scope}`); - return jsonDevices.map((d) => buildDevice(d, jsonDevices)); -}; - -const fetchDevicesDirty = (): Promise => get("/api/storage/devices/dirty"); - -export { fetchDevices, fetchDevicesDirty }; diff --git a/web/src/api/storage/iscsi.ts b/web/src/api/storage/iscsi.ts index 6c10d4dfc1..58fb6b4dcf 100644 --- a/web/src/api/storage/iscsi.ts +++ b/web/src/api/storage/iscsi.ts @@ -20,8 +20,87 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { del, get, patch, post } from "~/http"; -import { ISCSIInitiator, ISCSINode } from "~/api/storage/types"; + +export type ISCSIAuth = { + /** + * Password for authentication by target. + */ + password?: string | null; + /** + * Password for authentication by initiator. + */ + reverse_password?: string | null; + /** + * Username for authentication by initiator. + */ + reverse_username?: string | null; + /** + * Username for authentication by target. + */ + username?: string | null; +}; + +export type ISCSIInitiator = { + ibft: boolean; + name: string; +}; + +/** + * ISCSI node + */ +export type ISCSINode = { + /** + * Target IP address (in string-like form). + */ + address: string; + /** + * Whether the node is connected (there is a session). + */ + connected: boolean; + /** + * Whether the node was initiated by iBFT + */ + ibft: boolean; + /** + * Artificial ID to match it against the D-Bus backend. + */ + id: number; + /** + * Interface name. + */ + interface: string; + /** + * Target port. + */ + port: number; + /** + * Startup status (TODO: document better) + */ + startup: string; + /** + * Target name. + */ + target: string; +}; + +export type InitiatorParams = { + /** + * iSCSI initiator name. + */ + name: string; +}; + +export type LoginParams = ISCSIAuth & { + /** + * Startup value. + */ + startup: string; +}; + +export type LoginResult = "Success" | "InvalidStartup" | "Failed"; const ISCSI_NODES_NAMESPACE = "/api/storage/iscsi/nodes"; diff --git a/web/src/api/storage/types/checks.ts b/web/src/api/storage/types/checks.ts deleted file mode 100644 index cf3dab7393..0000000000 --- a/web/src/api/storage/types/checks.ts +++ /dev/null @@ -1,107 +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 * as config from "../config"; - -// Type guards. - -export function isFormattedDrive(drive: config.DriveElement): drive is config.NonPartitionedDrive { - return "filesystem" in drive; -} - -export function isPartitionedDrive(drive: config.DriveElement): drive is config.PartitionedDrive { - return !("filesystem" in drive); -} - -export function isSimpleSearchAll(search: config.SearchElement): search is config.SimpleSearchAll { - return search === "*"; -} - -export function isSimpleSearchByName( - search: config.SearchElement, -): search is config.SimpleSearchByName { - return !isSimpleSearchAll(search) && typeof search === "string"; -} - -export function isAdvancedSearch(search: config.SearchElement): search is config.AdvancedSearch { - return !isSimpleSearchAll(search) && !isSimpleSearchByName(search); -} - -export function isPartitionToDelete( - partition: config.PartitionElement, -): partition is config.PartitionToDelete { - return "delete" in partition; -} - -export function isPartitionToDeleteIfNeeded( - partition: config.PartitionElement, -): partition is config.PartitionToDeleteIfNeeded { - return "deleteIfNeeded" in partition; -} - -export function isRegularPartition( - partition: config.PartitionElement, -): partition is config.RegularPartition { - if ("generate" in partition) return false; - - return !isPartitionToDelete(partition) && !isPartitionToDeleteIfNeeded(partition); -} - -export function isFilesystemTypeAny( - fstype: config.FilesystemType, -): fstype is config.FilesystemTypeAny { - return typeof fstype === "string"; -} - -export function isFilesystemTypeBtrfs( - fstype: config.FilesystemType, -): fstype is config.FilesystemTypeBtrfs { - return !isFilesystemTypeAny(fstype) && "btrfs" in fstype; -} - -export function isSizeCurrent(size: config.SizeValueWithCurrent): size is config.SizeCurrent { - return size === "current"; -} - -export function isSizeBytes( - size: config.Size | config.SizeValueWithCurrent, -): size is config.SizeBytes { - return typeof size === "number"; -} - -export function isSizeString( - size: config.Size | config.SizeValueWithCurrent, -): size is config.SizeString { - return typeof size === "string" && size !== "current"; -} - -export function isSizeValue(size: config.Size): size is config.SizeValue { - return isSizeBytes(size) || isSizeString(size); -} - -export function isSizeTuple(size: config.Size): size is config.SizeTuple { - return Array.isArray(size); -} - -export function isSizeRange(size: config.Size): size is config.SizeRange { - return !isSizeTuple(size) && typeof size === "object"; -} diff --git a/web/src/api/storage/zfcp.ts b/web/src/api/storage/zfcp.ts index 72dccb0719..bbf528d3f8 100644 --- a/web/src/api/storage/zfcp.ts +++ b/web/src/api/storage/zfcp.ts @@ -20,6 +20,8 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { post, get } from "~/http"; import { ZFCPDisk, ZFCPController, ZFCPConfig } from "~/types/zfcp"; diff --git a/web/src/components/core/InstallButton.test.tsx b/web/src/components/core/InstallButton.test.tsx index f953801184..d352ba3adf 100644 --- a/web/src/components/core/InstallButton.test.tsx +++ b/web/src/components/core/InstallButton.test.tsx @@ -25,7 +25,7 @@ import { screen, waitFor, within } from "@testing-library/react"; import { installerRender, mockRoutes } from "~/test-utils"; import { InstallButton } from "~/components/core"; import { PRODUCT, ROOT } from "~/routes/paths"; -import { Issue, IssueSeverity, IssueSource } from "~/types/issues"; +import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; const mockStartInstallationFn = jest.fn(); let mockIssuesList: Issue[]; diff --git a/web/src/components/core/InstallButton.tsx b/web/src/components/core/InstallButton.tsx index 2323a5d6cf..2c146ce648 100644 --- a/web/src/components/core/InstallButton.tsx +++ b/web/src/components/core/InstallButton.tsx @@ -24,8 +24,7 @@ import React, { useId, useState } from "react"; import { Button, ButtonProps, Stack, Tooltip, TooltipProps } from "@patternfly/react-core"; import { Popup } from "~/components/core"; import { startInstallation } from "~/api/manager"; -import { useAllIssues } from "~/queries/issues"; -import { IssueSeverity } from "~/types/issues"; +import { useIssues } from "~/hooks/api"; import { useLocation } from "react-router-dom"; import { SIDE_PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; @@ -79,7 +78,7 @@ const InstallButton = ( ) => { const labelId = useId(); const tooltipId = useId(); - const issues = useAllIssues().filter((i) => i.severity === IssueSeverity.Error); + const issues = useIssues({ suspense: true }); const [isOpen, setIsOpen] = useState(false); const location = useLocation(); const hasIssues = !isEmpty(issues); diff --git a/web/src/components/core/InstallerOptions.tsx b/web/src/components/core/InstallerOptions.tsx index 34b680bcc2..4effa05fea 100644 --- a/web/src/components/core/InstallerOptions.tsx +++ b/web/src/components/core/InstallerOptions.tsx @@ -90,7 +90,7 @@ const LangaugeFormInput = ({ value, onChange }: SelectProps) => ( const KeyboardFormInput = ({ value, onChange }: SelectProps) => { const { l10n: { keymaps }, - } = useSystem(); + } = useSystem({ suspense: true }); if (!localConnection()) { return ( @@ -554,7 +554,7 @@ export default function InstallerOptions({ const location = useLocation(); const { l10n: { locales }, - } = useSystem(); + } = useSystem({ suspense: true }); const { language, keymap, changeLanguage, changeKeymap } = useInstallerL10n(); const { phase } = useInstallerStatus({ suspense: true }); const { selectedProduct } = useProduct({ suspense: true }); diff --git a/web/src/components/core/IssuesAlert.test.tsx b/web/src/components/core/IssuesAlert.test.tsx index 494a972cbb..4c198f71a2 100644 --- a/web/src/components/core/IssuesAlert.test.tsx +++ b/web/src/components/core/IssuesAlert.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { IssuesAlert } from "~/components/core"; -import { Issue, IssueSeverity, IssueSource } from "~/types/issues"; +import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; import { SOFTWARE } from "~/routes/paths"; describe("IssueAlert", () => { diff --git a/web/src/components/core/IssuesAlert.tsx b/web/src/components/core/IssuesAlert.tsx index 18c635aefd..ff46d6e4de 100644 --- a/web/src/components/core/IssuesAlert.tsx +++ b/web/src/components/core/IssuesAlert.tsx @@ -23,7 +23,7 @@ import React from "react"; import { Alert, List, ListItem } from "@patternfly/react-core"; import { _ } from "~/i18n"; -import { Issue } from "~/types/issues"; +import { Issue } from "~/api/issue"; import Link from "./Link"; import { PATHS } from "~/routes/software"; diff --git a/web/src/components/core/IssuesDrawer.test.tsx b/web/src/components/core/IssuesDrawer.test.tsx index 20c52bab18..427e049446 100644 --- a/web/src/components/core/IssuesDrawer.test.tsx +++ b/web/src/components/core/IssuesDrawer.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import { InstallationPhase } from "~/types/status"; -import { Issue, IssueSeverity, IssueSource } from "~/types/issues"; +import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; import IssuesDrawer from "./IssuesDrawer"; let phase = InstallationPhase.Config; diff --git a/web/src/components/core/IssuesDrawer.tsx b/web/src/components/core/IssuesDrawer.tsx index 493cd00f1a..ee495ff09d 100644 --- a/web/src/components/core/IssuesDrawer.tsx +++ b/web/src/components/core/IssuesDrawer.tsx @@ -30,9 +30,9 @@ import { Stack, } from "@patternfly/react-core"; import Link from "~/components/core/Link"; -import { useAllIssues } from "~/queries/issues"; +import { useIssues } from "~/hooks/api"; import { useInstallerStatus } from "~/queries/status"; -import { IssueSeverity } from "~/types/issues"; +import { IssueSeverity } from "~/api/issue"; import { InstallationPhase } from "~/types/status"; import { _ } from "~/i18n"; @@ -40,7 +40,7 @@ import { _ } from "~/i18n"; * Drawer for displaying installation issues */ const IssuesDrawer = forwardRef(({ onClose }: { onClose: () => void }, ref) => { - const issues = useAllIssues().filter((i) => i.severity === IssueSeverity.Error); + const issues = useIssues().filter((i) => i.severity === IssueSeverity.Error); const { phase } = useInstallerStatus({ suspense: true }); // FIXME: share below headers with navigation menu diff --git a/web/src/components/overview/StorageSection.test.tsx b/web/src/components/overview/StorageSection.test.tsx index 39f47e7b14..bc452cb09f 100644 --- a/web/src/components/overview/StorageSection.test.tsx +++ b/web/src/components/overview/StorageSection.test.tsx @@ -24,7 +24,7 @@ import React from "react"; import { screen } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { StorageSection } from "~/components/overview"; -import { IssueSeverity, IssueSource } from "~/types/issues"; +import { IssueSeverity, IssueSource } from "~/api/issue"; let mockModel = { drives: [], diff --git a/web/src/components/overview/StorageSection.tsx b/web/src/components/overview/StorageSection.tsx index 28cf116d59..25e2096cf6 100644 --- a/web/src/components/overview/StorageSection.tsx +++ b/web/src/components/overview/StorageSection.tsx @@ -23,9 +23,8 @@ import React from "react"; import { Content } from "@patternfly/react-core"; import { deviceLabel } from "~/components/storage/utils"; -import { useAvailableDevices, useDevices } from "~/hooks/storage/system"; +import { useAvailableDevices, useDevices, useIssues } from "~/hooks/storage/system"; import { useConfigModel } from "~/queries/storage/config-model"; -import { useSystemErrors } from "~/queries/issues"; import { storage } from "~/api/system"; import { apiModel } from "~/api/storage"; import { _ } from "~/i18n"; @@ -92,7 +91,7 @@ const ModelSummary = ({ model }: { model: apiModel.Config }): React.ReactNode => const NoModelSummary = (): React.ReactNode => { const availableDevices = useAvailableDevices(); - const systemErrors = useSystemErrors("storage"); + const systemErrors = useIssues(); const hasDisks = !!availableDevices.length; const hasResult = !systemErrors.length; diff --git a/web/src/components/product/ProductRegistrationAlert.test.tsx b/web/src/components/product/ProductRegistrationAlert.test.tsx index fc93a796c7..ef1e01049f 100644 --- a/web/src/components/product/ProductRegistrationAlert.test.tsx +++ b/web/src/components/product/ProductRegistrationAlert.test.tsx @@ -26,9 +26,9 @@ import { installerRender, mockRoutes } from "~/test-utils"; import ProductRegistrationAlert from "./ProductRegistrationAlert"; import { Product } from "~/types/software"; import { useProduct } from "~/queries/software"; -import { useIssues } from "~/queries/issues"; +import { useScopeIssues } from "~/hooks/issues"; import { PRODUCT, REGISTRATION, ROOT } from "~/routes/paths"; -import { Issue, IssueSeverity, IssueSource } from "~/types/issues"; +import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; const tw: Product = { id: "Tumbleweed", @@ -66,7 +66,7 @@ const registrationIssue: Issue = { jest.mock("~/queries/issues", () => ({ ...jest.requireActual("~/queries/issues"), - useIssues: (): ReturnType => issues, + useIssues: (): ReturnType => issues, })); const rendersNothingInSomePaths = () => { diff --git a/web/src/components/product/ProductRegistrationAlert.tsx b/web/src/components/product/ProductRegistrationAlert.tsx index 1e6660670d..1c7bbbcdd4 100644 --- a/web/src/components/product/ProductRegistrationAlert.tsx +++ b/web/src/components/product/ProductRegistrationAlert.tsx @@ -28,7 +28,7 @@ import { useProduct } from "~/queries/software"; import { REGISTRATION, SIDE_PATHS } from "~/routes/paths"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { useIssues } from "~/queries/issues"; +import { useScopeIssues } from "~/hooks/api"; const LinkToRegistration = ({ text }: { text: string }) => { const location = useLocation(); @@ -45,7 +45,8 @@ const LinkToRegistration = ({ text }: { text: string }) => { export default function ProductRegistrationAlert() { const location = useLocation(); const { selectedProduct: product } = useProduct(); - const issues = useIssues("product"); + // FIXME: what scope reports these issues with the new API? + const issues = useScopeIssues("product"); const registrationRequired = issues.find((i) => i.kind === "missing_registration"); // NOTE: it shouldn't be mounted in these paths, but let's prevent rendering diff --git a/web/src/components/software/SoftwarePage.tsx b/web/src/components/software/SoftwarePage.tsx index 707d51e996..574fed3294 100644 --- a/web/src/components/software/SoftwarePage.tsx +++ b/web/src/components/software/SoftwarePage.tsx @@ -36,7 +36,7 @@ import { } from "@patternfly/react-core"; import { Link, Page, IssuesAlert } from "~/components/core"; import UsedSize from "./UsedSize"; -import { useIssues } from "~/queries/issues"; +import { useScopeIssues } from "~/hooks/api"; import { usePatterns, useSoftwareProposal, @@ -133,7 +133,7 @@ const ReloadSection = ({ * Software page component */ function SoftwarePage(): React.ReactNode { - const issues = useIssues("software"); + const issues = useScopeIssues("software"); const proposal = useSoftwareProposal(); const patterns = usePatterns(); const repos = useRepositories(); diff --git a/web/src/components/storage/FixableConfigInfo.tsx b/web/src/components/storage/FixableConfigInfo.tsx index 92d8cd28da..d62e067978 100644 --- a/web/src/components/storage/FixableConfigInfo.tsx +++ b/web/src/components/storage/FixableConfigInfo.tsx @@ -23,7 +23,7 @@ import React from "react"; import { Alert, List, ListItem } from "@patternfly/react-core"; import { n_ } from "~/i18n"; -import { useConfigErrors } from "~/queries/issues"; +import { useScopeIssues } from "~/hooks/api"; const Description = ({ errors }) => { return ( @@ -40,7 +40,7 @@ const Description = ({ errors }) => { * */ export default function FixableConfigInfo() { - const configErrors = useConfigErrors("storage"); + const configErrors = useScopeIssues("storage"); if (!configErrors.length) return; diff --git a/web/src/components/storage/ProposalFailedInfo.test.tsx b/web/src/components/storage/ProposalFailedInfo.test.tsx index 43ebc2c25e..b979c33bab 100644 --- a/web/src/components/storage/ProposalFailedInfo.test.tsx +++ b/web/src/components/storage/ProposalFailedInfo.test.tsx @@ -25,7 +25,7 @@ import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProposalFailedInfo from "./ProposalFailedInfo"; import { LogicalVolume } from "~/types/storage/data"; -import { Issue, IssueSeverity, IssueSource } from "~/types/issues"; +import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; import { apiModel } from "~/api/storage/types"; const mockUseConfigErrorsFn = jest.fn(); diff --git a/web/src/components/storage/ProposalFailedInfo.tsx b/web/src/components/storage/ProposalFailedInfo.tsx index 29c4d47a0f..aa06e36c36 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -22,9 +22,8 @@ import React from "react"; import { Alert, Content } from "@patternfly/react-core"; -import { IssueSeverity } from "~/types/issues"; -import { useStorageModel } from "~/hooks/api"; -import { useIssues, useConfigErrors } from "~/queries/issues"; +import { useStorageModel, useScopeIssues } from "~/hooks/api"; +import { useConfigIssues } from "~/hooks/storage/issues"; import * as partitionUtils from "~/components/storage/utils/partition"; import { _, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; @@ -88,8 +87,8 @@ const Description = () => { * - The generated proposal contains no errors. */ export default function ProposalFailedInfo() { - const configErrors = useConfigErrors("storage"); - const errors = useIssues("storage").filter((s) => s.severity === IssueSeverity.Error); + const configErrors = useConfigIssues(); + const errors = useScopeIssues("storage"); if (configErrors.length !== 0) return; if (errors.length === 0) return; diff --git a/web/src/components/storage/ProposalPage.test.tsx b/web/src/components/storage/ProposalPage.test.tsx index 5f0b708e8e..7c69a65e90 100644 --- a/web/src/components/storage/ProposalPage.test.tsx +++ b/web/src/components/storage/ProposalPage.test.tsx @@ -30,7 +30,7 @@ import { screen } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import ProposalPage from "~/components/storage/ProposalPage"; import { StorageDevice } from "~/types/storage"; -import { Issue, IssueSeverity, IssueSource } from "~/types/issues"; +import { Issue, IssueSeverity, IssueSource } from "~/api/issue"; const disk: StorageDevice = { sid: 60, diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index 2c8df575a0..5a8966f786 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -50,14 +50,14 @@ import { useResetConfig } from "~/hooks/storage/config"; import { useConfigModel } from "~/queries/storage/config-model"; import { useZFCPSupported } from "~/queries/storage/zfcp"; import { useDASDSupported } from "~/queries/storage/dasd"; -import { useSystemErrors, useConfigErrors } from "~/queries/issues"; +import { useSystemIssues, useConfigIssues } from "~/hooks/storage/issues"; import { STORAGE as PATHS } from "~/routes/paths"; import { _, n_ } from "~/i18n"; import { useProgress, useProgressChanges } from "~/queries/progress"; import { useNavigate } from "react-router-dom"; function InvalidConfigEmptyState(): React.ReactNode { - const errors = useConfigErrors("storage"); + const errors = useConfigIssues(); const reset = useResetConfig(); return ( @@ -175,7 +175,7 @@ function ProposalEmptyState(): React.ReactNode { function ProposalSections(): React.ReactNode { const model = useConfigModel({ suspense: true }); - const systemErrors = useSystemErrors("storage"); + const systemErrors = useSystemIssues(); const hasResult = !systemErrors.length; return ( @@ -223,8 +223,8 @@ function ProposalSections(): React.ReactNode { export default function ProposalPage(): React.ReactNode { const model = useConfigModel({ suspense: true }); const availableDevices = useAvailableDevices(); - const systemErrors = useSystemErrors("storage"); - const configErrors = useConfigErrors("storage"); + const systemErrors = useSystemIssues(); + const configErrors = useConfigIssues(); const progress = useProgress("storage"); const navigate = useNavigate(); diff --git a/web/src/components/users/UsersPage.tsx b/web/src/components/users/UsersPage.tsx index 301f67d50d..ce568d8cba 100644 --- a/web/src/components/users/UsersPage.tsx +++ b/web/src/components/users/UsersPage.tsx @@ -24,11 +24,11 @@ import React from "react"; import { Content, Grid, GridItem } from "@patternfly/react-core"; import { IssuesAlert, Page } from "~/components/core"; import { FirstUser, RootUser } from "~/components/users"; -import { useIssues } from "~/queries/issues"; +import { useScopeIssues } from "~/hooks/api"; import { _ } from "~/i18n"; export default function UsersPage() { - const issues = useIssues("users"); + const issues = useScopeIssues("users"); return ( diff --git a/web/src/hooks/api.ts b/web/src/hooks/api.ts index 8ea8c1c985..3f425f4231 100644 --- a/web/src/hooks/api.ts +++ b/web/src/hooks/api.ts @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import React from "react"; +import React, { useCallback } from "react"; import { useQuery, useSuspenseQuery, useQueryClient } from "@tanstack/react-query"; import { getSystem, @@ -29,6 +29,7 @@ import { solveStorageModel, getStorageModel, getQuestions, + getIssues, } from "~/api"; import { useInstallerClient } from "~/context/installer"; import { System } from "~/api/system"; @@ -36,6 +37,7 @@ import { Proposal } from "~/api/proposal"; import { Config } from "~/api/config"; import { apiModel } from "~/api/storage"; import { Question } from "~/api/question"; +import { IssuesScope, Issue, IssuesMap } from "~/api/issue"; import { QueryHookOptions } from "~/types/queries"; const systemQuery = () => ({ @@ -60,6 +62,8 @@ function useSystemChanges() { return client.onEvent((event) => { if (event.type === "SystemChanged") { queryClient.invalidateQueries({ queryKey: ["system"] }); + if (event.scope === "storage") + queryClient.invalidateQueries({ queryKey: ["solvedStorageModel"] }); } }); }, [client, queryClient]); @@ -166,11 +170,66 @@ function useSolvedStorageModel( return func(query)?.data; } +const issuesQuery = () => { + return { + queryKey: ["issues"], + queryFn: getIssues, + }; +}; + +const selectIssues = (data: IssuesMap | null): Issue[] => { + if (!data) return []; + + return Object.keys(data).reduce((all: Issue[], key: IssuesScope) => { + const scoped = data[key].map((i) => ({ ...i, scope: key })); + return all.concat(scoped); + }, []); +}; + +function useIssues(options?: QueryHookOptions): Issue[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...issuesQuery(), + select: selectIssues, + }); + return data; +} + +const useIssuesChanges = () => { + const queryClient = useQueryClient(); + const client = useInstallerClient(); + + React.useEffect(() => { + if (!client) return; + + return client.onEvent((event) => { + if (event.type === "IssuesChanged") { + queryClient.invalidateQueries({ queryKey: ["issues"] }); + } + }); + }, [client, queryClient]); +}; + +function useScopeIssues(scope: IssuesScope, options?: QueryHookOptions): Issue[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...issuesQuery(), + select: useCallback( + (data: IssuesMap | null): Issue[] => + selectIssues(data).filter((i: Issue) => i.scope === scope), + [scope], + ), + }); + return data; +} + export { systemQuery, proposalQuery, extendedConfigQuery, storageModelQuery, + issuesQuery, + selectIssues, useSystem, useSystemChanges, useProposal, @@ -180,4 +239,7 @@ export { useQuestionsChanges, useStorageModel, useSolvedStorageModel, + useIssues, + useScopeIssues, + useIssuesChanges, }; diff --git a/web/src/hooks/storage/issues.ts b/web/src/hooks/storage/issues.ts new file mode 100644 index 0000000000..565bbc0577 --- /dev/null +++ b/web/src/hooks/storage/issues.ts @@ -0,0 +1,52 @@ +/* + * 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 { useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { QueryHookOptions } from "~/types/queries"; +import { IssueSource, Issue, IssuesMap } from "~/api/issue"; +import { issuesQuery, selectIssues } from "~/hooks/api"; + +const selectSystemIssues = (data: IssuesMap | null) => + selectIssues(data).filter((i) => i.scope === "storage" && i.source === IssueSource.System); + +function useSystemIssues(options?: QueryHookOptions): Issue[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...issuesQuery(), + select: selectSystemIssues, + }); + return data; +} + +const selectConfigIssues = (data: IssuesMap | null) => + selectIssues(data).filter((i) => i.scope === "storage" && i.source === IssueSource.Config); + +function useConfigIssues(options?: QueryHookOptions): Issue[] { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...issuesQuery(), + select: selectConfigIssues, + }); + return data; +} + +export { useSystemIssues, useConfigIssues }; diff --git a/web/src/hooks/storage/system.ts b/web/src/hooks/storage/system.ts index 5d4d223a5b..ff3d33ee85 100644 --- a/web/src/hooks/storage/system.ts +++ b/web/src/hooks/storage/system.ts @@ -190,6 +190,17 @@ function useVolumeTemplate(mountPath: string, options?: QueryHookOptions): stora return data; } +const selectIssues = (data: System | null): storage.Issue[] => data?.storage?.issues || []; + +function useIssues(options?: QueryHookOptions) { + const func = options?.suspense ? useSuspenseQuery : useQuery; + const { data } = func({ + ...systemQuery(), + select: selectIssues, + }); + return data; +} + export { useSystem, useEncryptionMethods, @@ -202,4 +213,5 @@ export { useDevices, useVolumeTemplates, useVolumeTemplate, + useIssues, }; diff --git a/web/src/queries/issues.ts b/web/src/queries/issues.ts deleted file mode 100644 index b767800062..0000000000 --- a/web/src/queries/issues.ts +++ /dev/null @@ -1,95 +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 { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; -import { useInstallerClient } from "~/context/installer"; -import { IssuesScope, IssueSeverity, IssueSource, Issue } from "~/types/issues"; -import { fetchIssues } from "~/api/issues"; - -const issuesQuery = (selectFn?: (i: Issue[]) => Issue[]) => { - return { - queryKey: ["issues"], - queryFn: fetchIssues, - select: selectFn, - }; -}; - -/** - * Returns the issues for the given scope. - * - * @param scope - Scope to get the issues from. - * @return issues for the given scope. - */ -const useIssues = (scope: IssuesScope): Issue[] => { - const { data } = useSuspenseQuery( - issuesQuery((issues: Issue[]) => { - return issues.filter((i: Issue) => i.scope === scope); - }), - ); - return data; -}; - -const useAllIssues = (): Issue[] => { - const { data } = useSuspenseQuery(issuesQuery()); - return data; -}; - -const useIssuesChanges = () => { - const queryClient = useQueryClient(); - const client = useInstallerClient(); - - React.useEffect(() => { - if (!client) return; - - return client.onEvent((event) => { - if (event.type === "IssuesChanged") { - queryClient.invalidateQueries({ queryKey: ["issues"] }); - queryClient.invalidateQueries({ queryKey: ["status"] }); - } - }); - }, [client, queryClient]); -}; - -/** - * Returns the system errors for the given scope. - */ -const useSystemErrors = (scope: IssuesScope) => { - const issues = useIssues(scope); - - return issues - .filter((i) => i.severity === IssueSeverity.Error) - .filter((i) => i.source === IssueSource.System); -}; - -/** - * Returns the config errors for the given scope. - */ -const useConfigErrors = (scope: IssuesScope) => { - const issues = useIssues(scope); - - return issues - .filter((i) => i.severity === IssueSeverity.Error) - .filter((i) => i.source === IssueSource.Config); -}; - -export { useIssues, useAllIssues, useIssuesChanges, useSystemErrors, useConfigErrors }; diff --git a/web/src/types/issues.ts b/web/src/types/issues.ts deleted file mode 100644 index 5efb26ecd9..0000000000 --- a/web/src/types/issues.ts +++ /dev/null @@ -1,95 +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. - */ - -/** - * Known scopes for issues. - */ -type IssuesScope = "localization" | "product" | "software" | "storage" | "users" | "iscsi"; - -/** - * Source of the issue - * - * Which is the origin of the issue (the system, the configuration or unknown). - */ -enum IssueSource { - /** Unknown source (it is kind of a fallback value) */ - Unknown = "unknown", - /** An unexpected situation in the system (e.g., missing device). */ - System = "system", - /** Wrong or incomplete configuration (e.g., an authentication mechanism is not set) */ - Config = "config", -} - -/** - * Issue severity - * - * It indicates how severe the problem is. - */ -enum IssueSeverity { - /** Just a warning, the installation can start */ - Warn = "warn", - /** An important problem that makes the installation not possible */ - Error = "error", -} - -/** - * Pre-installation issue as they come from the API. - */ -type ApiIssue = { - /** Issue description */ - description: string; - /** Issue kind **/ - kind: string; - /** Issue details */ - details?: string; - /** Where the issue comes from */ - source: IssueSource; - /** How severe is the issue */ - severity: IssueSeverity; -}; - -/** - * Issues grouped by scope as they come from the API. - */ -type IssuesMap = { - localization?: ApiIssue[]; - software?: ApiIssue[]; - product?: ApiIssue[]; - storage?: ApiIssue[]; - iscsi?: ApiIssue[]; - users?: ApiIssue[]; -}; - -/** - * Pre-installation issue augmented with the scope. - */ -type Issue = ApiIssue & { scope: IssuesScope }; - -/** - * Validation error - */ -type ValidationError = { - message: string; -}; - -export { IssueSource, IssueSeverity }; -export type { ApiIssue, IssuesMap, IssuesScope, Issue, ValidationError }; From 3c012bca1614030fc471a71a9aa6c90c71464b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Mon, 10 Nov 2025 16:01:20 +0000 Subject: [PATCH 6/9] Fix types - Tests are not fixed yet. --- web/src/api/config.ts | 4 +- .../storage/device.ts => api/l10n/config.ts} | 24 +-- web/src/api/l10n/proposal.ts | 6 +- web/src/api/manager.ts | 2 + web/src/api/network.ts | 2 + web/src/api/progress.ts | 2 + web/src/api/software.ts | 2 + web/src/api/status.ts | 2 + web/src/api/storage.ts | 1 + web/src/api/storage/config.ts | 179 ++++++++++++------ web/src/api/storage/proposal.ts | 7 +- web/src/api/storage/system.ts | 7 +- web/src/api/users.ts | 2 + web/src/components/overview/L10nSection.tsx | 7 +- web/src/components/storage/AutoSizeText.tsx | 3 +- web/src/components/storage/BootSelection.tsx | 14 +- .../storage/ConfigureDeviceMenu.tsx | 11 +- .../storage/DeviceEditorContent.tsx | 5 +- .../storage/DeviceSelectorModal.tsx | 34 ++-- .../components/storage/DeviceSelectorPage.tsx | 8 +- ...esFormSelect.jsx => DevicesFormSelect.tsx} | 41 ++-- .../{DevicesManager.js => DevicesManager.ts} | 151 ++++----------- web/src/components/storage/DriveEditor.tsx | 8 +- web/src/components/storage/DriveHeader.tsx | 5 +- .../components/storage/EncryptionSection.tsx | 2 +- web/src/components/storage/LvmPage.tsx | 14 +- web/src/components/storage/MdRaidEditor.tsx | 8 +- web/src/components/storage/MdRaidHeader.tsx | 5 +- .../storage/MenuDeviceDescription.tsx | 4 +- .../components/storage/MountPathMenuItem.tsx | 2 +- web/src/components/storage/PartitionPage.tsx | 4 +- .../storage/ProposalActionsDialog.tsx | 2 +- .../storage/ProposalResultSection.tsx | 8 +- .../storage/ProposalResultTable.tsx | 22 ++- .../components/storage/SearchedDeviceMenu.tsx | 26 ++- .../components/storage/SpaceActionsTable.tsx | 60 +++--- .../components/storage/SpacePolicyMenu.tsx | 12 +- .../storage/SpacePolicySelection.tsx | 22 +-- .../components/storage/device-utils.test.tsx | 12 +- web/src/components/storage/device-utils.tsx | 49 +++-- web/src/components/storage/utils.ts | 58 +++--- web/src/components/storage/utils/device.tsx | 38 ++-- web/src/components/storage/utils/drive.tsx | 2 +- .../components/storage/utils/partition.tsx | 2 +- web/src/helpers/storage/api-model.ts | 2 +- web/src/helpers/storage/boot.ts | 2 +- web/src/helpers/storage/device.ts | 93 +++++++++ web/src/helpers/storage/drive.ts | 2 +- web/src/helpers/storage/filesystem.ts | 2 +- web/src/helpers/storage/logical-volume.ts | 2 +- web/src/helpers/storage/md-raid.ts | 2 +- web/src/helpers/storage/model.ts | 2 +- web/src/helpers/storage/partition.ts | 2 +- web/src/helpers/storage/search.ts | 2 +- web/src/helpers/storage/space-policy.ts | 2 +- web/src/helpers/storage/volume-group.ts | 2 +- web/src/hooks/storage/proposal.ts | 13 +- web/src/hooks/storage/system.ts | 14 +- web/src/queries/l10n.ts | 48 ----- web/src/queries/storage/iscsi.ts | 10 +- web/src/test-utils.tsx | 1 - web/src/types/storage.ts | 106 ----------- web/src/types/storage/data.ts | 2 +- web/src/types/storage/model.ts | 2 +- 64 files changed, 595 insertions(+), 595 deletions(-) rename web/src/{factories/storage/device.ts => api/l10n/config.ts} (68%) rename web/src/components/storage/{DevicesFormSelect.jsx => DevicesFormSelect.tsx} (57%) rename web/src/components/storage/{DevicesManager.js => DevicesManager.ts} (57%) create mode 100644 web/src/helpers/storage/device.ts delete mode 100644 web/src/queries/l10n.ts diff --git a/web/src/api/config.ts b/web/src/api/config.ts index fc050d8899..a14daf0ca1 100644 --- a/web/src/api/config.ts +++ b/web/src/api/config.ts @@ -20,11 +20,13 @@ * find current contact information at www.suse.com. */ +import * as l10n from "~/api/l10n/config"; import * as storage from "~/api/storage/config"; type Config = { + l10n?: l10n.Config; storage?: storage.Config; }; -export { storage }; +export { l10n, storage }; export type { Config }; diff --git a/web/src/factories/storage/device.ts b/web/src/api/l10n/config.ts similarity index 68% rename from web/src/factories/storage/device.ts rename to web/src/api/l10n/config.ts index 5d9314150c..925b6c169c 100644 --- a/web/src/factories/storage/device.ts +++ b/web/src/api/l10n/config.ts @@ -20,22 +20,10 @@ * find current contact information at www.suse.com. */ -import { Device } from "~/api/storage/types/openapi"; +type Config = { + locale?: string; + keymap?: string; + timezone?: string; +}; -function generate({ name, size, sid }): Device { - return { - deviceInfo: { sid, name, description: "" }, - blockDevice: { - active: true, - encrypted: false, - shrinking: { unsupported: [] }, - size, - start: 0, - systems: [], - udevIds: [], - udevPaths: [], - }, - }; -} - -export { generate }; +export type { Config }; diff --git a/web/src/api/l10n/proposal.ts b/web/src/api/l10n/proposal.ts index e7ab01974f..c8a2766a7e 100644 --- a/web/src/api/l10n/proposal.ts +++ b/web/src/api/l10n/proposal.ts @@ -20,6 +20,10 @@ * find current contact information at www.suse.com. */ -type Proposal = object; +type Proposal = { + locale?: string; + keymap?: string; + timezone?: string; +}; export type { Proposal }; diff --git a/web/src/api/manager.ts b/web/src/api/manager.ts index 61653c8408..0e27a2de96 100644 --- a/web/src/api/manager.ts +++ b/web/src/api/manager.ts @@ -20,6 +20,8 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { get, post } from "~/http"; /** diff --git a/web/src/api/network.ts b/web/src/api/network.ts index a6fd2b9fe1..edd4ea14ec 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -20,6 +20,8 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { del, get, post, put } from "~/http"; import { APIAccessPoint, APIConnection, APIDevice, NetworkGeneralState } from "~/types/network"; diff --git a/web/src/api/progress.ts b/web/src/api/progress.ts index d62095f0e4..e4437e0fb8 100644 --- a/web/src/api/progress.ts +++ b/web/src/api/progress.ts @@ -20,6 +20,8 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { get } from "~/http"; import { APIProgress, Progress } from "~/types/progress"; diff --git a/web/src/api/software.ts b/web/src/api/software.ts index 0f6210fed6..b457b5c0e4 100644 --- a/web/src/api/software.ts +++ b/web/src/api/software.ts @@ -20,6 +20,8 @@ * find current contact information at www.suse.com. */ +// @todo Move to the new API. + import { AddonInfo, Conflict, diff --git a/web/src/api/status.ts b/web/src/api/status.ts index f7d0f7c65e..45de115e7e 100644 --- a/web/src/api/status.ts +++ b/web/src/api/status.ts @@ -20,6 +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"; diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts index dccba2ae09..9350eae137 100644 --- a/web/src/api/storage.ts +++ b/web/src/api/storage.ts @@ -23,3 +23,4 @@ export * as config from "~/api/storage/config"; export * as apiModel from "~/api/storage/model"; export * as system from "~/api/storage/system"; +export * as proposal from "~/api/storage/proposal"; diff --git a/web/src/api/storage/config.ts b/web/src/api/storage/config.ts index b6ee6456c1..e20ca9b81e 100644 --- a/web/src/api/storage/config.ts +++ b/web/src/api/storage/config.ts @@ -9,16 +9,40 @@ */ export type Alias = string; export type DriveElement = NonPartitionedDrive | PartitionedDrive; -export type SearchElement = SimpleSearchAll | SimpleSearchByName | AdvancedSearch; +export type DriveSearch = SearchAll | SearchName | DriveAdvancedSearch; /** * Shortcut to match all devices if there is any (equivalent to specify no conditions and to skip the entry if no device is found). */ -export type SimpleSearchAll = "*"; -export type SimpleSearchByName = string; +export type SearchAll = "*"; +/** + * Search by device name + */ +export type SearchName = string; +export type DriveSearchCondition = SearchConditionName | SearchConditionSize; +export type SizeValue = SizeString | SizeBytes; +/** + * Human readable size. + */ +export type SizeString = string; +/** + * Size in bytes. + */ +export type SizeBytes = number; +export type DriveSearchSort = DriveSearchSortCriterion | DriveSearchSortCriterion[]; +export type DriveSearchSortCriterion = DriveSearchSortCriterionShort | DriveSearchSortCriterionFull; +export type DriveSearchSortCriterionShort = "name" | "size"; +/** + * Direction of sorting at the search results + */ +export type SearchSortCriterionOrder = "asc" | "desc"; +/** + * Maximum devices to match. + */ +export type SearchMax = number; /** * How to handle the section if the device is not found. */ -export type SearchAction = "skip" | "error"; +export type SearchActions = "skip" | "error"; export type Encryption = | EncryptionLuks1 | EncryptionLuks2 @@ -74,17 +98,22 @@ export type PartitionElement = | RegularPartition | PartitionToDelete | PartitionToDeleteIfNeeded; -export type PartitionId = "linux" | "swap" | "lvm" | "raid" | "esp" | "prep" | "bios_boot"; -export type Size = SizeValue | SizeTuple | SizeRange; -export type SizeValue = SizeString | SizeBytes; -/** - * Human readable size. - */ -export type SizeString = string; +export type PartitionSearch = SearchAll | SearchName | PartitionAdvancedSearch; +export type PartitionSearchCondition = + | SearchConditionName + | SearchConditionSize + | SearchConditionPartitionNumber; +export type PartitionSearchSort = PartitionSearchSortCriterion | PartitionSearchSortCriterion[]; +export type PartitionSearchSortCriterion = + | PartitionSearchSortCriterionShort + | PartitionSearchSortCriterionFull; +export type PartitionSearchSortCriterionShort = "name" | "size" | "number"; /** - * Size in bytes. + * How to handle the section if the device is not found. */ -export type SizeBytes = number; +export type SearchCreatableActions = "skip" | "error" | "create"; +export type PartitionId = "linux" | "swap" | "lvm" | "raid" | "esp" | "prep" | "bios_boot"; +export type Size = SizeValue | SizeTuple | SizeRange; /** * Lower size limit and optionally upper size limit. * @@ -97,6 +126,11 @@ export type SizeValueWithCurrent = SizeValue | SizeCurrent; * The current size of the device. */ export type SizeCurrent = "current"; +export type DeletePartitionSearch = SearchAll | SearchName | DeletePartitionAdvancedSearch; +/** + * Device base name. + */ +export type BaseName = string; export type PhysicalVolumeElement = | Alias | SimplePhysicalVolumesGenerator @@ -112,10 +146,13 @@ export type LogicalVolumeElement = */ export type LogicalVolumeStripes = number; export type MdRaidElement = NonPartitionedMdRaid | PartitionedMdRaid; -/** - * MD base name. - */ -export type MdRaidName = string; +export type MdRaidSearch = SearchAll | SearchName | MdRaidAdvancedSearch; +export type MdRaidSearchCondition = SearchConditionName | SearchConditionSize; +export type MdRaidSearchSort = MdRaidSearchSortCriterion | MdRaidSearchSortCriterion[]; +export type MdRaidSearchSortCriterion = + | MdRaidSearchSortCriterionShort + | MdRaidSearchSortCriterionFull; +export type MdRaidSearchSortCriterionShort = "name" | "size"; export type MDLevel = "raid0" | "raid1" | "raid5" | "raid6" | "raid10"; /** * Only applies to raid5, raid6 and raid10 @@ -170,24 +207,35 @@ export interface Boot { * Drive without a partition table (e.g., directly formatted). */ export interface NonPartitionedDrive { - search?: SearchElement; + search?: DriveSearch; alias?: Alias; encryption?: Encryption; filesystem?: Filesystem; } -/** - * Advanced options for searching devices. - */ -export interface AdvancedSearch { - condition?: SearchCondition; - /** - * Maximum devices to match. - */ - max?: number; - ifNotFound?: SearchAction; +export interface DriveAdvancedSearch { + condition?: DriveSearchCondition; + sort?: DriveSearchSort; + max?: SearchMax; + ifNotFound?: SearchActions; } -export interface SearchCondition { - name: SimpleSearchByName; +export interface SearchConditionName { + name: SearchName; +} +export interface SearchConditionSize { + size: SizeValue | SearchConditionSizeEqual | SearchConditionSizeGreater | SearchConditionSizeLess; +} +export interface SearchConditionSizeEqual { + equal: SizeValue; +} +export interface SearchConditionSizeGreater { + greater: SizeValue; +} +export interface SearchConditionSizeLess { + less: SizeValue; +} +export interface DriveSearchSortCriterionFull { + name?: SearchSortCriterionOrder; + size?: SearchSortCriterionOrder; } /** * LUKS1 encryption. @@ -223,7 +271,7 @@ export interface EncryptionPervasiveLuks2 { }; } /** - * TPM-Based Full Disk Encrytion. + * TPM-Based Full Disk Encryption. */ export interface EncryptionTPM { tpmFde: { @@ -266,7 +314,7 @@ export interface FilesystemTypeBtrfs { }; } export interface PartitionedDrive { - search?: SearchElement; + search?: DriveSearch; alias?: Alias; ptableType?: PtableType; partitions: PartitionElement[]; @@ -287,13 +335,30 @@ export interface AdvancedPartitionsGenerator { }; } export interface RegularPartition { - search?: SearchElement; + search?: PartitionSearch; alias?: Alias; id?: PartitionId; size?: Size; encryption?: Encryption; filesystem?: Filesystem; } +export interface PartitionAdvancedSearch { + condition?: PartitionSearchCondition; + sort?: PartitionSearchSort; + max?: SearchMax; + ifNotFound?: SearchCreatableActions; +} +export interface SearchConditionPartitionNumber { + /** + * Partition number (e.g., 1 for vda1). + */ + number: number; +} +export interface PartitionSearchSortCriterionFull { + name?: SearchSortCriterionOrder; + size?: SearchSortCriterionOrder; + number?: SearchSortCriterionOrder; +} /** * Size range. */ @@ -302,14 +367,20 @@ export interface SizeRange { max?: SizeValueWithCurrent; } export interface PartitionToDelete { - search: SearchElement; + search: DeletePartitionSearch; /** * Delete the partition. */ delete: true; } +export interface DeletePartitionAdvancedSearch { + condition?: PartitionSearchCondition; + sort?: PartitionSearchSort; + max?: SearchMax; + ifNotFound?: SearchActions; +} export interface PartitionToDeleteIfNeeded { - search: SearchElement; + search: DeletePartitionSearch; /** * Delete the partition if needed to make space. */ @@ -320,10 +391,7 @@ export interface PartitionToDeleteIfNeeded { * LVM volume group. */ export interface VolumeGroup { - /** - * Volume group name. - */ - name: string; + name: BaseName; extentSize?: SizeValue; /** * Devices to use as physical volumes. @@ -358,10 +426,7 @@ export interface AdvancedLogicalVolumesGenerator { }; } export interface LogicalVolume { - /** - * Logical volume name. - */ - name?: string; + name?: BaseName; size?: Size; stripes?: LogicalVolumeStripes; stripeSize?: SizeValue; @@ -374,20 +439,14 @@ export interface ThinPoolLogicalVolume { */ pool: true; alias?: Alias; - /** - * Logical volume name. - */ - name?: string; + name?: BaseName; size?: Size; stripes?: LogicalVolumeStripes; stripeSize?: SizeValue; encryption?: Encryption; } export interface ThinLogicalVolume { - /** - * Thin logical volume name. - */ - name?: string; + name?: BaseName; size?: Size; usedPool: Alias; encryption?: Encryption; @@ -397,9 +456,9 @@ export interface ThinLogicalVolume { * MD RAID without a partition table (e.g., directly formatted). */ export interface NonPartitionedMdRaid { - search?: SearchElement; + search?: MdRaidSearch; alias?: Alias; - name?: MdRaidName; + name?: BaseName; level?: MDLevel; parity?: MDParity; chunkSize?: SizeValue; @@ -407,10 +466,20 @@ export interface NonPartitionedMdRaid { encryption?: Encryption; filesystem?: Filesystem; } +export interface MdRaidAdvancedSearch { + condition?: MdRaidSearchCondition; + sort?: MdRaidSearchSort; + max?: SearchMax; + ifNotFound?: SearchCreatableActions; +} +export interface MdRaidSearchSortCriterionFull { + name?: SearchSortCriterionOrder; + size?: SearchSortCriterionOrder; +} export interface PartitionedMdRaid { - search?: SearchElement; + search?: MdRaidSearch; alias?: Alias; - name?: MdRaidName; + name?: BaseName; level?: MDLevel; parity?: MDParity; chunkSize?: SizeValue; diff --git a/web/src/api/storage/proposal.ts b/web/src/api/storage/proposal.ts index 1b3bdf0c01..ee3d33062b 100644 --- a/web/src/api/storage/proposal.ts +++ b/web/src/api/storage/proposal.ts @@ -97,12 +97,17 @@ export interface Md { export interface Multipath { wireNames: string[]; } +export type PartitionSlot = { + start: number; + size: number; +}; export interface PartitionTable { type: "gpt" | "msdos" | "dasd"; - unusedSlots: number[][]; + unusedSlots: PartitionSlot[]; } export interface Partition { efi: boolean; + start: number; } export interface VolumeGroup { size: number; diff --git a/web/src/api/storage/system.ts b/web/src/api/storage/system.ts index d35c77a155..9c70bfa588 100644 --- a/web/src/api/storage/system.ts +++ b/web/src/api/storage/system.ts @@ -130,12 +130,17 @@ export interface Md { export interface Multipath { wireNames: string[]; } +export type PartitionSlot = { + start: number; + size: number; +}; export interface PartitionTable { type: "gpt" | "msdos" | "dasd"; - unusedSlots: number[][]; + unusedSlots: PartitionSlot[]; } export interface Partition { efi: boolean; + start: number; } export interface VolumeGroup { size: number; diff --git a/web/src/api/users.ts b/web/src/api/users.ts index ae8db6e116..da9c19bff5 100644 --- a/web/src/api/users.ts +++ b/web/src/api/users.ts @@ -20,6 +20,8 @@ * 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"; diff --git a/web/src/components/overview/L10nSection.tsx b/web/src/components/overview/L10nSection.tsx index 806a0ab80c..044921d0d5 100644 --- a/web/src/components/overview/L10nSection.tsx +++ b/web/src/components/overview/L10nSection.tsx @@ -27,10 +27,11 @@ import { _ } from "~/i18n"; import { Locale } from "~/api/l10n/system"; export default function L10nSection() { - const { l10n: l10nProposal } = useProposal(); - const { l10n: l10nSystem } = useSystem(); + const proposal = useProposal({ suspense: true }); + const system = useSystem({ suspense: true }); const locale = - l10nProposal.locale && l10nSystem.locales.find((l: Locale) => l.id === l10nProposal.locale); + proposal?.l10n?.locale && + system?.l10n?.locales?.find((l: Locale) => l.id === proposal.l10n.locale); // TRANSLATORS: %s will be replaced by a language name and territory, example: // "English (United States)". diff --git a/web/src/components/storage/AutoSizeText.tsx b/web/src/components/storage/AutoSizeText.tsx index 03b7a050d8..83403ed754 100644 --- a/web/src/components/storage/AutoSizeText.tsx +++ b/web/src/components/storage/AutoSizeText.tsx @@ -26,7 +26,8 @@ import { SubtleContent } from "~/components/core/"; import { deviceSize } from "~/components/storage/utils"; import { _, formatList } from "~/i18n"; import { sprintf } from "sprintf-js"; -import { apiModel, Volume } from "~/api/storage/types"; +import { apiModel } from "~/api/storage"; +import { Volume } from "~/api/storage/system"; type DeviceType = "partition" | "logicalVolume"; diff --git a/web/src/components/storage/BootSelection.tsx b/web/src/components/storage/BootSelection.tsx index f6201c7dba..895c466f3c 100644 --- a/web/src/components/storage/BootSelection.tsx +++ b/web/src/components/storage/BootSelection.tsx @@ -26,21 +26,23 @@ import { ActionGroup, Content, Form, FormGroup, Radio, Stack } from "@patternfly import { DevicesFormSelect } from "~/components/storage"; import { Page, SubtleContent } from "~/components/core"; import { deviceLabel } from "~/components/storage/utils"; -import { StorageDevice } from "~/types/storage"; +import { storage } from "~/api/system"; import { useCandidateDevices, useDevices } from "~/hooks/storage/system"; import textStyles from "@patternfly/react-styles/css/utilities/Text/text"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import { useModel } from "~/hooks/storage/model"; +import { Model } from "~/types/storage/model"; +import { isDrive } from "~/helpers/storage/device"; import { useSetBootDevice, useSetDefaultBootDevice, useDisableBootConfig, } from "~/hooks/storage/boot"; -const filteredCandidates = (candidates, model): StorageDevice[] => { +const filteredCandidates = (candidates: storage.Device[], model: Model): storage.Device[] => { return candidates.filter((candidate) => { - const collection = candidate.isDrive ? model.drives : model.mdRaids; + const collection = isDrive(candidate) ? model.drives : model.mdRaids; const device = collection.find((d) => d.name === candidate.name); return !device || !device.filesystem; }); @@ -57,9 +59,9 @@ type BootSelectionState = { load: boolean; selectedOption?: string; configureBoot?: boolean; - bootDevice?: StorageDevice; - defaultBootDevice?: StorageDevice; - candidateDevices?: StorageDevice[]; + bootDevice?: storage.Device; + defaultBootDevice?: storage.Device; + candidateDevices?: storage.Device[]; }; /** diff --git a/web/src/components/storage/ConfigureDeviceMenu.tsx b/web/src/components/storage/ConfigureDeviceMenu.tsx index fe01608c0e..02e0fef9a3 100644 --- a/web/src/components/storage/ConfigureDeviceMenu.tsx +++ b/web/src/components/storage/ConfigureDeviceMenu.tsx @@ -31,14 +31,15 @@ import { useAddReusedMdRaid } from "~/hooks/storage/md-raid"; import { STORAGE as PATHS } from "~/routes/paths"; import { sprintf } from "sprintf-js"; import { _, n_ } from "~/i18n"; -import { StorageDevice } from "~/types/storage"; +import { storage } from "~/api/system"; import DeviceSelectorModal from "./DeviceSelectorModal"; +import { isDrive } from "~/helpers/storage/device"; type AddDeviceMenuItemProps = { /** Whether some of the available devices is an MD RAID */ withRaids: boolean; /** Available devices to be chosen */ - devices: StorageDevice[]; + devices: storage.Device[]; /** The total amount of drives and RAIDs already configured */ usedCount: number; } & MenuItemProps; @@ -132,10 +133,10 @@ export default function ConfigureDeviceMenu(): React.ReactNode { const usedDevicesNames = model.drives.concat(model.mdRaids).map((d) => d.name); const usedDevicesCount = usedDevicesNames.length; const devices = allDevices.filter((d) => !usedDevicesNames.includes(d.name)); - const withRaids = !!allDevices.filter((d) => !d.isDrive).length; + const withRaids = !!allDevices.filter((d) => !isDrive(d)).length; - const addDevice = (device: StorageDevice) => { - const hook = device.isDrive ? addDrive : addReusedMdRaid; + const addDevice = (device: storage.Device) => { + const hook = isDrive(device) ? addDrive : addReusedMdRaid; hook({ name: device.name, spacePolicy: "keep" }); }; diff --git a/web/src/components/storage/DeviceEditorContent.tsx b/web/src/components/storage/DeviceEditorContent.tsx index 65f5143f83..1ffb5b7ae5 100644 --- a/web/src/components/storage/DeviceEditorContent.tsx +++ b/web/src/components/storage/DeviceEditorContent.tsx @@ -25,9 +25,10 @@ import UnusedMenu from "~/components/storage/UnusedMenu"; import FilesystemMenu from "~/components/storage/FilesystemMenu"; import PartitionsMenu from "~/components/storage/PartitionsMenu"; import SpacePolicyMenu from "~/components/storage/SpacePolicyMenu"; -import { model, StorageDevice } from "~/types/storage"; +import { model } from "~/types/storage"; +import { system } from "~/api/storage"; -type DeviceEditorContentProps = { deviceModel: model.Drive | model.MdRaid; device: StorageDevice }; +type DeviceEditorContentProps = { deviceModel: model.Drive | model.MdRaid; device: system.Device }; export default function DeviceEditorContent({ deviceModel, diff --git a/web/src/components/storage/DeviceSelectorModal.tsx b/web/src/components/storage/DeviceSelectorModal.tsx index 197e577328..7b5f5accd4 100644 --- a/web/src/components/storage/DeviceSelectorModal.tsx +++ b/web/src/components/storage/DeviceSelectorModal.tsx @@ -27,7 +27,7 @@ import SelectableDataTable, { SortedBy, SelectableDataTableProps, } from "~/components/core/SelectableDataTable"; -import { StorageDevice } from "~/types/storage"; +import { storage } from "~/api/system"; import { typeDescription, contentDescription, @@ -36,29 +36,31 @@ import { import { deviceSize } from "~/components/storage/utils"; import { sortCollection } from "~/utils"; import { _ } from "~/i18n"; +import { deviceSystems } from "~/helpers/storage/device"; type DeviceSelectorProps = { - devices: StorageDevice[]; - selectedDevices?: StorageDevice[]; - onSelectionChange: SelectableDataTableProps["onSelectionChange"]; - selectionMode?: SelectableDataTableProps["selectionMode"]; + devices: storage.Device[]; + selectedDevices?: storage.Device[]; + onSelectionChange: SelectableDataTableProps["onSelectionChange"]; + selectionMode?: SelectableDataTableProps["selectionMode"]; }; -const size = (device: StorageDevice) => { - return deviceSize(device.size); +const size = (device: storage.Device) => { + return deviceSize(device.block.size); }; -const description = (device: StorageDevice) => { - if (device.model && device.model.length) return device.model; +const description = (device: storage.Device) => { + const model = device.drive?.model; + if (model && model.length) return model; return typeDescription(device); }; -const details = (device: StorageDevice) => { +const details = (device: storage.Device) => { return ( {contentDescription(device)} - {device.systems.map((s, i) => ( + {deviceSystems(device).map((s, i) => ( @@ -82,7 +84,7 @@ const DeviceSelector = ({ const [sortedBy, setSortedBy] = useState({ index: 0, direction: "asc" }); const columns = [ - { name: _("Device"), value: (device: StorageDevice) => device.name, sortingKey: "name" }, + { name: _("Device"), value: (device: storage.Device) => device.name, sortingKey: "name" }, { name: _("Size"), value: size, @@ -114,9 +116,9 @@ const DeviceSelector = ({ }; type DeviceSelectorModalProps = Omit & { - selected?: StorageDevice; - devices: StorageDevice[]; - onConfirm: (selection: StorageDevice[]) => void; + selected?: storage.Device; + devices: storage.Device[]; + onConfirm: (selection: storage.Device[]) => void; onCancel: ButtonProps["onClick"]; }; @@ -128,7 +130,7 @@ export default function DeviceSelectorModal({ ...popupProps }: DeviceSelectorModalProps): React.ReactNode { // FIXME: improve initial selection handling - const [selectedDevices, setSelectedDevices] = useState( + const [selectedDevices, setSelectedDevices] = useState( selected ? [selected] : [devices[0]], ); diff --git a/web/src/components/storage/DeviceSelectorPage.tsx b/web/src/components/storage/DeviceSelectorPage.tsx index 33126af9b3..574c00427f 100644 --- a/web/src/components/storage/DeviceSelectorPage.tsx +++ b/web/src/components/storage/DeviceSelectorPage.tsx @@ -23,7 +23,7 @@ import React, { useState } from "react"; import { Content } from "@patternfly/react-core"; import { SelectableDataTable, Page } from "~/components/core/"; -import { StorageDevice } from "~/types/storage"; +import { storage } from "~/api/system"; import { useAvailableDevices } from "~/hooks/storage/system"; import { _ } from "~/i18n"; import { SelectableDataTableProps } from "../core/SelectableDataTable"; @@ -34,8 +34,8 @@ import { } from "~/components/storage/utils/device"; type DeviceSelectorProps = { - devices: StorageDevice[]; - selectedDevices?: StorageDevice[]; + devices: storage.Device[]; + selectedDevices?: storage.Device[]; onSelectionChange: SelectableDataTableProps["onSelectionChange"]; selectionMode?: SelectableDataTableProps["selectionMode"]; }; @@ -51,7 +51,7 @@ const DeviceSelector = ({ device.name }, + { name: _("Name"), value: (device: storage.Device) => device.name }, { name: _("Content"), value: contentDescription }, { name: _("Filesystems"), value: filesystemLabels }, ]} diff --git a/web/src/components/storage/DevicesFormSelect.jsx b/web/src/components/storage/DevicesFormSelect.tsx similarity index 57% rename from web/src/components/storage/DevicesFormSelect.jsx rename to web/src/components/storage/DevicesFormSelect.tsx index 3614339177..d3927f986a 100644 --- a/web/src/components/storage/DevicesFormSelect.jsx +++ b/web/src/components/storage/DevicesFormSelect.tsx @@ -20,34 +20,27 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; -import { FormSelect, FormSelectOption } from "@patternfly/react-core"; +import { FormSelectProps, FormSelect, FormSelectOption } from "@patternfly/react-core"; + import { deviceLabel } from "~/components/storage/utils"; +import { storage } from "~/api/system"; -/** - * @typedef {import ("@patternfly/react-core").FormSelectProps} PFFormSelectProps - * @typedef {import ("~/types/storage").StorageDevice} StorageDevice - */ +type DevicesFormSelectBaseProps = { + devices: storage.Device[]; + selectedDevice: storage.Device; + onChange: (device: storage.Device) => void; +}; -/** - * A PF/Select for simple device selection - * @component - * - * @example Simple usage - * import { devices, selected } from "somewhere"; - * - * - * - * @typedef {object} DevicesFormSelectBaseProps - * @property {StorageDevice[]} props.devices - Devices to show in the selector. - * @property {StorageDevice} [props.selectedDevice] - Currently selected device. In case of - * @property {(StorageDevice) => void} props.onChange - Callback to be called when the selection changes - * - * @param {DevicesFormSelectBaseProps & Omit} props - */ -export default function DevicesFormSelect({ devices, selectedDevice, onChange, ...otherProps }) { +type DevicesFormSelectProps = DevicesFormSelectBaseProps & + Omit; + +export default function DevicesFormSelect({ + devices, + selectedDevice, + onChange, + ...otherProps +}: DevicesFormSelectProps) { return ( /** @ts-expect-error: for some reason using otherProps makes TS complain */ d.sid === sid); } /** * Staging device with the given SID. - * @method - * - * @param {Number} sid - * @returns {StorageDevice|undefined} */ - stagingDevice(sid) { - return this.#device(sid, this.staging); + stagingDevice(sid: number): proposal.Device { + return this.staging.find((d) => d.sid === sid); } /** * Whether the given device exists in system. - * @method - * - * @param {StorageDevice} device - * @returns {Boolean} */ - existInSystem(device) { - return this.#exist(device, this.system); + existInSystem(device: system.Device): boolean { + return this.system.find((d) => d.sid === device.sid) !== undefined; } /** * Whether the given device exists in staging. - * @method - * - * @param {StorageDevice} device - * @returns {Boolean} */ - existInStaging(device) { - return this.#exist(device, this.staging); + existInStaging(device: proposal.Device): boolean { + return this.staging.find((d) => d.sid === device.sid) !== undefined; } /** * Whether the given device is going to be formatted. - * @method - * - * @param {StorageDevice} device - * @returns {Boolean} */ - hasNewFilesystem(device) { + hasNewFilesystem(device: proposal.Device): boolean { if (!device.filesystem) return false; const systemDevice = this.systemDevice(device.sid); @@ -109,46 +87,37 @@ export default class DevicesManager { /** * Whether the given device is going to be shrunk. - * @method - * - * @param {StorageDevice} device - * @returns {Boolean} */ - isShrunk(device) { + isShrunk(device: proposal.Device): boolean { return this.shrinkSize(device) > 0; } /** * Amount of bytes the given device is going to be shrunk. - * @method - * - * @param {StorageDevice} device - * @returns {Number} */ - shrinkSize(device) { + shrinkSize(device: proposal.Device): number { const systemDevice = this.systemDevice(device.sid); const stagingDevice = this.stagingDevice(device.sid); if (!systemDevice || !stagingDevice) return 0; - const amount = systemDevice.size - stagingDevice.size; + const amount = systemDevice.block.size - stagingDevice.block.size; return amount > 0 ? amount : 0; } /** * Disk devices and LVM volume groups used for the installation. - * @method * * @note The used devices are extracted from the actions, but the optional argument * can be used to expand the list if some devices must be included despite not * being affected by the actions. * - * @param {string[]} knownNames - names of devices already known to be used, even if - * there are no actions on them - * @returns {StorageDevice[]} + * @param knownNames - names of devices already known to be used, even if there are no actions on + * them. */ - usedDevices(knownNames = []) { - const isTarget = (device) => device.isDrive || ["md", "lvmVg"].includes(device.type); + usedDevices(knownNames: string[] = []): proposal.Device[] { + const isTarget = (device: system.Device | proposal.Device): boolean => + isDrive(device) || isMd(device) || isVolumeGroup(device); // Check in system devices to detect removals. const targetSystem = this.system.filter(isTarget); @@ -164,82 +133,48 @@ export default class DevicesManager { /** * Devices deleted. - * @method * * @note The devices are extracted from the actions. - * - * @returns {StorageDevice[]} */ - deletedDevices() { - return this.#deleteActionsDevice().filter((d) => !d.isDrive); + deletedDevices(): system.Device[] { + return this.#deleteActionsDevice().filter((d) => !d.drive); } /** * Devices resized. - * @method * * @note The devices are extracted from the actions. - * - * @returns {StorageDevice[]} */ - resizedDevices() { - return this.#resizeActionsDevice().filter((d) => !d.isDrive); + resizedDevices(): system.Device[] { + return this.#resizeActionsDevice().filter((d) => !d.drive); } /** * Systems deleted. - * @method - * - * @returns {string[]} */ - deletedSystems() { + deletedSystems(): string[] { const systems = this.#deleteActionsDevice() .filter((d) => !d.partitionTable) - .map((d) => d.systems) + .map(deviceSystems) .flat(); return compact(systems); } /** * Systems resized. - * @method - * - * @returns {string[]} */ - resizedSystems() { + resizedSystems(): string[] { const systems = this.#resizeActionsDevice() .filter((d) => !d.partitionTable) - .map((d) => d.systems) + .map(deviceSystems) .flat(); return compact(systems); } - /** - * @param {number} sid - * @param {StorageDevice[]} source - * @returns {StorageDevice|undefined} - */ - #device(sid, source) { - return source.find((d) => d.sid === sid); - } - - /** - * @param {StorageDevice} device - * @param {StorageDevice[]} source - * @returns {boolean} - */ - #exist(device, source) { - return this.#device(device.sid, source) !== undefined; - } - - /** - * @param {StorageDevice} device - * @returns {boolean} - */ - #isUsed(device) { + #isUsed(device: system.Device | proposal.Device): boolean { const sids = unique(compact(this.actions.map((a) => a.device))); - const partitions = device.partitionTable?.partitions || []; + const partitions = device.partitions || []; const lvmLvs = device.logicalVolumes || []; return ( @@ -249,19 +184,13 @@ export default class DevicesManager { ); } - /** - * @returns {StorageDevice[]} - */ - #deleteActionsDevice() { + #deleteActionsDevice(): system.Device[] { const sids = this.actions.filter((a) => a.delete).map((a) => a.device); const devices = sids.map((sid) => this.systemDevice(sid)); return compact(devices); } - /** - * @returns {StorageDevice[]} - */ - #resizeActionsDevice() { + #resizeActionsDevice(): system.Device[] { const sids = this.actions.filter((a) => a.resize).map((a) => a.device); const devices = sids.map((sid) => this.systemDevice(sid)); return compact(devices); diff --git a/web/src/components/storage/DriveEditor.tsx b/web/src/components/storage/DriveEditor.tsx index d2147ce5c2..15d8453c30 100644 --- a/web/src/components/storage/DriveEditor.tsx +++ b/web/src/components/storage/DriveEditor.tsx @@ -25,13 +25,13 @@ import ConfigEditorItem from "~/components/storage/ConfigEditorItem"; import DriveHeader from "~/components/storage/DriveHeader"; import DeviceEditorContent from "~/components/storage/DeviceEditorContent"; import SearchedDeviceMenu from "~/components/storage/SearchedDeviceMenu"; -import { Drive } from "~/types/storage/model"; -import { model, StorageDevice } from "~/types/storage"; +import { model } from "~/types/storage"; +import { storage } from "~/api/system"; import { useDeleteDrive } from "~/hooks/storage/drive"; type DriveDeviceMenuProps = { drive: model.Drive; - selected: StorageDevice; + selected: storage.Device; }; /** @@ -44,7 +44,7 @@ const DriveDeviceMenu = ({ drive, selected }: DriveDeviceMenuProps) => { return ; }; -export type DriveEditorProps = { drive: Drive; driveDevice: StorageDevice }; +export type DriveEditorProps = { drive: model.Drive; driveDevice: storage.Device }; /** * Component responsible for displaying detailed information and available actions diff --git a/web/src/components/storage/DriveHeader.tsx b/web/src/components/storage/DriveHeader.tsx index 266c642a83..ad0cd0b8f3 100644 --- a/web/src/components/storage/DriveHeader.tsx +++ b/web/src/components/storage/DriveHeader.tsx @@ -20,12 +20,13 @@ * find current contact information at www.suse.com. */ -import { model, StorageDevice } from "~/types/storage"; +import { model } from "~/types/storage"; +import { storage } from "~/api/system"; import { sprintf } from "sprintf-js"; import { deviceLabel } from "./utils"; import { _ } from "~/i18n"; -export type DriveHeaderProps = { drive: model.Drive; device: StorageDevice }; +export type DriveHeaderProps = { drive: model.Drive; device: storage.Device }; const text = (drive: model.Drive): string => { if (drive.filesystem) { diff --git a/web/src/components/storage/EncryptionSection.tsx b/web/src/components/storage/EncryptionSection.tsx index edb1087930..753c9a9f53 100644 --- a/web/src/components/storage/EncryptionSection.tsx +++ b/web/src/components/storage/EncryptionSection.tsx @@ -24,7 +24,7 @@ import React from "react"; import { Card, CardBody, Content } from "@patternfly/react-core"; import { Link, Page } from "~/components/core"; import { useEncryption } from "~/queries/storage/config-model"; -import { apiModel } from "~/api/storage/types"; +import { apiModel } from "~/api/storage"; import { STORAGE } from "~/routes/paths"; import { _ } from "~/i18n"; import PasswordCheck from "~/components/users/PasswordCheck"; diff --git a/web/src/components/storage/LvmPage.tsx b/web/src/components/storage/LvmPage.tsx index eaee72523b..5af666664b 100644 --- a/web/src/components/storage/LvmPage.tsx +++ b/web/src/components/storage/LvmPage.tsx @@ -36,7 +36,8 @@ import { } from "@patternfly/react-core"; import { Page, SubtleContent } from "~/components/core"; import { useAvailableDevices } from "~/hooks/storage/system"; -import { StorageDevice, model, data } from "~/types/storage"; +import { model, data } from "~/types/storage"; +import { storage } from "~/api/system"; import { useModel } from "~/hooks/storage/model"; import { useVolumeGroup, @@ -48,19 +49,20 @@ import { contentDescription, filesystemLabels, typeDescription } from "./utils/d import { STORAGE as PATHS } from "~/routes/paths"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; +import { deviceSystems, isDrive } from "~/helpers/storage/device"; /** * Hook that returns the devices that can be selected as target to automatically create LVM PVs. * * Filters out devices that are going to be directly formatted. */ -function useLvmTargetDevices(): StorageDevice[] { +function useLvmTargetDevices(): storage.Device[] { const availableDevices = useAvailableDevices(); const model = useModel({ suspense: true }); const targetDevices = useMemo(() => { return availableDevices.filter((candidate) => { - const collection = candidate.isDrive ? model.drives : model.mdRaids; + const collection = isDrive(candidate) ? model.drives : model.mdRaids; const device = collection.find((d) => d.name === candidate.name); return !device || !device.filesystem; }); @@ -81,7 +83,7 @@ function vgNameError( return sprintf(_("Volume group '%s' already exists. Enter a different name."), vgName); } -function targetDevicesError(targetDevices: StorageDevice[]): string | undefined { +function targetDevicesError(targetDevices: storage.Device[]): string | undefined { if (!targetDevices.length) return _("Select at least one disk."); } @@ -100,7 +102,7 @@ export default function LvmPage() { const editVolumeGroup = useEditVolumeGroup(); const allDevices = useLvmTargetDevices(); const [name, setName] = useState(""); - const [selectedDevices, setSelectedDevices] = useState([]); + const [selectedDevices, setSelectedDevices] = useState([]); const [moveMountPoints, setMoveMountPoints] = useState(true); const [errors, setErrors] = useState([]); @@ -199,7 +201,7 @@ export default function LvmPage() { {s} ))} - {device.systems.map((s, i) => ( + {deviceSystems(device).map((s, i) => ( diff --git a/web/src/components/storage/MdRaidEditor.tsx b/web/src/components/storage/MdRaidEditor.tsx index 8d1465ac3e..c0741369ff 100644 --- a/web/src/components/storage/MdRaidEditor.tsx +++ b/web/src/components/storage/MdRaidEditor.tsx @@ -25,13 +25,13 @@ import ConfigEditorItem from "~/components/storage/ConfigEditorItem"; import MdRaidHeader from "~/components/storage/MdRaidHeader"; import DeviceEditorContent from "~/components/storage/DeviceEditorContent"; import SearchedDeviceMenu from "~/components/storage/SearchedDeviceMenu"; -import { model, StorageDevice } from "~/types/storage"; -import { MdRaid } from "~/types/storage/model"; +import { model } from "~/types/storage"; +import { storage } from "~/api/system"; import { useDeleteMdRaid } from "~/hooks/storage/md-raid"; type MdRaidDeviceMenuProps = { raid: model.MdRaid; - selected: StorageDevice; + selected: storage.Device; }; /** @@ -44,7 +44,7 @@ const MdRaidDeviceMenu = ({ raid, selected }: MdRaidDeviceMenuProps): React.Reac return ; }; -type MdRaidEditorProps = { raid: MdRaid; raidDevice: StorageDevice }; +type MdRaidEditorProps = { raid: model.MdRaid; raidDevice: storage.Device }; /** * Component responsible for displaying detailed information and available diff --git a/web/src/components/storage/MdRaidHeader.tsx b/web/src/components/storage/MdRaidHeader.tsx index 503c778088..79a4eaddf5 100644 --- a/web/src/components/storage/MdRaidHeader.tsx +++ b/web/src/components/storage/MdRaidHeader.tsx @@ -20,12 +20,13 @@ * find current contact information at www.suse.com. */ -import { model, StorageDevice } from "~/types/storage"; +import { model } from "~/types/storage"; +import { storage } from "~/api/system"; import { sprintf } from "sprintf-js"; import { deviceLabel } from "./utils"; import { _ } from "~/i18n"; -export type MdRaidHeaderProps = { raid: model.MdRaid; device: StorageDevice }; +export type MdRaidHeaderProps = { raid: model.MdRaid; device: storage.Device }; const text = (raid: model.MdRaid): string => { if (raid.filesystem) { diff --git a/web/src/components/storage/MenuDeviceDescription.tsx b/web/src/components/storage/MenuDeviceDescription.tsx index 6c6b92bf8e..612ad94c2b 100644 --- a/web/src/components/storage/MenuDeviceDescription.tsx +++ b/web/src/components/storage/MenuDeviceDescription.tsx @@ -27,7 +27,7 @@ import { contentDescription, filesystemLabels, } from "~/components/storage/utils/device"; -import { StorageDevice } from "~/types/storage"; +import { storage } from "~/api/system"; /** * Renders the content to be used at a menu entry describing a device. @@ -35,7 +35,7 @@ import { StorageDevice } from "~/types/storage"; * * @param device - Device to represent */ -export default function MenuDeviceDescription({ device }: { device: StorageDevice }) { +export default function MenuDeviceDescription({ device }: { device: storage.Device }) { return ( diff --git a/web/src/components/storage/MountPathMenuItem.tsx b/web/src/components/storage/MountPathMenuItem.tsx index 4a667652d2..8d3860f919 100644 --- a/web/src/components/storage/MountPathMenuItem.tsx +++ b/web/src/components/storage/MountPathMenuItem.tsx @@ -25,7 +25,7 @@ import { useNavigate } from "react-router-dom"; import * as partitionUtils from "~/components/storage/utils/partition"; import { Icon } from "~/components/layout"; import { MenuItem, MenuItemAction } from "@patternfly/react-core"; -import { apiModel } from "~/api/storage/types"; +import { apiModel } from "~/api/storage"; export type MountPathMenuItemProps = { device: apiModel.Partition | apiModel.LogicalVolume; diff --git a/web/src/components/storage/PartitionPage.tsx b/web/src/components/storage/PartitionPage.tsx index 8c2d2d722a..7e4a5a9c28 100644 --- a/web/src/components/storage/PartitionPage.tsx +++ b/web/src/components/storage/PartitionPage.tsx @@ -204,7 +204,7 @@ function usePartition(target: string): system.Device | null { if (target === NEW_PARTITION) return null; - const partitions = device.partitionTable?.partitions || []; + const partitions = device.partitions || []; return partitions.find((p: system.Device) => p.name === target); } @@ -247,7 +247,7 @@ function useUnusedMountPoints(): string[] { /** Unused partitions. Includes the currently used partition when editing (if any). */ function useUnusedPartitions(): system.Device[] { const device = useDevice(); - const allPartitions = device.partitionTable?.partitions || []; + const allPartitions = device.partitions || []; const initialPartitionConfig = useInitialPartitionConfig(); const configuredPartitionConfigs = useModelDevice() .getConfiguredExistingPartitions() diff --git a/web/src/components/storage/ProposalActionsDialog.tsx b/web/src/components/storage/ProposalActionsDialog.tsx index 885d5e4e05..79b775874c 100644 --- a/web/src/components/storage/ProposalActionsDialog.tsx +++ b/web/src/components/storage/ProposalActionsDialog.tsx @@ -25,7 +25,7 @@ import { List, ListItem, ExpandableSection } from "@patternfly/react-core"; import { n_ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { fork } from "radashi"; -import { Action } from "~/types/storage"; +import { Action } from "~/api/storage/proposal"; const ActionsList = ({ actions }: { actions: Action[] }) => { // Some actions (e.g., deleting a LV) are reported as several actions joined by a line break diff --git a/web/src/components/storage/ProposalResultSection.tsx b/web/src/components/storage/ProposalResultSection.tsx index 61803f86e7..4db0f566b3 100644 --- a/web/src/components/storage/ProposalResultSection.tsx +++ b/web/src/components/storage/ProposalResultSection.tsx @@ -27,8 +27,8 @@ import DevicesManager from "~/components/storage/DevicesManager"; import ProposalResultTable from "~/components/storage/ProposalResultTable"; import { ProposalActionsDialog } from "~/components/storage"; import { _, n_, formatList } from "~/i18n"; -import { useDevices } from "~/hooks/storage/system"; -import { useActions } from "~/hooks/storage/proposal"; +import { useDevices as useSystemDevices } from "~/hooks/storage/system"; +import { useDevices as useProposalDevices, useActions } from "~/hooks/storage/proposal"; import { sprintf } from "sprintf-js"; /** @@ -115,8 +115,8 @@ export type ProposalResultSectionProps = { }; export default function ProposalResultSection({ isLoading = false }: ProposalResultSectionProps) { - const system = useDevices("system", { suspense: true }); - const staging = useDevices("result", { suspense: true }); + const system = useSystemDevices({ suspense: true }); + const staging = useProposalDevices({ suspense: true }); const actions = useActions(); const devicesManager = new DevicesManager(system, staging, actions); diff --git a/web/src/components/storage/ProposalResultTable.tsx b/web/src/components/storage/ProposalResultTable.tsx index 66e3cfc085..7aa02149bf 100644 --- a/web/src/components/storage/ProposalResultTable.tsx +++ b/web/src/components/storage/ProposalResultTable.tsx @@ -26,25 +26,25 @@ import { DeviceName, DeviceDetails, DeviceSize, - toStorageDevice, + toDevice, + toPartitionSlot, } from "~/components/storage/device-utils"; import DevicesManager from "~/components/storage/DevicesManager"; import { TreeTable } from "~/components/core"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; import { deviceChildren, deviceSize } from "~/components/storage/utils"; -import { PartitionSlot, StorageDevice } from "~/types/storage"; +import { proposal } from "~/api/storage"; import { TreeTableColumn } from "~/components/core/TreeTable"; -import { DeviceInfo } from "~/api/storage/types"; import { useConfigModel } from "~/queries/storage/config-model"; -type TableItem = StorageDevice | PartitionSlot; +type TableItem = proposal.Device | proposal.PartitionSlot; /** * @component */ const MountPoint = ({ item }: { item: TableItem }) => { - const device = toStorageDevice(item); + const device = toDevice(item); if (!(device && device.filesystem?.mountPath)) return null; @@ -62,7 +62,7 @@ const DeviceCustomDetails = ({ devicesManager: DevicesManager; }) => { const isNew = () => { - const device = toStorageDevice(item); + const device = toDevice(item); if (!device) return false; // FIXME New PVs over a disk is not detected as new. @@ -91,9 +91,11 @@ const DeviceCustomSize = ({ item: TableItem; devicesManager: DevicesManager; }) => { - const device = toStorageDevice(item); + const device = toDevice(item); const isResized = device && devicesManager.isShrunk(device); - const sizeBefore = isResized ? devicesManager.systemDevice(device.sid).size : item.size; + const sizeBefore = isResized + ? devicesManager.systemDevice(device.sid).block.size + : toPartitionSlot(item)?.size; return ( @@ -154,8 +156,8 @@ export default function ProposalResultTable({ devicesManager }: ProposalResultTa items={devices} expandedItems={devices} itemChildren={deviceChildren} - rowClassNames={(item: DeviceInfo) => { - if (!item.sid) return "dimmed-row"; + rowClassNames={(item: TableItem) => { + if (!toDevice(item)) return "dimmed-row"; }} className="proposal-result" /> diff --git a/web/src/components/storage/SearchedDeviceMenu.tsx b/web/src/components/storage/SearchedDeviceMenu.tsx index cb015a3855..94c5683534 100644 --- a/web/src/components/storage/SearchedDeviceMenu.tsx +++ b/web/src/components/storage/SearchedDeviceMenu.tsx @@ -28,15 +28,17 @@ import { useModel } from "~/hooks/storage/model"; import { useSwitchToDrive } from "~/hooks/storage/drive"; import { useSwitchToMdRaid } from "~/hooks/storage/md-raid"; import { deviceBaseName, formattedPath } from "~/components/storage/utils"; -import * as model from "~/types/storage/model"; -import { StorageDevice } from "~/types/storage"; +import { model } from "~/types/storage"; +import { Model } from "~/types/storage/model"; +import { storage } from "~/api/system"; import { sprintf } from "sprintf-js"; import { _, formatList } from "~/i18n"; import DeviceSelectorModal from "./DeviceSelectorModal"; import { MenuItemProps } from "@patternfly/react-core"; import { Icon } from "../layout"; +import { isDrive } from "~/helpers/storage/device"; -const baseName = (device: StorageDevice): string => deviceBaseName(device, true); +const baseName = (device: storage.Device): string => deviceBaseName(device, true); const useOnlyOneOption = (device: model.Drive | model.MdRaid): boolean => { if (device.filesystem && device.filesystem.reuse) return true; @@ -49,7 +51,7 @@ const useOnlyOneOption = (device: model.Drive | model.MdRaid): boolean => { type ChangeDeviceMenuItemProps = { modelDevice: model.Drive | model.MdRaid; - device: StorageDevice; + device: storage.Device; } & MenuItemProps; const ChangeDeviceTitle = ({ modelDevice }) => { @@ -259,11 +261,15 @@ const RemoveEntryOption = ({ device, onClick }: RemoveEntryOptionProps): React.R ); }; -const targetDevices = (modelDevice, model, availableDevices): StorageDevice[] => { +const targetDevices = ( + modelDevice: model.Drive | model.MdRaid, + model: Model, + availableDevices: storage.Device[], +): storage.Device[] => { return availableDevices.filter((availableDev) => { if (modelDevice.name === availableDev.name) return true; - const collection = availableDev.isDrive ? model.drives : model.mdRaids; + const collection = isDrive(availableDev) ? model.drives : model.mdRaids; const device = collection.find((d) => d.name === availableDev.name); if (!device) return true; @@ -273,7 +279,7 @@ const targetDevices = (modelDevice, model, availableDevices): StorageDevice[] => export type SearchedDeviceMenuProps = { modelDevice: model.Drive | model.MdRaid; - selected: StorageDevice; + selected: storage.Device; deleteFn: (device: model.Drive | model.MdRaid) => void; }; @@ -290,13 +296,13 @@ export default function SearchedDeviceMenu({ const [isSelectorOpen, setIsSelectorOpen] = useState(false); const switchToDrive = useSwitchToDrive(); const switchToMdRaid = useSwitchToMdRaid(); - const changeTargetFn = (device: StorageDevice) => { - const hook = device.isDrive ? switchToDrive : switchToMdRaid; + const changeTargetFn = (device: storage.Device) => { + const hook = isDrive(device) ? switchToDrive : switchToMdRaid; hook(modelDevice.name, { name: device.name }); }; const devices = targetDevices(modelDevice, useModel(), useAvailableDevices()); - const onDeviceChange = ([drive]: StorageDevice[]) => { + const onDeviceChange = ([drive]: storage.Device[]) => { setIsSelectorOpen(false); changeTargetFn(drive); }; diff --git a/web/src/components/storage/SpaceActionsTable.tsx b/web/src/components/storage/SpaceActionsTable.tsx index d4b97c3f26..a49cf43938 100644 --- a/web/src/components/storage/SpaceActionsTable.tsx +++ b/web/src/components/storage/SpaceActionsTable.tsx @@ -35,18 +35,19 @@ import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; import { deviceSize, formattedPath } from "~/components/storage/utils"; -import { - DeviceName, - DeviceDetails, - DeviceSize, - toStorageDevice, -} from "~/components/storage/device-utils"; +import { DeviceName, DeviceDetails, DeviceSize, toDevice } from "~/components/storage/device-utils"; import { Icon } from "~/components/layout"; -import { PartitionSlot, SpacePolicyAction, StorageDevice } from "~/types/storage"; -import { apiModel } from "~/api/storage/types"; +import { Device, PartitionSlot } from "~/api/storage/proposal"; +import { apiModel } from "~/api/storage"; import { TreeTableColumn } from "~/components/core/TreeTable"; import { Table, Td, Th, Tr, Thead, Tbody } from "@patternfly/react-table"; import { useConfigModel } from "~/queries/storage/config-model"; +import { isPartition } from "~/helpers/storage/device"; + +export type SpacePolicyAction = { + deviceName: string; + value: "delete" | "resizeIfNeeded"; +}; const isUsedPartition = (partition: apiModel.Partition): boolean => { return partition.filesystem !== undefined; @@ -67,8 +68,9 @@ const useReusedPartition = (name: string): apiModel.Partition | undefined => { * Info about the device. * @component */ -const DeviceInfoContent = ({ device }: { device: StorageDevice }) => { - const minSize = device.shrinking?.supported; +const DeviceInfoContent = ({ device }: { device: Device }) => { + // FIXME + const minSize = device.block?.shrinking?.min; const reused = useReusedPartition(device.name); if (reused) { @@ -79,20 +81,21 @@ const DeviceInfoContent = ({ device }: { device: StorageDevice }) => { } if (minSize) { - const recoverable = device.size - minSize; + const recoverable = device.block.size - minSize; return sprintf( _("Up to %s can be recovered by shrinking the device."), deviceSize(recoverable), ); } - const reasons = device.shrinking.unsupported; + // FXIME + const reasons = device.shrinking.unsupportedReasons; return ( <> {_("The device cannot be shrunk:")} - {reasons.map((reason, idx) => ( + {reasons.map((reason: string, idx: number) => ( {reason} ))} @@ -105,9 +108,9 @@ const DeviceInfoContent = ({ device }: { device: StorageDevice }) => { * @component * * @param {object} props - * @param {StorageDevice} props.device + * @param {Device} props.device */ -const DeviceInfo = ({ device }: { device: StorageDevice }) => { +const DeviceInfo = ({ device }: { device: Device }) => { return ( }>