diff --git a/rust/agama-lib/src/progress.rs b/rust/agama-lib/src/progress.rs index ab9bd5aa89..d693ade1cb 100644 --- a/rust/agama-lib/src/progress.rs +++ b/rust/agama-lib/src/progress.rs @@ -54,6 +54,7 @@ use zbus::Connection; /// Represents the progress for an Agama service. #[derive(Clone, Default, Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct Progress { /// Current step pub current_step: u32, diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index 5a64753915..d7e5944bbd 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -23,6 +23,10 @@ trait Progress { /// TotalSteps property #[dbus_proxy(property)] fn total_steps(&self) -> zbus::Result; + + /// Steps property + #[dbus_proxy(property)] + fn steps(&self) -> zbus::Result>; } #[dbus_proxy( diff --git a/rust/agama-server/src/web/common.rs b/rust/agama-server/src/web/common.rs index b7e88d0686..814a811811 100644 --- a/rust/agama-server/src/web/common.rs +++ b/rust/agama-server/src/web/common.rs @@ -165,10 +165,22 @@ struct ProgressState<'a> { proxy: ProgressProxy<'a>, } -async fn progress(State(state): State>) -> Result, Error> { +/// Information about the current progress sequence. +#[derive(Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProgressSequence { + /// Sequence steps if known in advance + steps: Vec, + #[serde(flatten)] + progress: Progress, +} + +async fn progress(State(state): State>) -> Result, Error> { let proxy = state.proxy; let progress = Progress::from_proxy(&proxy).await?; - Ok(Json(progress)) + let steps = proxy.steps().await?; + let sequence = ProgressSequence { steps, progress }; + Ok(Json(sequence)) } #[pin_project] diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 2c0bffdd96..714a954bb5 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,11 @@ +------------------------------------------------------------------- +Thu Jun 20 05:32:42 UTC 2024 - Imobach Gonzalez Sosa + +- Add support for progress sequences with pre-defined descriptions + (gh#openSUSE/agama#1356). +- Fix the "Progress" signal to use camelCase + (gh#openSUSE/agama#1356). + ------------------------------------------------------------------- Fri Jun 14 06:17:52 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/lib/agama/dbus/interfaces/progress.rb b/service/lib/agama/dbus/interfaces/progress.rb index 88f556e003..104952910f 100644 --- a/service/lib/agama/dbus/interfaces/progress.rb +++ b/service/lib/agama/dbus/interfaces/progress.rb @@ -78,6 +78,15 @@ def progress_finished backend.progress.finished? end + # Returns the known step descriptions + # + # @return [Array] + def progress_steps + return [] unless backend.progress + + backend.progress.descriptions + end + # D-Bus properties of the Progress interface # # @return [Hash] @@ -104,6 +113,7 @@ def self.included(base) dbus_reader :progress_total_steps, "u", dbus_name: "TotalSteps" dbus_reader :progress_current_step, "(us)", dbus_name: "CurrentStep" dbus_reader :progress_finished, "b", dbus_name: "Finished" + dbus_reader :progress_steps, "as", dbus_name: "Steps" end end end diff --git a/service/lib/agama/manager.rb b/service/lib/agama/manager.rb index 90326e0b83..956f46c5b0 100644 --- a/service/lib/agama/manager.rb +++ b/service/lib/agama/manager.rb @@ -84,9 +84,11 @@ def config_phase service_status.busy installation_phase.config - start_progress(2) - progress.step(_("Probing Storage")) { storage.probe } - progress.step(_("Probing Software")) { software.probe } + start_progress_with_descriptions( + _("Analyze disks"), _("Configure software") + ) + progress.step { storage.probe } + progress.step { software.probe } logger.info("Config phase done") rescue StandardError => e @@ -102,11 +104,15 @@ def config_phase def install_phase service_status.busy installation_phase.install - start_progress(7) + start_progress_with_descriptions( + _("Prepare disks"), + _("Install software"), + _("Configure the system") + ) Yast::Installation.destdir = "/mnt" - progress.step(_("Partitioning")) do + progress.step do storage.install proxy.propose # propose software after /mnt is already separated, so it uses proper @@ -114,14 +120,15 @@ def install_phase software.propose end - progress.step(_("Installing Software")) { software.install } - - on_target do - progress.step(_("Writing Users")) { users.write } - progress.step(_("Writing Network Configuration")) { network.install } - progress.step(_("Saving Language Settings")) { language.finish } - progress.step(_("Writing repositories information")) { software.finish } - progress.step(_("Finishing storage configuration")) { storage.finish } + progress.step { software.install } + progress.step do + on_target do + users.write + network.install + language.finish + software.finish + storage.finish + end end logger.info("Install phase done") diff --git a/service/lib/agama/progress.rb b/service/lib/agama/progress.rb index 6a71b511ec..797ecda71b 100644 --- a/service/lib/agama/progress.rb +++ b/service/lib/agama/progress.rb @@ -24,9 +24,15 @@ module Agama # # It allows to configure callbacks to be called on each step and also when the progress finishes. # + # In most cases all steps are known in advance (e.g., "probing software", "probing storage", etc.) + # but, in some situations, only the number of steps is known (e.g., "Installing package X"). + # + # Use the Progress.with_descriptions to initialize a progress with known step descriptions and + # Progress.with_size when only the number of steps is known + # # @example # - # progress = Progress.new(3) # 3 steps + # progress = Progress.with_size(3) # 3 steps # progress.on_change { puts progress.message } # configures callbacks # progress.on_finish { puts "finished" } # configures callbacks # @@ -45,6 +51,14 @@ module Agama # # progress.finished? #=> true # progress.current_step #=> nil + # + # @example Progress with known step descriptions + # + # progress = Progress.with_descriptions(["Partitioning", "Installing", "Finishing"]) + # progress.step { partitioning } # next step + # progress.current_step.description #=> "Partitioning" + # progress.step("Installing packages") { installing } # overwrite the description + # progress.current_step.description # "Installing packages" class Progress # Step of the progress class Step @@ -73,11 +87,30 @@ def initialize(id, description) # @return [Integer] attr_reader :total_steps + # Step descriptions in case they are known + # + # @return [Array] + attr_reader :descriptions + + class << self + def with_size(size) + new(size: size) + end + + def with_descriptions(descriptions) + new(descriptions: descriptions) + end + end + # Constructor # - # @param total_steps [Integer] total number of steps - def initialize(total_steps) - @total_steps = total_steps + # @param descriptions [Array] Steps of the progress sequence. This argument + # has precedence over the `size` + # @param size [Integer] total number of steps of the progress sequence + def initialize(descriptions: [], size: nil) + @descriptions = descriptions || [] + @total_steps = descriptions.size unless descriptions.empty? + @total_steps ||= size @current_step = nil @counter = 0 @finished = false @@ -99,15 +132,16 @@ def current_step # It calls the `on_change` callbacks and then runs the given block, if any. It also calls # `on_finish` callbacks after the last step. # - # @param description [String] description of the step + # @param description [String, nil] description of the step # @param block [Proc] # # @return [Object, nil] result of the given block or nil if no block is given - def step(description, &block) + def step(description = nil, &block) return if finished? @counter += 1 - @current_step = Step.new(@counter, description) + step_description = description || description_for(@counter) + @current_step = Step.new(@counter, step_description) @on_change_callbacks.each(&:call) result = block_given? ? block.call : nil @@ -154,5 +188,11 @@ def to_s "#{current_step.description} (#{@counter}/#{total_steps})" end + + private + + def description_for(step) + @descriptions[step - 1] || "" + end end end diff --git a/service/lib/agama/software/callbacks/progress.rb b/service/lib/agama/software/callbacks/progress.rb index 04a32f5a3e..60ebb3f77f 100644 --- a/service/lib/agama/software/callbacks/progress.rb +++ b/service/lib/agama/software/callbacks/progress.rb @@ -41,8 +41,8 @@ def initialize(pkg_count, progress) end def setup - Yast::Pkg.CallbackDonePackage( - fun_ref(method(:package_installed), "string (integer, string)") + Yast::Pkg.CallbackStartPackage( + fun_ref(method(:start_package), "void (string, string, string, integer, boolean)") ) end @@ -55,12 +55,8 @@ def fun_ref(method, signature) Yast::FunRef.new(method, signature) end - # TODO: error handling - def package_installed(_error, _reason) - @installed += 1 - progress.step(msg) - - "" + def start_package(package, _file, _summary, _size, _other) + progress.step("Installing #{package}") end def msg diff --git a/service/lib/agama/software/manager.rb b/service/lib/agama/software/manager.rb index ddb08989fd..3a8ef6734e 100644 --- a/service/lib/agama/software/manager.rb +++ b/service/lib/agama/software/manager.rb @@ -132,11 +132,11 @@ def probe logger.info "Probing software" if repositories.empty? - start_progress(3) + start_progress_with_size(3) Yast::PackageCallbacks.InitPackageCallbacks(logger) progress.step(_("Initializing sources")) { add_base_repos } else - start_progress(2) + start_progress_with_size(2) end progress.step(_("Refreshing repositories metadata")) { repositories.load } @@ -178,7 +178,7 @@ def install Yast::Pkg.TargetLoad steps = proposal.packages_count - start_progress(steps) + start_progress_with_size(steps) Callbacks::Progress.setup(steps, progress) # TODO: error handling @@ -197,14 +197,11 @@ def install # Writes the repositories information to the installed system def finish - start_progress(1) - progress.step(_("Writing repositories to the target system")) do - Yast::Pkg.SourceSaveAll - Yast::Pkg.TargetFinish - # copy the libzypp caches to the target - copy_zypp_to_target - registration.finish - end + Yast::Pkg.SourceSaveAll + Yast::Pkg.TargetFinish + # copy the libzypp caches to the target + copy_zypp_to_target + registration.finish end # Determine whether the given tag is provided by the selected packages diff --git a/service/lib/agama/storage/finisher.rb b/service/lib/agama/storage/finisher.rb index bfeb3c58c4..38d9835353 100644 --- a/service/lib/agama/storage/finisher.rb +++ b/service/lib/agama/storage/finisher.rb @@ -50,7 +50,7 @@ def initialize(logger, config, security) # Execute the final storage actions, reporting the progress def run steps = possible_steps.select(&:run?) - start_progress(steps.size) + start_progress_with_size(steps.size) on_target do steps.each do |step| diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index af5fc39eb5..75046fb9d3 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -107,7 +107,7 @@ def on_probe(&block) # Probes storage devices and performs an initial proposal def probe - start_progress(4) + start_progress_with_size(4) config.pick_product(software.selected_product) check_multipath progress.step(_("Activating storage devices")) { activate_devices } @@ -120,7 +120,7 @@ def probe # Prepares the partitioning to install the system def install - start_progress(4) + start_progress_with_size(4) progress.step(_("Preparing bootloader proposal")) do # first make bootloader proposal to be sure that required packages are installed proposal = ::Bootloader::ProposalClient.new.make_proposal({}) diff --git a/service/lib/agama/with_progress.rb b/service/lib/agama/with_progress.rb index 8d81adb1fc..b3a5820aa5 100644 --- a/service/lib/agama/with_progress.rb +++ b/service/lib/agama/with_progress.rb @@ -30,22 +30,37 @@ class NotFinishedProgress < StandardError; end # @return [Progress, nil] attr_reader :progress + # Creates a new progress with a given number of steps + # + # @param size [Integer] Number of steps + def start_progress_with_size(size) + start_progress(size: size) + end + + # Creates a new progress with a given set of steps + # + # @param descriptions [Array] Steps descriptions + def start_progress_with_descriptions(*descriptions) + start_progress(descriptions: descriptions) + end + # Creates a new progress # # @raise [RuntimeError] if there is an unfinished progress. # - # @param total_steps [Integer] total number of the steps for the progress. - def start_progress(total_steps) + # @param args [*Hash] Progress constructor arguments. + def start_progress(args) raise NotFinishedProgress if progress && !progress.finished? on_change_callbacks = @on_progress_change_callbacks || [] on_finish_callbacks = @on_progress_finish_callbacks || [] - @progress = Progress.new(total_steps).tap do |progress| + @progress = Progress.new(**args).tap do |progress| progress.on_change { on_change_callbacks.each(&:call) } progress.on_finish { on_finish_callbacks.each(&:call) } end end + private :start_progress # Finishes the current progress def finish_progress diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 89fa95ad10..fc5d6af4a9 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jun 20 05:25:49 UTC 2024 - Imobach Gonzalez Sosa + +- Add support for progress sequences with pre-defined descriptions + (gh#openSUSE/agama#1356). + ------------------------------------------------------------------- Wed Jun 19 06:04:46 UTC 2024 - Ladislav Slezák diff --git a/service/test/agama/dbus/interfaces/progress_test.rb b/service/test/agama/dbus/interfaces/progress_test.rb index c5bc18fc2d..fcd0f4f280 100644 --- a/service/test/agama/dbus/interfaces/progress_test.rb +++ b/service/test/agama/dbus/interfaces/progress_test.rb @@ -59,7 +59,7 @@ class Backend context " if there is a progress" do before do - subject.backend.start_progress(2) + subject.backend.start_progress_with_size(2) end it "returns the total number of steps of the progress" do @@ -77,7 +77,7 @@ class Backend context " if there is a progress" do before do - subject.backend.start_progress(2) + subject.backend.start_progress_with_size(2) end before do @@ -99,7 +99,7 @@ class Backend context " if there is a progress" do before do - subject.backend.start_progress(2) + subject.backend.start_progress_with_size(2) end context "and the progress is not started" do @@ -132,25 +132,45 @@ class Backend end describe "#progress_properties" do - before do - subject.backend.start_progress(2) - progress.step("step 1") + context "when steps are not known in advance" do + before do + subject.backend.start_progress_with_size(2) + progress.step("step 1") + end + + it "returns de D-Bus properties of the progress interface" do + expected_properties = { + "TotalSteps" => 2, + "CurrentStep" => [1, "step 1"], + "Finished" => false, + "Steps" => [] + } + expect(subject.progress_properties).to eq(expected_properties) + end end - it "returns de D-Bus properties of the progress interface" do - expected_properties = { - "TotalSteps" => 2, - "CurrentStep" => [1, "step 1"], - "Finished" => false - } - expect(subject.progress_properties).to eq(expected_properties) + context "when steps are known in advance" do + before do + subject.backend.start_progress_with_descriptions("step 1", "step 2") + progress.step + end + + it "includes the steps" do + expected_properties = { + "TotalSteps" => 2, + "CurrentStep" => [1, "step 1"], + "Finished" => false, + "Steps" => ["step 1", "step 2"] + } + expect(subject.progress_properties).to eq(expected_properties) + end end end describe "#register_progress_callbacks" do it "register callbacks to be called when the progress changes" do subject.register_progress_callbacks - subject.backend.start_progress(2) + subject.backend.start_progress_with_size(2) expect(subject).to receive(:dbus_properties_changed) .with(progress_interface, anything, anything) @@ -160,7 +180,7 @@ class Backend it "register callbacks to be called when the progress finishes" do subject.register_progress_callbacks - subject.backend.start_progress(2) + subject.backend.start_progress_with_size(2) expect(subject).to receive(:dbus_properties_changed) .with(progress_interface, anything, anything) diff --git a/service/test/agama/progress_test.rb b/service/test/agama/progress_test.rb index f0d0cad0c3..4688cbd86f 100644 --- a/service/test/agama/progress_test.rb +++ b/service/test/agama/progress_test.rb @@ -23,7 +23,27 @@ require "agama/progress" describe Agama::Progress do - subject { described_class.new(steps) } + subject { described_class.with_size(steps) } + + describe "when the steps are known in advance" do + subject do + described_class.with_descriptions( + ["Partitioning", "Installing", "Configuring"] + ) + end + + it "sets the total_steps to the number of steps" do + expect(subject.total_steps).to eq(3) + end + + it "uses the given descriptions" do + subject.step + expect(subject.current_step.description).to eq("Partitioning") + + subject.step + expect(subject.current_step.description).to eq("Installing") + end + end describe "#current_step" do let(:steps) { 3 } @@ -56,10 +76,30 @@ subject.step("step 3") end - it "returns nil" do + it "returns the last step" do expect(subject.current_step).to be_nil end end + + context "if the descriptions are known in advance" do + subject do + described_class.with_descriptions( + ["Partitioning", "Installing", "Configuring"] + ) + end + + it "uses the descriptions" do + subject.step + expect(subject.current_step.description).to eq("Partitioning") + end + + context "but a description is given" do + it "uses the given descriptions" do + subject.step("Finishing") + expect(subject.current_step.description).to eq("Finishing") + end + end + end end describe "#step" do diff --git a/service/test/agama/storage/finisher_test.rb b/service/test/agama/storage/finisher_test.rb index 3f5f3eac19..17b57a7f58 100644 --- a/service/test/agama/storage/finisher_test.rb +++ b/service/test/agama/storage/finisher_test.rb @@ -53,7 +53,7 @@ end it "runs the possible steps that must be run" do - expect(subject).to receive(:start_progress).with(1) + expect(subject).to receive(:start_progress_with_size).with(1) expect(subject.progress).to receive(:step) do |label, &block| expect(label).to eql(copy_files.label) expect(copy_files).to receive(:run) diff --git a/service/test/agama/with_progress_examples.rb b/service/test/agama/with_progress_examples.rb index a4e463e799..89146bcef1 100644 --- a/service/test/agama/with_progress_examples.rb +++ b/service/test/agama/with_progress_examples.rb @@ -31,7 +31,7 @@ context "if a progress was started" do before do - subject.start_progress(1) + subject.start_progress_with_size(1) end it "returns the progress object" do @@ -40,27 +40,27 @@ end end - describe "#start_progress" do + describe "#start_progress_with_size" do context "if there is an unfinished progress" do before do - subject.start_progress(1) + subject.start_progress_with_size(1) end it "raises an error" do - expect { subject.start_progress(1) } + expect { subject.start_progress_with_size(1) } .to raise_error(Agama::WithProgress::NotFinishedProgress) end end context "if there is no unfinished progress" do before do - subject.start_progress(1) + subject.start_progress_with_size(1) subject.progress.finish end it "creates a new progress" do previous_progress = subject.progress - subject.start_progress(1) + subject.start_progress_with_size(1) expect(subject.progress).to_not eq(previous_progress) expect(subject.progress.finished?).to eq(false) end @@ -71,7 +71,7 @@ expect(callback).to receive(:call) - subject.start_progress(1) + subject.start_progress_with_size(1) subject.progress.step("step 1") end @@ -81,7 +81,7 @@ expect(callback).to receive(:call) - subject.start_progress(1) + subject.start_progress_with_size(1) subject.progress.finish end end @@ -90,7 +90,7 @@ describe "#finish" do context "when the current progress is not finished" do before do - subject.start_progress(1) + subject.start_progress_with_size(1) end it "finishes the current progress" do @@ -102,7 +102,7 @@ context "when the current progress is already finished" do before do - subject.start_progress(1) + subject.start_progress_with_size(1) subject.progress.step("") { nil } end diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 3356e8ecdd..a862bfa63a 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Jun 20 05:33:29 UTC 2024 - Imobach Gonzalez Sosa + +- Adapt the installation progress screen to look like the + product selection one (gh#openSUSE/agama#1356). + ------------------------------------------------------------------- Fri Jun 14 13:55:27 UTC 2024 - David Diaz diff --git a/web/src/client/mixins.js b/web/src/client/mixins.js index 72c63b6320..3c39361d07 100644 --- a/web/src/client/mixins.js +++ b/web/src/client/mixins.js @@ -176,6 +176,15 @@ const WithStatus = (superclass, status_path, service_name) => * @property {boolean} finished - whether the progress already finished */ +/** + * @typedef {object} ProgressSequence + * @property {string[]} steps - sequence steps if known in advance + * @property {number} total - number of steps + * @property {number} current - current step + * @property {string} message - message of the current step + * @property {boolean} finished - whether the progress already finished + */ + /** * @callback ProgressHandler * @param {Progress} progress - progress status @@ -195,7 +204,7 @@ const WithProgress = (superclass, progress_path, service_name) => /** * Returns the service progress * - * @return {Promise} an object containing the total steps, + * @return {Promise} an object containing the total steps, * the current step and whether the service finished or not. */ async getProgress() { @@ -203,17 +212,20 @@ const WithProgress = (superclass, progress_path, service_name) => if (!response.ok) { console.log("get progress failed with:", response); return { + steps: [], total: 0, current: 0, message: "Failed to get progress", finished: false, }; } else { - const { current_step, max_steps, current_title, finished } = await response.json(); + const { steps, currentStep, maxSteps, currentTitle, finished } = + await response.json(); return { - total: max_steps, - current: current_step, - message: current_title, + steps, + total: maxSteps, + current: currentStep, + message: currentTitle, finished, }; } @@ -228,11 +240,11 @@ const WithProgress = (superclass, progress_path, service_name) => onProgressChange(handler) { return this.client.onEvent("Progress", ({ service, ...progress }) => { if (service === service_name) { - const { current_step, max_steps, current_title, finished } = progress; + const { currentStep, maxSteps, currentTitle, finished } = progress; handler({ - total: max_steps, - current: current_step, - message: current_title, + total: maxSteps, + current: currentStep, + message: currentTitle, finished, }); } diff --git a/web/src/components/core/InstallationProgress.jsx b/web/src/components/core/InstallationProgress.jsx index 4dcc00ce7a..5e7c083293 100644 --- a/web/src/components/core/InstallationProgress.jsx +++ b/web/src/components/core/InstallationProgress.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -20,26 +20,24 @@ */ import React from "react"; -import { Card, CardBody, Grid, GridItem } from "@patternfly/react-core"; -import SimpleLayout from "~/SimpleLayout"; -import ProgressReport from "./ProgressReport"; -import { Center } from "~/components/layout"; +import { useProduct } from "~/context/product"; +import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; +import ProgressReport from "./ProgressReport"; +import SimpleLayout from "~/SimpleLayout"; function InstallationProgress() { + const { selectedProduct } = useProduct(); + + if (!selectedProduct) { + return; + } + + // TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) + const title = sprintf(_("Installing %s, please wait ..."), selectedProduct.name); return ( - -
- - - - - - - - - -
+ + ); } diff --git a/web/src/components/core/ProgressReport.jsx b/web/src/components/core/ProgressReport.jsx index bdd1a8017e..7b4cd47f08 100644 --- a/web/src/components/core/ProgressReport.jsx +++ b/web/src/components/core/ProgressReport.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -19,80 +19,125 @@ * find current contact information at www.suse.com. */ -import React, { useState, useEffect } from "react"; -import { Flex, Progress, Spinner, Text } from "@patternfly/react-core"; -import { useCancellablePromise } from "~/utils"; +import React, { useEffect, useState } from "react"; +import { + Card, + CardBody, + Grid, + GridItem, + ProgressStepper, + ProgressStep, + Spinner, + Stack, +} from "@patternfly/react-core"; + +import { _ } from "~/i18n"; +import { Center } from "~/components/layout"; import { useInstallerClient } from "~/context/installer"; -const ProgressReport = () => { - const client = useInstallerClient(); - const { cancellablePromise } = useCancellablePromise(); - // progress and subprogress are basically objects containing { message, step, steps } - const [progress, setProgress] = useState({}); - const [subProgress, setSubProgress] = useState(undefined); +const Progress = ({ steps, step, firstStep, detail }) => { + const variant = (index) => { + if (index < step.current) return "success"; + if (index === step.current) return "info"; + if (index > step.current) return "pending"; + }; - useEffect(() => { - cancellablePromise(client.manager.getProgress()).then(({ message, current, total }) => { - setProgress({ message, step: current, steps: total }); - }); - }, [client.manager, cancellablePromise]); + const stepProperties = (stepNumber) => { + const properties = { + variant: variant(stepNumber), + isCurrent: stepNumber === step.current, + id: `step-${stepNumber}-id`, + titleId: `step-${stepNumber}-title`, + }; - useEffect(() => { - return client.manager.onProgressChange(({ message, current, total, finished }) => { - if (!finished) setProgress({ message, step: current, steps: total }); - }); - }, [client.manager]); + if (properties.isCurrent) { + properties.icon = ; + if (detail && detail.message !== "") { + const { message, current, total } = detail; + properties.description = `${message} (${current}/${total})`; + } + } + + return properties; + }; + + return ( + + {firstStep && ( + + {firstStep} + + )} + {steps.map((description, idx) => { + return ( + + {description} + + ); + })} + + ); +}; + +/** + * @component + * + * Shows progress steps when a product is selected. + */ +function ProgressReport({ title, firstStep }) { + const { manager, storage, software } = useInstallerClient(); + const [steps, setSteps] = useState(); + const [step, setStep] = useState(); + const [detail, setDetail] = useState(); + + useEffect(() => software.onProgressChange(setDetail), [software, setDetail]); + useEffect(() => storage.onProgressChange(setDetail), [storage, setDetail]); useEffect(() => { - return client.software.onProgressChange(({ message, current, total, finished }) => { - if (finished) { - setSubProgress(undefined); - } else { - setSubProgress({ message, step: current, steps: total }); - } + manager.getProgress().then((progress) => { + setSteps(progress.steps); + setStep(progress); }); - }, [client.software]); - if (!progress.steps) { - return ( - - - Waiting for progress status... - - ); - } + return manager.onProgressChange(setStep); + }, [manager, setSteps]); - return ( - + const Content = () => { + if (!steps) { + return; + } + + return ( + ); + }; - { - subProgress && - - } - + const progressTitle = !steps ? _("Waiting for progress status...") : title; + return ( +
+ + + + + +

+ {progressTitle} +

+ +
+
+
+
+
+
); -}; +} export default ProgressReport; diff --git a/web/src/components/core/ProgressReport.test.jsx b/web/src/components/core/ProgressReport.test.jsx index c70589ea2e..d0fc0ed374 100644 --- a/web/src/components/core/ProgressReport.test.jsx +++ b/web/src/components/core/ProgressReport.test.jsx @@ -32,82 +32,83 @@ jest.mock("~/client"); let callbacks; let onManagerProgressChange = jest.fn(); let onSoftwareProgressChange = jest.fn(); -const getProgressFn = jest.fn(); +let onStorageProgressChange = jest.fn(); beforeEach(() => { createClient.mockImplementation(() => { return { manager: { onProgressChange: onManagerProgressChange, - getProgress: getProgressFn + getProgress: jest.fn().mockResolvedValue({ + message: "Partition disks", + current: 1, + total: 10, + steps: ["Partition disks", "Install software"], + }), }, software: { - onProgressChange: onSoftwareProgressChange - } + onProgressChange: onSoftwareProgressChange, + }, + storage: { + onProgressChange: onStorageProgressChange, + }, }; }); }); describe("ProgressReport", () => { - describe("when there is not progress information available", () => { - beforeEach(() => { - getProgressFn.mockResolvedValue({}); - }); - - it("renders a waiting message", async () => { - installerRender(); - await screen.findByText(/Waiting for progress status/i); - }); - }); - describe("when there is progress information available", () => { beforeEach(() => { const [onManagerProgress, managerCallbacks] = createCallbackMock(); const [onSoftwareProgress, softwareCallbacks] = createCallbackMock(); + const [onStorageProgress, storageCallbacks] = createCallbackMock(); onManagerProgressChange = onManagerProgress; onSoftwareProgressChange = onSoftwareProgress; - getProgressFn.mockResolvedValue( - { message: "Reading repositories", current: 1, total: 10 } - ); - callbacks = { manager: managerCallbacks, software: softwareCallbacks }; + onStorageProgressChange = onStorageProgress; + callbacks = { + manager: managerCallbacks, + software: softwareCallbacks, + storage: storageCallbacks, + }; }); - it("shows the main progress bar", async () => { + it("shows the progress including the details from the storage service", async () => { installerRender(); - await screen.findByText(/Reading/i); + await screen.findByText(/Waiting/i); + await screen.findByText(/Partition disks/i); + await screen.findByText(/Install software/i); - // NOTE: there can be more than one subscriptions to the - // manager#onProgressChange. We're interested in the latest one here. - const cb = callbacks.manager[callbacks.manager.length - 1]; + const cb = callbacks.storage[callbacks.storage.length - 1]; act(() => { - cb({ message: "Partitioning", current: 1, total: 10 }); + cb({ + message: "Doing some partitioning", + current: 1, + total: 10, + finished: false, + }); }); - await screen.findByRole("progressbar", { name: "Partitioning" }); + await screen.findByText("Doing some partitioning (1/10)"); }); - it("shows secondary progress bar when there is information from software service ", async () => { + it("shows the progress including the details from the software service", async () => { installerRender(); - const managerCallback = callbacks.manager[callbacks.manager.length - 1]; - const softwareCallback = callbacks.software[callbacks.software.length - 1]; - - act(() => { - managerCallback({ message: "Partitioning", current: 1, total: 10 }); - }); - - await screen.findByRole("progressbar", { name: "Partitioning" }); - const bars = await screen.findAllByRole("progressbar"); - expect(bars.length).toBe(1); + await screen.findByText(/Waiting/i); + await screen.findByText(/Install software/i); + const cb = callbacks.software[callbacks.software.length - 1]; act(() => { - managerCallback({ message: "Installing software", current: 4, total: 10 }); - softwareCallback({ message: "Installing YaST2", current: 256, total: 500, finished: false }); + cb({ + message: "Installing packages", + current: 495, + total: 500, + finished: false, + }); }); - await screen.findByRole("progressbar", { name: "Installing software" }); - await screen.findByRole("progressbar", { name: "Installing YaST2" }); + await screen.findByText("Installing packages (495/500)"); }); }); }); diff --git a/web/src/components/overview/OverviewPage.jsx b/web/src/components/overview/OverviewPage.jsx index 2011517eb1..78185b647d 100644 --- a/web/src/components/overview/OverviewPage.jsx +++ b/web/src/components/overview/OverviewPage.jsx @@ -59,12 +59,12 @@ const ReadyForInstallation = () => ( const IssuesList = ({ issues }) => { const { isEmpty, ...scopes } = issues; const list = []; - Object.entries(scopes).forEach(([scope, issues]) => { - issues.forEach((issue, idx) => { + Object.entries(scopes).forEach(([scope, issues], idx) => { + issues.forEach((issue, subIdx) => { const variant = issue.severity === "error" ? "warning" : "info"; const link = ( - + {issue.description} diff --git a/web/src/components/product/ProductSelectionProgress.jsx b/web/src/components/product/ProductSelectionProgress.jsx index ec9e40bb73..bfda9a3cc9 100644 --- a/web/src/components/product/ProductSelectionProgress.jsx +++ b/web/src/components/product/ProductSelectionProgress.jsx @@ -21,97 +21,20 @@ import React, { useEffect, useState } from "react"; import { Navigate } from "react-router-dom"; -import { - Card, CardBody, - Grid, GridItem, - ProgressStepper, ProgressStep, - Spinner, - Stack -} from "@patternfly/react-core"; - import { _ } from "~/i18n"; -import { Center } from "~/components/layout"; -import { useCancellablePromise } from "~/utils"; -import { useInstallerClient } from "~/context/installer"; import { useProduct } from "~/context/product"; +import { ProgressReport } from "~/components/core"; import { IDLE } from "~/client/status"; - -const Progress = ({ selectedProduct, storageProgress, softwareProgress }) => { - const variant = (progress) => { - if (progress.start && progress.current === 0) return "success"; - if (!progress.start) return "pending"; - if (progress.current > 0) return "info"; - }; - - const isCurrent = (progress) => progress.current > 0; - - const description = ({ message, current, total }) => { - if (!message) return ""; - - return (current === 0) ? message : `${message} (${current}/${total})`; - }; - - const stepProperties = (progress) => { - const properties = { - variant: variant(progress), - isCurrent: isCurrent(progress), - description: description(progress) - }; - - if (properties.isCurrent) properties.icon = ; - - return properties; - }; - - // Emulates progress for product selection step. - const productProgress = () => { - if (!storageProgress.start) return { start: true, current: 1 }; - - return { start: true, current: 0 }; - }; - - /** @todo Add aria-label to steps, describing its status and variant. */ - return ( - - - {selectedProduct.name} - - - {_("Analyze disks")} - - - {_("Configure software")} - - - ); -}; +import { useInstallerClient } from "~/context/installer"; /** * @component * * Shows progress steps when a product is selected. - * - * @note Some details are hardcoded (e.g., the steps, the order, etc). The progress API has to be - * improved. */ function ProductSelectionProgress() { - const { cancellablePromise } = useCancellablePromise(); - const { manager, storage, software } = useInstallerClient(); const { selectedProduct } = useProduct(); - const [storageProgress, setStorageProgress] = useState({}); - const [softwareProgress, setSoftwareProgress] = useState({}); + const { manager } = useInstallerClient(); const [status, setStatus] = useState(); useEffect(() => { @@ -119,54 +42,17 @@ function ProductSelectionProgress() { return manager.onStatusChange(setStatus); }, [manager, setStatus]); - useEffect(() => { - const updateProgress = (progress) => { - if (progress.current > 0) progress.start = true; - setStorageProgress(p => ({ ...p, ...progress })); - }; - - cancellablePromise(storage.getProgress()).then(updateProgress); - - return storage.onProgressChange(updateProgress); - }, [cancellablePromise, setStorageProgress, storage]); - - useEffect(() => { - const updateProgress = (progress) => { - if (progress.current > 0) progress.start = true; - setSoftwareProgress(p => ({ ...p, ...progress })); - // Let's assume storage was started too. - setStorageProgress(p => ({ ...p, start: progress.start })); - }; - - cancellablePromise(software.getProgress()).then(updateProgress); - - return software.onProgressChange(updateProgress); - }, [cancellablePromise, setSoftwareProgress, software]); + if (!selectedProduct) { + return; + } if (status === IDLE) return ; return ( -
- - - - - -

- {_("Configuring the product, please wait ...")} -

- { status && - } -
-
-
-
-
-
+ ); }