diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1b1b6f774d..a9d6ef75f1 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1018,7 +1018,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2590,12 +2590,12 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link 0.2.1", ] [[package]] @@ -3941,15 +3941,14 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", "ryu", "serde", - "serde_core", ] [[package]] @@ -5159,7 +5158,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings 0.4.0", ] @@ -5192,6 +5191,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-registry" version = "0.4.0" @@ -5209,7 +5214,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5218,7 +5223,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5227,7 +5232,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5303,7 +5308,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", 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 68a21d32fc..56259ea46a 100644 --- a/rust/agama-manager/src/service.rs +++ b/rust/agama-manager/src/service.rs @@ -449,6 +449,20 @@ impl MessageHandler for Service { } } +#[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?) + } +} + // FIXME: write a macro to forward a message. #[async_trait] impl MessageHandler for Service { diff --git a/rust/agama-server/src/server/web.rs b/rust/agama-server/src/server/web.rs index ada38182cc..a6a78c48f3 100644 --- a/rust/agama-server/src/server/web.rs +++ b/rust/agama-server/src/server/web.rs @@ -27,14 +27,14 @@ use agama_software::Resolvable; use agama_utils::{ actor::Handler, api::{ - event, + event, query, question::{Question, QuestionSpec, UpdateQuestion}, Action, Config, IssueMap, Patch, Status, SystemInfo, }, question, }; use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, response::{IntoResponse, Response}, routing::{get, post, put}, Json, Router, @@ -110,11 +110,11 @@ 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)) .route("/private/resolvables/:id", put(set_resolvables)) .with_state(state)) } -/// Returns the status of the installation. #[utoipa::path( get, path = "/status", @@ -380,6 +380,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)) +} + #[utoipa::path( put, path = "/resolvables/:id", diff --git a/rust/agama-server/src/web/docs/config.rs b/rust/agama-server/src/web/docs/config.rs index 1c0960f5a9..fee20113b8 100644 --- a/rust/agama-server/src/web/docs/config.rs +++ b/rust/agama-server/src/web/docs/config.rs @@ -175,6 +175,7 @@ impl ApiDocBuilder for ConfigApiDocBuilder { .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 6da4d7f5f3..55d8f7e776 100644 --- a/rust/agama-utils/src/api.rs +++ b/rust/agama-utils/src/api.rs @@ -54,6 +54,7 @@ pub use action::Action; pub mod l10n; pub mod manager; pub mod network; +pub mod query; pub mod question; pub mod software; 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, +} diff --git a/rust/share/device.storage.schema.json b/rust/share/device.storage.schema.json index 1adddfe1af..e84b7a51b7 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/rust/share/system.storage.schema.json b/rust/share/system.storage.schema.json index 8ededfb2c5..4382a24ca7 100644 --- a/rust/share/system.storage.schema.json +++ b/rust/share/system.storage.schema.json @@ -55,7 +55,7 @@ "encryptionMethod": { "title": "Encryption method", "enum": [ - "luks1", "luks2", "pervasiveLuks2", "tmpFde", "protectedSwap", "secureSwap", "randomSwap" + "luks1", "luks2", "pervasiveLuks2", "tpmFde", "protectedSwap", "secureSwap", "randomSwap" ] }, "volume": { diff --git a/web/src/App.tsx b/web/src/App.tsx index 161950a3ab..960c36988f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -24,11 +24,8 @@ import React, { useEffect } from "react"; import { Navigate, Outlet, useLocation } from "react-router"; import { Loading } from "~/components/layout"; import { useProduct, useProductChanges } from "~/queries/software"; -import { useProposalChanges } from "~/queries/proposal"; -import { useSystemChanges } from "~/queries/system"; -import { useIssuesChanges } from "~/queries/issues"; +import { useSystemChanges, useProposalChanges, useIssuesChanges } from "~/hooks/api"; 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 +40,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..9787c969c2 --- /dev/null +++ b/web/src/api.ts @@ -0,0 +1,114 @@ +/* + * 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 { IssuesMap } 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 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)); + +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, + patchQuestion, + 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/action.ts b/web/src/api/action.ts new file mode 100644 index 0000000000..5b36cfb563 --- /dev/null +++ b/web/src/api/action.ts @@ -0,0 +1,49 @@ +/* + * 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. + */ + +type Action = ConfigureL10n | ActivateStorage | ProbeStorage; + +type ConfigureL10n = { + configureL10n: L10nSystemConfig; +}; + +type L10nSystemConfig = { + locale?: string; + keymap?: string; +}; + +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 { configureL10n, activateStorage, probeStorage }; +export type { Action, L10nSystemConfig }; diff --git a/web/src/api/storage/types.ts b/web/src/api/config.ts similarity index 76% rename from web/src/api/storage/types.ts rename to web/src/api/config.ts index 09ad88d304..a14daf0ca1 100644 --- a/web/src/api/storage/types.ts +++ b/web/src/api/config.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024-2025] SUSE LLC + * Copyright (c) [2025] SUSE LLC * * All Rights Reserved. * @@ -20,6 +20,13 @@ * find current contact information at www.suse.com. */ -export * from "./types/openapi"; -export * as config from "./types/config"; -export * as apiModel from "./types/model"; +import * as l10n from "~/api/l10n/config"; +import * as storage from "~/api/storage/config"; + +type Config = { + l10n?: l10n.Config; + storage?: storage.Config; +}; + +export { l10n, storage }; +export type { Config }; diff --git a/web/src/api/hostname.ts b/web/src/api/hostname.ts index dfac34b69e..d119ebaf0b 100644 --- a/web/src/api/hostname.ts +++ b/web/src/api/hostname.ts @@ -20,7 +20,9 @@ * find current contact information at www.suse.com. */ -import { get, put } from "~/api/http"; +// @todo Move to the new API. + +import { get, put } from "~/http"; import { Hostname } from "~/types/hostname"; /** diff --git a/web/src/types/issues.ts b/web/src/api/issue.ts similarity index 100% rename from web/src/types/issues.ts rename to web/src/api/issue.ts diff --git a/web/src/api/issues.ts b/web/src/api/issues.ts deleted file mode 100644 index fee355836f..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 "~/api/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/types/config.ts b/web/src/api/l10n/config.ts similarity index 93% rename from web/src/types/config.ts rename to web/src/api/l10n/config.ts index f7248c72cf..925b6c169c 100644 --- a/web/src/types/config.ts +++ b/web/src/api/l10n/config.ts @@ -20,10 +20,10 @@ * find current contact information at www.suse.com. */ -import { Localization } from "./l10n"; - type Config = { - l10n?: Localization; + locale?: string; + keymap?: string; + timezone?: string; }; export type { Config }; diff --git a/web/src/types/proposal.ts b/web/src/api/l10n/proposal.ts similarity index 93% rename from web/src/types/proposal.ts rename to web/src/api/l10n/proposal.ts index 1eacf176f9..c8a2766a7e 100644 --- a/web/src/types/proposal.ts +++ b/web/src/api/l10n/proposal.ts @@ -20,10 +20,10 @@ * find current contact information at www.suse.com. */ -import { Localization } from "./l10n"; - type Proposal = { - l10n?: Localization; + locale?: string; + keymap?: string; + timezone?: string; }; 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..0e27a2de96 100644 --- a/web/src/api/manager.ts +++ b/web/src/api/manager.ts @@ -20,7 +20,9 @@ * find current contact information at www.suse.com. */ -import { get, post } from "~/api/http"; +// @todo Move to the new API. + +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..edd4ea14ec 100644 --- a/web/src/api/network.ts +++ b/web/src/api/network.ts @@ -20,7 +20,9 @@ * find current contact information at www.suse.com. */ -import { del, get, post, put } from "~/api/http"; +// @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 e94538a6fe..e4437e0fb8 100644 --- a/web/src/api/progress.ts +++ b/web/src/api/progress.ts @@ -20,7 +20,9 @@ * find current contact information at www.suse.com. */ -import { get } from "~/api/http"; +// @todo Move to the new API. + +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/types/questions.ts b/web/src/api/question.ts similarity index 97% rename from web/src/types/questions.ts rename to web/src/api/question.ts index d3f799e7a3..0466470426 100644 --- a/web/src/types/questions.ts +++ b/web/src/api/question.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2025] SUSE LLC * * All Rights Reserved. * diff --git a/web/src/api/questions.ts b/web/src/api/questions.ts deleted file mode 100644 index 80680a3bf7..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 "~/api/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/api/software.ts b/web/src/api/software.ts index ca9efc52dc..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, @@ -34,7 +36,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..45de115e7e 100644 --- a/web/src/api/status.ts +++ b/web/src/api/status.ts @@ -20,7 +20,9 @@ * find current contact information at www.suse.com. */ -import { get } from "~/api/http"; +// @todo Move to the new API. + +import { get } from "~/http"; import { InstallerStatus } from "~/types/status"; /** @@ -31,4 +33,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..9350eae137 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,7 @@ * 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"; +export * as proposal from "~/api/storage/proposal"; diff --git a/web/src/api/storage/types/config.ts b/web/src/api/storage/config.ts similarity index 68% rename from web/src/api/storage/types/config.ts rename to web/src/api/storage/config.ts index b6ee6456c1..e20ca9b81e 100644 --- a/web/src/api/storage/types/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/dasd.ts b/web/src/api/storage/dasd.ts index b3962d26ec..d9317970a3 100644 --- a/web/src/api/storage/dasd.ts +++ b/web/src/api/storage/dasd.ts @@ -20,7 +20,9 @@ * find current contact information at www.suse.com. */ -import { post, get, put } from "~/api/http"; +// @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 ea1fbe4fe1..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 "~/api/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 fb808a4187..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. */ -import { del, get, patch, post } from "~/api/http"; -import { ISCSIInitiator, ISCSINode } from "~/api/storage/types"; +// @todo Move to the new API. + +import { del, get, patch, post } from "~/http"; + +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/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 938619c194..a5d1ddd87c 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, @@ -34,7 +33,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. */ @@ -43,7 +42,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; @@ -55,9 +54,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 29406456f5..82aedb2de0 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, @@ -30,7 +29,7 @@ export type EncryptionMethod = | "luks1" | "luks2" | "pervasiveLuks2" - | "tmpFde" + | "tpmFde" | "protectedSwap" | "secureSwap" | "randomSwap"; @@ -43,7 +42,7 @@ export interface System { /** * All relevant devices on the system */ - devices?: StorageDevice[]; + devices?: Device[]; /** * SIDs of the available drives */ @@ -77,7 +76,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; @@ -89,9 +88,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/types/checks.ts b/web/src/api/storage/types/checks.ts deleted file mode 100644 index 0b373aa1e3..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/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..bbf528d3f8 100644 --- a/web/src/api/storage/zfcp.ts +++ b/web/src/api/storage/zfcp.ts @@ -20,7 +20,9 @@ * find current contact information at www.suse.com. */ -import { post, get } from "~/api/http"; +// @todo Move to the new API. + +import { post, get } from "~/http"; import { ZFCPDisk, ZFCPController, ZFCPConfig } from "~/types/zfcp"; /** diff --git a/web/src/types/system.ts b/web/src/api/system.ts similarity index 83% rename from web/src/types/system.ts rename to web/src/api/system.ts index 60fb1f35c1..a7325b498b 100644 --- a/web/src/types/system.ts +++ b/web/src/api/system.ts @@ -20,10 +20,13 @@ * find current contact information at www.suse.com. */ -import { Localization } from "./l10n"; +import * as l10n from "~/api/l10n/system"; +import * as storage from "~/api/storage/system"; type System = { - l10n?: Localization; + 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..da9c19bff5 100644 --- a/web/src/api/users.ts +++ b/web/src/api/users.ts @@ -20,8 +20,10 @@ * 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 "~/api/http"; +import { del, get, patch, post, put } from "~/http"; import { FirstUser, PasswordCheckResult, RootUser } from "~/types/users"; /** 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 2665b9cb21..9421496c9b 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"; 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/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 eb81183a4f..723868a0ad 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"; 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 89b771eddb..ae31d0a670 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 @@ -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 }); @@ -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/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/l10n/KeyboardSelection.test.tsx b/web/src/components/l10n/KeyboardSelection.test.tsx index 7b256dc184..65b06976f6 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 e685692cfa..5885d39e0d 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"; 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 1507979541..8e890ca97b 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 ac2e19fcf8..0b7f89169b 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"; 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 bb8a79f8d5..98d3c2d67f 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 b2da31b0a9..f7d14e971c 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"; 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..044921d0d5 100644 --- a/web/src/components/overview/L10nSection.tsx +++ b/web/src/components/overview/L10nSection.tsx @@ -22,16 +22,16 @@ 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(); - 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/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 919d4fe24d..25e2096cf6 100644 --- a/web/src/components/overview/StorageSection.tsx +++ b/web/src/components/overview/StorageSection.tsx @@ -23,21 +23,19 @@ 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, useIssues } 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 +79,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)); @@ -93,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 199dc88465..0ef83dfa13 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/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 fe65d32c72..3da73f7406 100644 --- a/web/src/components/questions/LuksActivationQuestion.test.tsx +++ b/web/src/components/questions/LuksActivationQuestion.test.tsx @@ -23,11 +23,11 @@ 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"; -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/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 60a2961f83..5e857ebac3 100644 --- a/web/src/components/questions/QuestionWithPassword.test.tsx +++ b/web/src/components/questions/QuestionWithPassword.test.tsx @@ -23,11 +23,11 @@ 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"; -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/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/components/software/SoftwarePage.tsx b/web/src/components/software/SoftwarePage.tsx index c4b04929e5..a15a053dcf 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, @@ -132,7 +132,7 @@ const ReloadSection = ({ * Software page component */ function SoftwarePage(): React.ReactNode { - const issues = useIssues("software"); + const issues = useScopeIssues("software"); const proposal = useSoftwareProposal(); const patterns = usePatterns(); // FIXME: temporarily disabled, the API end point is not implemented yet 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 b0aeb21d86..ad8067e202 100644 --- a/web/src/components/storage/BootSelection.tsx +++ b/web/src/components/storage/BootSelection.tsx @@ -26,22 +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 { useCandidateDevices } from "~/hooks/storage/system"; +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 { useDevices } from "~/queries/storage"; 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; }); @@ -58,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[]; }; /** @@ -69,7 +70,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 fa379a9ede..ebaea725d6 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/ConfigureDeviceMenu.tsx b/web/src/components/storage/ConfigureDeviceMenu.tsx index 8042a7283e..e766ff9b6a 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/EncryptionSettingsPage.tsx b/web/src/components/storage/EncryptionSettingsPage.tsx index d11b482348..4bd8aa03fb 100644 --- a/web/src/components/storage/EncryptionSettingsPage.tsx +++ b/web/src/components/storage/EncryptionSettingsPage.tsx @@ -25,9 +25,9 @@ import { useNavigate } from "react-router"; 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"; @@ -55,9 +55,9 @@ export default function EncryptionSettingsPage() { } }, [encryptionConfig]); - const changePassword = (_, v) => setPassword(v); + const changePassword = (_, v: string) => setPassword(v); - const changeMethod = (_, useTPM) => { + const changeMethod = (_, useTPM: boolean) => { const method = useTPM ? "tpmFde" : "luks2"; setMethod(method); }; 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/FormattableDevicePage.tsx b/web/src/components/storage/FormattableDevicePage.tsx index 6d21d236d0..b4c06a199d 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 46f9359c28..67ce9cb7be 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/LvmPage.tsx b/web/src/components/storage/LvmPage.tsx index b3a4b03311..e263108fcf 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 2f18118f23..4d25a8c88b 100644 --- a/web/src/components/storage/MountPathMenuItem.tsx +++ b/web/src/components/storage/MountPathMenuItem.tsx @@ -25,7 +25,7 @@ import { useNavigate } from "react-router"; 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 5d07970230..1d747b7eeb 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); + const partitions = device.partitions || []; + 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,9 +245,9 @@ 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 allPartitions = device.partitions || []; const initialPartitionConfig = useInitialPartitionConfig(); const configuredPartitionConfigs = useModelDevice() .getConfiguredExistingPartitions() @@ -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 91fe74fe48..a2aa579eb7 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/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/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 8620d65752..aa06e36c36 100644 --- a/web/src/components/storage/ProposalFailedInfo.tsx +++ b/web/src/components/storage/ProposalFailedInfo.tsx @@ -22,15 +22,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 { 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"; 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 || []); @@ -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 a2f496c7a9..6a52c3e06f 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,24 +46,19 @@ 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"; -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"; function InvalidConfigEmptyState(): React.ReactNode { - const errors = useConfigErrors("storage"); - const { mutate: reset } = useResetConfigMutation(); + const errors = useConfigIssues(); + 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..4db0f566b3 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 as useSystemDevices } from "~/hooks/storage/system"; +import { useDevices as useProposalDevices, useActions } from "~/hooks/storage/proposal"; import { sprintf } from "sprintf-js"; /** @@ -114,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..b72fe5af53 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.UnusedSlot; /** * @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/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/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..9781640376 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, UnusedSlot } 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, supportShrink } 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,8 @@ 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 }) => { + const minSize = device.block?.shrinking?.minSize; const reused = useReusedPartition(device.name); if (reused) { @@ -79,20 +80,20 @@ 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; + const reasons = device.block?.shrinking?.reasons || []; return ( <> {_("The device cannot be shrunk:")} - {reasons.map((reason, idx) => ( + {reasons.map((reason: string, idx: number) => ( {reason} ))} @@ -105,9 +106,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 ( }>