From 423e20b313e83037e81d8f04833a38adbdea2185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:07:34 +0000 Subject: [PATCH 1/8] feat(storage): add D-Bus method for reprobing --- .../bus/org.opensuse.Agama.Storage1.bus.xml | 2 + doc/dbus/org.opensuse.Agama.Storage1.doc.xml | 5 ++ service/lib/agama/dbus/storage/manager.rb | 9 ++-- service/lib/agama/storage/manager.rb | 26 +++++++---- service/test/agama/storage/manager_test.rb | 46 ++++++++++++++++++- 5 files changed, 74 insertions(+), 14 deletions(-) diff --git a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml index 9eedf3e6e6..93dd879063 100644 --- a/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml +++ b/doc/dbus/bus/org.opensuse.Agama.Storage1.bus.xml @@ -45,6 +45,8 @@ + + diff --git a/doc/dbus/org.opensuse.Agama.Storage1.doc.xml b/doc/dbus/org.opensuse.Agama.Storage1.doc.xml index a4ac2393f4..3a8beb2e6c 100644 --- a/doc/dbus/org.opensuse.Agama.Storage1.doc.xml +++ b/doc/dbus/org.opensuse.Agama.Storage1.doc.xml @@ -7,6 +7,11 @@ + + + diff --git a/service/lib/agama/dbus/storage/manager.rb b/service/lib/agama/dbus/storage/manager.rb index 2554e8fd29..918f43de6d 100644 --- a/service/lib/agama/dbus/storage/manager.rb +++ b/service/lib/agama/dbus/storage/manager.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2024] SUSE LLC +# Copyright (c) [2022-2025] SUSE LLC # # All Rights Reserved. # @@ -87,13 +87,15 @@ def issues STORAGE_INTERFACE = "org.opensuse.Agama.Storage1" private_constant :STORAGE_INTERFACE - def probe + # @param keep_config [Boolean] Whether to use the current storage config for calculating + # the proposal. + def probe(keep_config: false) busy_while do # Clean trees in advance to avoid having old objects exported in D-Bus. system_devices_tree.clean staging_devices_tree.clean - backend.probe + backend.probe(keep_config: keep_config) end end @@ -162,6 +164,7 @@ def deprecated_system dbus_interface STORAGE_INTERFACE do dbus_method(:Probe) { probe } + dbus_method(:Reprobe) { probe(keep_config: true) } dbus_method(:SetConfig, "in serialized_config:s, out result:u") do |serialized_config| busy_while { apply_config(serialized_config) } end diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index 9d8c4463ac..48a28c427b 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2024] SUSE LLC +# Copyright (c) [2022-2025] SUSE LLC # # All Rights Reserved. # @@ -113,14 +113,21 @@ def on_probe(&block) end # Probes storage devices and performs an initial proposal - def probe + # + # @param keep_config [Boolean] Whether to use the current storage config for calculating the + # proposal. + def probe(keep_config: false) start_progress_with_size(4) product_config.pick_product(software.selected_product) check_multipath progress.step(_("Activating storage devices")) { activate_devices } progress.step(_("Probing storage devices")) { probe_devices } - progress.step(_("Calculating the storage proposal")) { calculate_proposal } + progress.step(_("Calculating the storage proposal")) do + calculate_proposal(keep_config: keep_config) + end progress.step(_("Selecting Linux Security Modules")) { security.probe } + # The system is not deprecated anymore + self.deprecated_system = false update_issues @on_probe_callbacks&.each(&:call) end @@ -216,14 +223,15 @@ def probe_devices iscsi.probe Y2Storage::StorageManager.instance.probe(callbacks) - - # The system is not deprecated anymore - self.deprecated_system = false end - # Calculates the proposal using the storage config from the product. - def calculate_proposal - config_json = ConfigJSONReader.new(product_config).read + # Calculates the proposal. + # + # @param keep_config [Boolean] Whether to use the current storage config for calculating the + # proposal. If false, then the default config from the product is used. + def calculate_proposal(keep_config: false) + config_json = proposal.storage_json if keep_config + config_json ||= ConfigJSONReader.new(product_config).read proposal.calculate_from_json(config_json) end diff --git a/service/test/agama/storage/manager_test.rb b/service/test/agama/storage/manager_test.rb index d4515423d9..ec43df5d01 100644 --- a/service/test/agama/storage/manager_test.rb +++ b/service/test/agama/storage/manager_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2024] SUSE LLC +# Copyright (c) [2022-2025] SUSE LLC # # All Rights Reserved. # @@ -167,6 +167,10 @@ allow(proposal).to receive(:issues).and_return(proposal_issues) allow(proposal).to receive(:available_devices).and_return(devices) allow(proposal).to receive(:calculate_from_json) + allow(proposal).to receive(:storage_json).and_return(current_config) + + allow_any_instance_of(Agama::Storage::ConfigJSONReader) + .to receive(:read).and_return(default_config) allow(config).to receive(:pick_product) allow(iscsi).to receive(:activate) @@ -181,6 +185,26 @@ let(:proposal) { Agama::Storage::Proposal.new(config, logger: logger) } + let(:default_config) do + { + storage: { + drives: [ + search: "/dev/vda1" + ] + } + } + end + + let(:current_config) do + { + storage: { + drives: [ + search: "/dev/vda2" + ] + } + } + end + let(:iscsi) { Agama::Storage::ISCSI::Manager.new } let(:devices) { [disk1, disk2] } @@ -194,7 +218,7 @@ let(:callback) { proc {} } - it "probes the storage devices and calculates a proposal with the default settings" do + it "probes the storage devices and calculates a proposal" do expect(config).to receive(:pick_product).with("ALP") expect(iscsi).to receive(:activate) expect(y2storage_manager).to receive(:activate) do |callbacks| @@ -237,6 +261,24 @@ storage.probe end + context "if :keep_config is false" do + let(:keep_config) { false } + + it "calculates a proposal using the default product config" do + expect(proposal).to receive(:calculate_from_json).with(default_config) + storage.probe(keep_config: keep_config) + end + end + + context "if :keep_config is true" do + let(:keep_config) { true } + + it "calculates a proposal using the current config" do + expect(proposal).to receive(:calculate_from_json).with(current_config) + storage.probe(keep_config: keep_config) + end + end + context "if there are available devices" do let(:devices) { [disk1] } From 8d50eba664db16b224ca23af0ca3da8523fc1aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:09:01 +0000 Subject: [PATCH 2/8] feat(storage): add reprobe endpoint --- rust/agama-lib/src/storage/client.rs | 7 ++++++- .../agama-lib/src/storage/proxies/storage1.rs | 5 ++++- rust/agama-server/src/storage/web.rs | 20 +++++++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/rust/agama-lib/src/storage/client.rs b/rust/agama-lib/src/storage/client.rs index cfbf228609..b99626b13d 100644 --- a/rust/agama-lib/src/storage/client.rs +++ b/rust/agama-lib/src/storage/client.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2024-2025] SUSE LLC // // All Rights Reserved. // @@ -139,6 +139,11 @@ impl<'a> StorageClient<'a> { Ok(self.storage_proxy.probe().await?) } + /// Runs the reprobing process + pub async fn reprobe(&self) -> Result<(), ServiceError> { + Ok(self.storage_proxy.reprobe().await?) + } + /// Set the storage config according to the JSON schema pub async fn set_config(&self, settings: StorageSettings) -> Result { Ok(self diff --git a/rust/agama-lib/src/storage/proxies/storage1.rs b/rust/agama-lib/src/storage/proxies/storage1.rs index 67e31c247d..563bbfd356 100644 --- a/rust/agama-lib/src/storage/proxies/storage1.rs +++ b/rust/agama-lib/src/storage/proxies/storage1.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2024-2025] SUSE LLC // // All Rights Reserved. // @@ -56,6 +56,9 @@ pub trait Storage1 { /// Probe method fn probe(&self) -> zbus::Result<()>; + /// Reprobe method + fn reprobe(&self) -> zbus::Result<()>; + /// Set the storage config according to the JSON schema fn set_config(&self, settings: &str) -> zbus::Result; diff --git a/rust/agama-server/src/storage/web.rs b/rust/agama-server/src/storage/web.rs index 26c0b60bef..260f2a9280 100644 --- a/rust/agama-server/src/storage/web.rs +++ b/rust/agama-server/src/storage/web.rs @@ -1,4 +1,4 @@ -// Copyright (c) [2024] SUSE LLC +// Copyright (c) [2024-2025] SUSE LLC // // All Rights Reserved. // @@ -116,6 +116,7 @@ pub async fn storage_service(dbus: zbus::Connection) -> Result>) -> Result, Error> Ok(Json(state.client.probe().await?)) } +/// Reprobes the storage devices. +#[utoipa::path( + post, + path = "/reprobe", + context_path = "/api/storage", + responses( + (status = 200, description = "Devices were probed and the proposal was recalculated"), + (status = 400, description = "The D-Bus service could not perform the action") + ), + operation_id = "storage_reprobe" +)] +async fn reprobe(State(state): State>) -> Result, Error> { + Ok(Json(state.client.reprobe().await?)) +} + /// Gets whether the system is in a deprecated status. /// /// The system is usually set as deprecated as effect of managing some kind of devices, for example, From 3dd10237b5c494dff36e2c52e1ac2ae90532bf04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:11:25 +0000 Subject: [PATCH 3/8] feat(web): adapt storage api - Add #reprobe - Remove #refresh - Move useful methods from proposal file. --- web/src/api/storage.ts | 39 ++++++++++++++++++--------------- web/src/api/storage/proposal.ts | 36 ++++-------------------------- 2 files changed, 25 insertions(+), 50 deletions(-) diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts index b09a0b64f2..e0c4b3852c 100644 --- a/web/src/api/storage.ts +++ b/web/src/api/storage.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -22,16 +22,17 @@ import { get, post, put } from "~/api/http"; import { Job } from "~/types/job"; -import { calculate, fetchSettings } from "~/api/storage/proposal"; -import { config, configModel } from "~/api/storage/types"; +import { Action, config, configModel, 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"); + const fetchConfig = (): Promise => get("/api/storage/config").then((config) => config.storage); @@ -42,6 +43,17 @@ const setConfig = (config: config.Config) => put("/api/storage/config", { storag const setConfigModel = (model: configModel.Config) => put("/api/storage/config_model", model); +const fetchUsableDevices = (): Promise => get(`/api/storage/proposal/usable_devices`); + +const fetchProductParams = (): Promise => get("/api/storage/product/params"); + +const fetchDefaultVolume = (mountPath: string): Promise => { + const path = encodeURIComponent(mountPath); + return get(`/api/storage/product/volume_for?mount_path=${path}`); +}; + +const fetchActions = (): Promise => get("/api/storage/devices/actions"); + /** * Returns the list of jobs */ @@ -53,26 +65,17 @@ const fetchStorageJobs = (): Promise => get("/api/storage/jobs"); const findStorageJob = (id: string): Promise => fetchStorageJobs().then((jobs: Job[]) => jobs.find((value) => value.id === id)); -/** - * Refreshes the storage layer. - * - * It does the probing again and recalculates the proposal with the same - * settings. Internally, it is composed of three different API calls - * (retrieve the settings, probe the system, and calculate the proposal). - */ -const refresh = async (): Promise => { - const settings = await fetchSettings(); - await probe().catch(console.log); - await calculate(settings).catch(console.log); -}; - export { probe, + reprobe, fetchConfig, fetchConfigModel, setConfig, setConfigModel, + fetchUsableDevices, + fetchProductParams, + fetchDefaultVolume, + fetchActions, fetchStorageJobs, findStorageJob, - refresh, }; diff --git a/web/src/api/storage/proposal.ts b/web/src/api/storage/proposal.ts index 22ce49f066..333f3ae633 100644 --- a/web/src/api/storage/proposal.ts +++ b/web/src/api/storage/proposal.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -20,38 +20,10 @@ * find current contact information at www.suse.com. */ -import { get, put } from "../http"; -import { - Action, - ProductParams, - ProposalSettings, - ProposalSettingsPatch, - Volume, -} from "~/api/storage/types"; - -const fetchUsableDevices = (): Promise => get(`/api/storage/proposal/usable_devices`); - -const fetchProductParams = (): Promise => get("/api/storage/product/params"); - -const fetchDefaultVolume = (mountPath: string): Promise => { - const path = encodeURIComponent(mountPath); - return get(`/api/storage/product/volume_for?mount_path=${path}`); -}; - -// NOTE: the settings might not exist. -const fetchSettings = (): Promise => - get("/api/storage/proposal/settings").catch(() => null); - -const fetchActions = (): Promise => get("/api/storage/devices/actions"); +import { put } from "../http"; +import { ProposalSettingsPatch } from "~/api/storage/types"; const calculate = (settings: ProposalSettingsPatch) => put("/api/storage/proposal/settings", settings); -export { - fetchUsableDevices, - fetchProductParams, - fetchDefaultVolume, - fetchSettings, - fetchActions, - calculate, -}; +export { calculate }; From 3186e9b7d949199233e8ecb46231e37cb34033e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:24:21 +0000 Subject: [PATCH 4/8] feat(web): add query for reprobing storage --- web/src/queries/storage.ts | 41 +++++++++++++++----------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/web/src/queries/storage.ts b/web/src/queries/storage.ts index 957bf4c956..cd2b7c4f71 100644 --- a/web/src/queries/storage.ts +++ b/web/src/queries/storage.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -28,15 +28,17 @@ import { useSuspenseQuery, } from "@tanstack/react-query"; import React from "react"; -import { fetchConfig, refresh, setConfig } from "~/api/storage"; -import { fetchDevices, fetchDevicesDirty } from "~/api/storage/devices"; import { - calculate, + fetchConfig, + setConfig, fetchActions, fetchDefaultVolume, fetchProductParams, fetchUsableDevices, -} from "~/api/storage/proposal"; + reprobe, +} from "~/api/storage"; +import { calculate } from "~/api/storage/proposal"; +import { fetchDevices, fetchDevicesDirty } from "~/api/storage/devices"; import { useInstallerClient } from "~/context/installer"; import { config, @@ -314,30 +316,19 @@ const useDeprecatedChanges = () => { }); }; -type RefreshHandler = { - onStart?: () => void; - onFinish?: () => void; -}; - /** * Hook that reprobes the devices and recalculates the proposal using the current settings. */ -const useRefresh = (handler?: RefreshHandler) => { +const useReprobeMutation = () => { const queryClient = useQueryClient(); - const deprecated = useDeprecated(); - - handler ||= {}; - handler.onStart ||= () => undefined; - handler.onFinish ||= () => undefined; - - React.useEffect(() => { - if (!deprecated) return; + const query = { + mutationFn: async () => { + await reprobe(); + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["storage"] }), + }; - handler.onStart(); - refresh() - .then(() => queryClient.invalidateQueries({ queryKey: ["storage"] })) - .then(() => handler.onFinish()); - }, [handler, deprecated, queryClient]); + return useMutation(query); }; export { @@ -352,7 +343,7 @@ export { useProposalMutation, useDeprecated, useDeprecatedChanges, - useRefresh, + useReprobeMutation, }; export * from "~/queries/storage/config-model"; From 267fa7cd040175988521d901381df2cbc32bb37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:27:03 +0000 Subject: [PATCH 5/8] fix(web): reprobe the system --- web/src/components/storage/ProposalPage.tsx | 26 ++++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index 620d14731b..9f8347be87 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2024] SUSE LLC + * Copyright (c) [2022-2025] SUSE LLC * * All Rights Reserved. * @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import React, { useState } from "react"; +import React from "react"; import { Grid, GridItem, SplitItem } from "@patternfly/react-core"; import { Page } from "~/components/core/"; import { Loading } from "~/components/layout"; @@ -32,7 +32,13 @@ import ConfigEditorMenu from "./ConfigEditorMenu"; import { toValidationError } from "~/utils"; import { useIssues } from "~/queries/issues"; import { IssueSeverity } from "~/types/issues"; -import { useDevices, useProposalResult, useRefresh } from "~/queries/storage"; +import { + useDevices, + useDeprecated, + useDeprecatedChanges, + useProposalResult, + useReprobeMutation, +} from "~/queries/storage"; import { _ } from "~/i18n"; /** @@ -59,21 +65,23 @@ export const NOT_AFFECTED = { }; export default function ProposalPage() { - const [isLoading, setIsLoading] = useState(false); const systemDevices = useDevices("system"); const stagingDevices = useDevices("result"); + const isDeprecated = useDeprecated(); + const { mutateAsync: reprobe } = useReprobeMutation(); const { actions } = useProposalResult(); - useRefresh({ - onStart: () => setIsLoading(true), - onFinish: () => setIsLoading(false), - }); + useDeprecatedChanges(); + + React.useEffect(() => { + if (isDeprecated) reprobe().catch(console.log); + }, [isDeprecated, reprobe]); const errors = useIssues("storage") .filter((s) => s.severity === IssueSeverity.Error) .map(toValidationError); - if (isLoading) { + if (isDeprecated) { return ( From ad8b6c2747ae9c8a5258eba04f6e4f1a072ca78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:28:30 +0000 Subject: [PATCH 6/8] fix(web): avoid to fail if a storage device is not found --- web/src/components/storage/ConfigEditor.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/components/storage/ConfigEditor.tsx b/web/src/components/storage/ConfigEditor.tsx index acc377a257..a804198617 100644 --- a/web/src/components/storage/ConfigEditor.tsx +++ b/web/src/components/storage/ConfigEditor.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2024] SUSE LLC + * Copyright (c) [2024-2025] SUSE LLC * * All Rights Reserved. * @@ -34,6 +34,12 @@ export default function ConfigEditor() { {model.drives.map((drive, i) => { const device = devices.find((d) => d.name === drive.name); + /** + * @fixme Make DriveEditor to work when the device is not found (e.g., after disabling + * a iSCSI device). + */ + if (device === undefined) return null; + return ( From ee3035b845321a4eee0cfda1783b261c1867647c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:31:42 +0000 Subject: [PATCH 7/8] fix(web): adapt back button of iSCSI page --- web/src/components/storage/ISCSIPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/storage/ISCSIPage.tsx b/web/src/components/storage/ISCSIPage.tsx index 79c7852505..a61fa35d82 100644 --- a/web/src/components/storage/ISCSIPage.tsx +++ b/web/src/components/storage/ISCSIPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2023-2024] SUSE LLC + * Copyright (c) [2023-2025] SUSE LLC * * All Rights Reserved. * @@ -44,8 +44,8 @@ export default function ISCSIPage() { - - {_("Back to device selection")} + + {_("Back")} From 26a5dc3681119fc734e16fb235bf762c426e4632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Fri, 17 Jan 2025 06:57:14 +0000 Subject: [PATCH 8/8] fix(web): remove reprobing from device selector - DeviceSelector component is not used anymore. --- web/src/components/storage/DeviceSelection.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/web/src/components/storage/DeviceSelection.tsx b/web/src/components/storage/DeviceSelection.tsx index aefffe1f81..5fc87aae9e 100644 --- a/web/src/components/storage/DeviceSelection.tsx +++ b/web/src/components/storage/DeviceSelection.tsx @@ -27,17 +27,11 @@ import { Page } from "~/components/core"; import { DeviceSelectorTable } from "~/components/storage"; import DevicesTechMenu from "./DevicesTechMenu"; import { ProposalTarget, StorageDevice } from "~/types/storage"; -import { - useAvailableDevices, - useProposalMutation, - useProposalResult, - useRefresh, -} from "~/queries/storage"; +import { useAvailableDevices, useProposalMutation, useProposalResult } from "~/queries/storage"; import { deviceChildren } from "~/components/storage/utils"; import { compact } from "~/utils"; import a11y from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; import { _ } from "~/i18n"; -import { Loading } from "~/components/layout"; const SELECT_DISK_ID = "select-disk"; const CREATE_LVM_ID = "create-lvm"; @@ -59,17 +53,11 @@ export default function DeviceSelection() { const availableDevices = useAvailableDevices(); const updateProposal = useProposalMutation(); const navigate = useNavigate(); - const [isLoading, setIsLoading] = useState(false); const [state, setState] = useState({}); const isTargetDisk = state.target === ProposalTarget.DISK; const isTargetNewLvmVg = state.target === ProposalTarget.NEW_LVM_VG; - useRefresh({ - onStart: () => setIsLoading(true), - onFinish: () => setIsLoading(false), - }); - useEffect(() => { if (state.target !== undefined) return; @@ -130,8 +118,6 @@ physical volumes will be created on demand as new partitions at the selected \ devices.", ).split(/[[\]]/); - if (isLoading) return ; - return (