diff --git a/service/lib/dinstaller/dbus/storage/manager.rb b/service/lib/dinstaller/dbus/storage/manager.rb index 5d5a9e5cf9..cf9d4496ea 100644 --- a/service/lib/dinstaller/dbus/storage/manager.rb +++ b/service/lib/dinstaller/dbus/storage/manager.rb @@ -245,6 +245,7 @@ def register_iscsi_callbacks backend.iscsi.on_probe do refresh_iscsi_nodes + deprecate_proposal end end @@ -259,6 +260,13 @@ def export_proposal @service.export(@dbus_proposal) end + def deprecate_proposal + return unless dbus_proposal + + dbus_proposal.deprecate + update_validation + end + def refresh_iscsi_nodes nodes = backend.iscsi.nodes iscsi_nodes_tree.update(nodes) diff --git a/service/lib/dinstaller/dbus/storage/proposal.rb b/service/lib/dinstaller/dbus/storage/proposal.rb index df2a6c0dfc..5a237c7df3 100644 --- a/service/lib/dinstaller/dbus/storage/proposal.rb +++ b/service/lib/dinstaller/dbus/storage/proposal.rb @@ -53,6 +53,19 @@ def initialize(backend, logger) dbus_reader :volumes, "aa{sv}" dbus_reader :actions, "aa{sv}" + + dbus_reader :deprecated, "b" + end + + def deprecate + return if backend.deprecated? + + backend.deprecate + dbus_properties_changed(STORAGE_PROPOSAL_INTERFACE, { "Deprecated" => true }, []) + end + + def deprecated + backend.deprecated? end # Devices used by the storage proposal diff --git a/service/lib/dinstaller/storage/manager.rb b/service/lib/dinstaller/storage/manager.rb index 3cc03fc6d7..2e07fd216c 100644 --- a/service/lib/dinstaller/storage/manager.rb +++ b/service/lib/dinstaller/storage/manager.rb @@ -128,10 +128,14 @@ def probe_devices # Calculates the default proposal def calculate_proposal - settings = ProposalSettings.new - # FIXME: by now, the UI only allows to select one disk - device = proposal.available_devices.first&.name - settings.candidate_devices << device if device + settings = proposal.settings + + if !settings + settings = ProposalSettings.new + # FIXME: by now, the UI only allows to select one disk + device = proposal.available_devices.first&.name + settings.candidate_devices << device if device + end proposal.calculate(settings) end diff --git a/service/lib/dinstaller/storage/proposal.rb b/service/lib/dinstaller/storage/proposal.rb index 880e6609e0..e4bf282783 100644 --- a/service/lib/dinstaller/storage/proposal.rb +++ b/service/lib/dinstaller/storage/proposal.rb @@ -41,6 +41,11 @@ module Storage # proposal.calculate(settings) #=> true # proposal.calculated_volumes #=> [Volume, Volume] class Proposal + # Settings used for calculating the proposal + # + # @return [ProposalSettings] + attr_reader :settings + # Constructor # # @param logger [Logger] @@ -56,6 +61,14 @@ def on_calculate(&block) @on_calculate_callbacks << block end + def deprecate + @deprecated = true + end + + def deprecated? + !!@deprecated + end + # Available devices for installation # # @return [Array] @@ -117,11 +130,12 @@ def calculated_settings # @param settings [ProposalSettings] settings to calculate the proposal # @return [Boolean] whether the proposal was correctly calculated def calculate(settings = nil) - settings ||= ProposalSettings.new - settings.freeze - proposal_settings = to_y2storage_settings(settings) + @deprecated = false + @settings = settings || ProposalSettings.new + @settings.freeze + y2storage_settings = to_y2storage_settings(@settings) - @proposal = new_proposal(proposal_settings) + @proposal = new_proposal(y2storage_settings) storage_manager.proposal = proposal @on_calculate_callbacks.each(&:call) @@ -145,9 +159,11 @@ def validate return [] if proposal.nil? [ - validate_proposal, - validate_available_devices, - validate_candidate_devices + deprecated_proposal_error, + empty_available_devices_error, + empty_candidate_devices_error, + missing_candidate_devices_error, + proposal_error, ].compact end @@ -248,24 +264,38 @@ def storage_manager Y2Storage::StorageManager.instance end - def validate_proposal - return if candidate_devices.empty? || !proposal.failed? + def deprecated_proposal_error + return unless deprecated? - ValidationError.new("Cannot accommodate the required file systems for installation") + ValidationError.new("The proposal is deprecated") end - def validate_available_devices + def empty_available_devices_error return if available_devices.any? ValidationError.new("There is no suitable device for installation") end - def validate_candidate_devices - return if available_devices.empty? || candidate_devices.any? + def empty_candidate_devices_error + return if candidate_devices.any? ValidationError.new("No devices are selected for installation") end + def missing_candidate_devices_error + available_names = available_devices.map(&:name) + missing = candidate_devices - available_names + return if missing.none? + + ValidationError.new("Some selected devices are not found in the system") + end + + def proposal_error + return unless proposal.failed? + + ValidationError.new("Cannot accommodate the required file systems for installation") + end + # Adjusts the encryption-related settings of the given Y2Storage::ProposalSettings object # # @param settings [Y2Storage::ProposalSettings] diff --git a/web/src/client/storage.js b/web/src/client/storage.js index bfac92a1d7..69dfa8537a 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -24,11 +24,13 @@ import DBusClient from "./dbus"; import { WithStatus, WithProgress, WithValidation } from "./mixins"; +const STORAGE_IFACE = "org.opensuse.DInstaller.Storage1"; const PROPOSAL_CALCULATOR_IFACE = "org.opensuse.DInstaller.Storage1.Proposal.Calculator"; const ISCSI_NODE_IFACE = "org.opensuse.DInstaller.Storage1.ISCSI.Node"; const ISCSI_NODES_NAMESPACE = "/org/opensuse/DInstaller/Storage1/iscsi_nodes"; const ISCSI_INITIATOR_IFACE = "org.opensuse.DInstaller.Storage1.ISCSI.Initiator"; const PROPOSAL_IFACE = "org.opensuse.DInstaller.Storage1.Proposal"; +const PROPOSAL_OBJECT = "/org/opensuse/DInstaller/Storage1/Proposal"; const STORAGE_OBJECT = "/org/opensuse/DInstaller/Storage1"; /** @@ -59,7 +61,14 @@ class ProposalManager { */ constructor(client) { this.client = client; - this.proxies = {}; + this.proxies = { + proposalCalculator: this.client.proxy(PROPOSAL_CALCULATOR_IFACE, STORAGE_OBJECT) + }; + } + + async isDeprecated() { + const proxy = await this.proposalProxy(); + return proxy?.Deprecated; } /** @@ -95,7 +104,7 @@ class ProposalManager { }; }; - const proxy = await this.proposalCalculatorProxy(); + const proxy = await this.proxies.proposalCalculator; return proxy.AvailableDevices.map(buildDevice); } @@ -214,21 +223,14 @@ class ProposalManager { Volumes: { t: "aa{sv}", v: volumes?.map(dbusVolume) } }); - const proxy = await this.proposalCalculatorProxy(); + const proxy = await this.proxies.proposalCalculator; return proxy.Calculate(settings); } - /** - * @private - * Proxy for org.opensuse.DInstaller.Storage1.Proposal.Calculator iface - * - * @returns {Promise} - */ - async proposalCalculatorProxy() { - if (!this.proxies.proposalCalculator) - this.proxies.proposalCalculator = await this.client.proxy(PROPOSAL_CALCULATOR_IFACE, STORAGE_OBJECT); - - return this.proxies.proposalCalculator; + onDeprecate(handler) { + return this.client.onObjectChanged(PROPOSAL_OBJECT, PROPOSAL_IFACE, (changes) => { + if (changes.Deprecated?.v) return handler(); + }); } /** @@ -516,6 +518,14 @@ class StorageBaseClient { this.client = new DBusClient(StorageBaseClient.SERVICE, address); this.proposal = new ProposalManager(this.client); this.iscsi = new ISCSIManager(StorageBaseClient.SERVICE, address); + this.proxies = { + storage: this.client.proxy(STORAGE_IFACE) + }; + } + + async probe() { + const proxy = await this.proxies.storage; + return proxy.Probe(); } } diff --git a/web/src/components/overview/StorageSection.jsx b/web/src/components/overview/StorageSection.jsx index d0587e68f6..0b653142c7 100644 --- a/web/src/components/overview/StorageSection.jsx +++ b/web/src/components/overview/StorageSection.jsx @@ -75,13 +75,16 @@ export default function StorageSection({ showErrors }) { useEffect(() => { const updateProposal = async () => { + const isDeprecated = await cancellablePromise(client.proposal.isDeprecated()); + if (isDeprecated) await cancellablePromise(client.probe()); + const proposal = await cancellablePromise(client.proposal.getData()); const errors = await cancellablePromise(client.getValidationErrors()); dispatch({ type: "UPDATE_PROPOSAL", payload: { proposal, errors } }); }; - updateProposal(); + if (!state.busy) updateProposal(); }, [client, cancellablePromise, state.busy]); useEffect(() => { @@ -102,6 +105,10 @@ export default function StorageSection({ showErrors }) { }); }, [client, cancellablePromise]); + useEffect(() => { + return client.proposal.onDeprecate(() => client.probe()); + }, [client]); + const errors = showErrors ? state.errors : []; const busy = state.busy || !state.proposal; diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index bda752a747..f3f1d3a277 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React, { useReducer, useEffect } from "react"; +import React, { useCallback, useReducer, useEffect } from "react"; import { Alert } from "@patternfly/react-core"; import { Link } from "react-router-dom"; @@ -34,24 +34,21 @@ import { } from "~/components/storage"; const initialState = { - busy: false, + loading: false, proposal: undefined, errors: [] }; const reducer = (state, action) => { switch (action.type) { - case "SET_BUSY" : { - return { ...state, busy: true }; + case "UPDATE_LOADING" : { + const { loading } = action.payload; + return { ...state, loading }; } - case "LOAD": { + case "UPDATE_PROPOSAL": { const { proposal, errors } = action.payload; - return { ...state, proposal, errors, busy: false }; - } - - case "CALCULATE": { - return initialState; + return { ...state, proposal, errors }; } default: { @@ -61,34 +58,46 @@ const reducer = (state, action) => { }; export default function ProposalPage() { - const client = useInstallerClient(); + const { storage: client } = useInstallerClient(); const { cancellablePromise } = useCancellablePromise(); const [state, dispatch] = useReducer(reducer, initialState); - useEffect(() => { - const loadProposal = async () => { - dispatch({ type: "SET_BUSY" }); + const loadProposal = useCallback(async (hooks = {}) => { + dispatch({ type: "UPDATE_LOADING", payload: { loading: true } }); + + if (hooks.before !== undefined) await cancellablePromise(hooks.before()); + const proposal = await cancellablePromise(client.proposal.getData()); + const errors = await cancellablePromise(client.getValidationErrors()); + + dispatch({ type: "UPDATE_PROPOSAL", payload: { proposal, errors } }); + dispatch({ type: "UPDATE_LOADING", payload: { loading: false } }); + }, [client, cancellablePromise]); - const proposal = await cancellablePromise(client.storage.proposal.getData()); - const errors = await cancellablePromise(client.storage.getValidationErrors()); + useEffect(() => { + const probeAndLoad = async () => { + await loadProposal({ before: () => client.probe() }); + }; - dispatch({ - type: "LOAD", - payload: { proposal, errors } - }); + const load = async () => { + const isDeprecated = await cancellablePromise(client.proposal.isDeprecated()); + isDeprecated ? probeAndLoad() : loadProposal(); }; - if (!state.proposal) loadProposal().catch(console.error); - }, [client.storage, cancellablePromise, state.proposal]); + load().catch(console.error); + + return client.proposal.onDeprecate(() => probeAndLoad()); + }, [client, cancellablePromise, loadProposal]); const calculateProposal = async (settings) => { - dispatch({ type: "SET_BUSY" }); - await client.storage.proposal.calculate({ ...state.proposal.result, ...settings }); - dispatch({ type: "CALCULATE" }); + const calculate = async () => { + await client.proposal.calculate({ ...state.proposal.result, ...settings }); + }; + + loadProposal({ before: calculate }).catch(console.error); }; const PageContent = () => { - if (state.busy || state.proposal?.result === undefined) return ; + if (state.loading || state.proposal?.result === undefined) return ; return ( <> diff --git a/web/src/components/storage/ProposalSummary.jsx b/web/src/components/storage/ProposalSummary.jsx index f4d83fb760..ae07b8e510 100644 --- a/web/src/components/storage/ProposalSummary.jsx +++ b/web/src/components/storage/ProposalSummary.jsx @@ -43,9 +43,11 @@ export default function ProposalSummary({ proposal }) { ); } + const deviceLabel = device?.label || candidateDevice; + return ( - Install using device {device.label} and deleting all its content + Install using device {deviceLabel} and deleting all its content ); } diff --git a/web/src/components/storage/ProposalTargetForm.jsx b/web/src/components/storage/ProposalTargetForm.jsx index c1be608c17..a2bf1ba14f 100644 --- a/web/src/components/storage/ProposalTargetForm.jsx +++ b/web/src/components/storage/ProposalTargetForm.jsx @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Form, @@ -30,6 +30,17 @@ import { DeviceSelector } from "~/components/storage"; export default function ProposalTargetForm({ id, proposal, onSubmit }) { const [candidateDevices, setCandidateDevices] = useState(proposal.result.candidateDevices); + useEffect(() => { + const existCandidates = () => { + const devices = proposal.result.candidateDevices; + const device = proposal.availableDevices.find(d => d.id === devices[0]); + return device !== undefined; + }; + + if (!existCandidates()) + setCandidateDevices([proposal.availableDevices[0].id]); + }, [proposal]); + const accept = (e) => { e.preventDefault(); onSubmit({ candidateDevices }); diff --git a/web/src/index.js b/web/src/index.js index b30a12b47d..64d847b54c 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -19,7 +19,7 @@ * find current contact information at www.suse.com. */ -import React, { StrictMode } from "react"; +import React from "react"; import { createRoot } from "react-dom/client"; import "core-js/stable"; import "regenerator-runtime/runtime"; @@ -52,28 +52,26 @@ const container = document.getElementById("root"); const root = createRoot(container); root.render( - - - - - - - }> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - } /> + + + + + + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - - - - - - + } /> + + + + + + );