From fd4d7d2c90b8147b2395d8e999030480e03e3967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 7 Feb 2024 12:20:28 +0000 Subject: [PATCH 01/38] [service] Export space actions on D-Bus - Moreover, the list of actions is always recovered from Y2Storage instead of remembering the actions passed in the settings to calculate a proposal. --- service/lib/agama/dbus/storage/proposal.rb | 11 ++++++- .../proposal_settings_conversion/from_dbus.rb | 8 +++-- .../proposal_settings_conversion/to_dbus.rb | 13 ++++++-- service/lib/agama/storage/proposal.rb | 7 ++--- .../from_y2storage.rb | 5 ++-- .../to_y2storage.rb | 2 +- .../from_dbus_test.rb | 17 +++++++++-- .../to_dbus_test.rb | 17 ++++++++--- .../test/agama/dbus/storage/proposal_test.rb | 30 ++++++++++++++++++- .../from_y2storage_test.rb | 9 +++++- service/test/agama/storage/proposal_test.rb | 13 ++++---- 11 files changed, 101 insertions(+), 31 deletions(-) diff --git a/service/lib/agama/dbus/storage/proposal.rb b/service/lib/agama/dbus/storage/proposal.rb index fe1df2f179..2d55331f35 100644 --- a/service/lib/agama/dbus/storage/proposal.rb +++ b/service/lib/agama/dbus/storage/proposal.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -58,6 +58,8 @@ def initialize(backend, logger) dbus_reader :space_policy, "s" + dbus_reader :space_actions, "aa{sv}" + dbus_reader :volumes, "aa{sv}" dbus_reader :actions, "aa{sv}" @@ -109,6 +111,13 @@ def space_policy dbus_settings.fetch("SpacePolicy", "") end + # Space actions + # + # @return [Array] + def space_actions + dbus_settings.fetch("SpaceActions", []) + end + # Volumes used to calculate the storage proposal # # @return [Array] diff --git a/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb b/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb index 4e7d7b1356..412607d72c 100644 --- a/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb +++ b/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -130,9 +130,11 @@ def space_policy_conversion(target, value) end # @param target [Agama::Storage::ProposalSettings] - # @param value [Hash] + # @param value [Array] def space_actions_conversion(target, value) - target.space.actions = value + target.space.actions = value.each_with_object({}) do |v, result| + result[v["Device"]] = v["Action"].to_sym + end end # @param target [Agama::Storage::ProposalSettings] diff --git a/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb b/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb index 8d641f0b1b..d4820e847f 100644 --- a/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb +++ b/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -35,7 +35,7 @@ def initialize(settings) # Performs the conversion to D-Bus format. # # @return [Hash] - def convert # rubocop:disable Metrics/AbcSize + def convert { "BootDevice" => settings.boot_device.to_s, "LVM" => settings.lvm.enabled?, @@ -44,7 +44,7 @@ def convert # rubocop:disable Metrics/AbcSize "EncryptionMethod" => settings.encryption.method.id.to_s, "EncryptionPBKDFunction" => settings.encryption.pbkd_function&.value || "", "SpacePolicy" => settings.space.policy.to_s, - "SpaceActions" => settings.space.actions, + "SpaceActions" => space_actions_conversion, "Volumes" => settings.volumes.map { |v| VolumeConversion.to_dbus(v) } } end @@ -53,6 +53,13 @@ def convert # rubocop:disable Metrics/AbcSize # @return [Agama::Storage::ProposalSettings] attr_reader :settings + + # @return [Array] + def space_actions_conversion + settings.space.actions.each_with_object([]) do |(device, action), actions| + actions << { "Device" => device, "Action" => action.to_s } + end + end end end end diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb index 7232bc71a6..0b3d546dc3 100644 --- a/service/lib/agama/storage/proposal.rb +++ b/service/lib/agama/storage/proposal.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -85,10 +85,9 @@ def settings proposal.settings, config: config ).tap do |settings| - # FIXME: The conversion from Y2Storage cannot infer the space policy. Copying space - # settings from the original settings. + # The conversion from Y2Storage cannot infer the space policy. Copying space policy from + # the original settings. settings.space.policy = original_settings.space.policy - settings.space.actions = original_settings.space.actions # FIXME: The conversion from Y2Storage cannot reliably infer the system VG devices in all # cases. Copying system VG devices from the original settings. settings.lvm.system_vg_devices = original_settings.lvm.system_vg_devices diff --git a/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb b/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb index db13a73bc8..a60a0c59a3 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb +++ b/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -82,9 +82,10 @@ def encryption_conversion(target) target.encryption.pbkd_function = settings.encryption_pbkdf end + # @note The space policy cannot be inferred from Y2Storage settings. # @param target [Agama::Storage::ProposalSettings] def space_settings_conversion(target) - # FIXME: No way to infer space settings from Y2Storage. + target.space.actions = settings.space_settings.actions end # @param target [Agama::Storage::ProposalSettings] diff --git a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb b/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb index e583db872b..e7ca55e481 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb +++ b/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb @@ -162,7 +162,7 @@ def find_max_size_fallback(mount_path) # All block devices affected by the space policy. # - # This includes the partitions from the root device, the candidate devices and from the + # This includes the partitions from the boot device, the candidate devices and from the # devices directly assigned to a volume as target device. If a device is not partitioned, # then the device itself is included. # diff --git a/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb b/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb index bd43e1362c..3114f1a9e6 100644 --- a/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb +++ b/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -69,7 +69,16 @@ "EncryptionMethod" => "luks1", "EncryptionPBKDFunction" => "pbkdf2", "SpacePolicy" => "custom", - "SpaceActions" => { "/dev/sda" => "force_delete" }, + "SpaceActions" => [ + { + "Device" => "/dev/sda", + "Action" => "force_delete" + }, + { + "Device" => "/dev/sdb1", + "Action" => "resize" + } + ], "Volumes" => [ { "MountPath" => "/" }, { "MountPath" => "/test" } @@ -87,7 +96,9 @@ expect(settings.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS1) expect(settings.encryption.pbkd_function).to eq(Y2Storage::PbkdFunction::PBKDF2) expect(settings.space.policy).to eq(:custom) - expect(settings.space.actions).to eq({ "/dev/sda" => "force_delete" }) + expect(settings.space.actions).to eq({ + "/dev/sda" => :force_delete, "/dev/sdb1" => :resize + }) expect(settings.volumes.map(&:mount_path)).to contain_exactly("/", "/test") end diff --git a/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb b/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb index 168ab4852e..8cc085e5ff 100644 --- a/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb +++ b/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2024] SUSE LLC # # All Rights Reserved. # @@ -38,7 +38,7 @@ settings.encryption.method = Y2Storage::EncryptionMethod::LUKS2 settings.encryption.pbkd_function = Y2Storage::PbkdFunction::ARGON2ID settings.space.policy = :custom - settings.space.actions = { "/dev/sda" => :force_delete } + settings.space.actions = { "/dev/sda" => :force_delete, "/dev/sdb1" => "resize" } settings.volumes = [Agama::Storage::Volume.new("/test")] end end @@ -53,7 +53,7 @@ "EncryptionMethod" => "luks2", "EncryptionPBKDFunction" => "pbkdf2", "SpacePolicy" => "keep", - "SpaceActions" => {}, + "SpaceActions" => [], "Volumes" => [] ) @@ -65,7 +65,16 @@ "EncryptionMethod" => "luks2", "EncryptionPBKDFunction" => "argon2id", "SpacePolicy" => "custom", - "SpaceActions" => { "/dev/sda" => :force_delete }, + "SpaceActions" => [ + { + "Device" => "/dev/sda", + "Action" => "force_delete" + }, + { + "Device" => "/dev/sdb1", + "Action" => "resize" + } + ], "Volumes" => [ { "MountPath" => "/test", diff --git a/service/test/agama/dbus/storage/proposal_test.rb b/service/test/agama/dbus/storage/proposal_test.rb index 47791823af..790014eeaa 100644 --- a/service/test/agama/dbus/storage/proposal_test.rb +++ b/service/test/agama/dbus/storage/proposal_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -181,6 +181,34 @@ end end + describe "#space_actions" do + context "if a proposal has not been calculated yet" do + let(:settings) { nil } + + it "returns an empty list" do + expect(subject.space_actions).to eq([]) + end + end + + context "if a proposal has been calculated" do + let(:settings) do + Agama::Storage::ProposalSettings.new.tap do |settings| + settings.space.actions = { + "/dev/vda1" => :force_delete, + "/dev/vda2" => :resize + } + end + end + + it "return a list with a hash for each action" do + expect(subject.space_actions).to contain_exactly( + { "Device" => "/dev/vda1", "Action" => "force_delete" }, + { "Device" => "/dev/vda2", "Action" => "resize" } + ) + end + end + end + describe "#volumes" do let(:settings) do Agama::Storage::ProposalSettings.new.tap { |s| s.volumes = calculated_volumes } diff --git a/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb index 9a6f4ff517..c01e40a4e9 100644 --- a/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb +++ b/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -38,6 +38,10 @@ settings.encryption_password = "notsecret" settings.encryption_method = Y2Storage::EncryptionMethod::LUKS2 settings.encryption_pbkdf = Y2Storage::PbkdFunction::ARGON2ID + settings.space_settings.actions = { + "/dev/sda" => :force_delete, + "/dev/sdb1" => :resize + } settings.volumes = [] end end @@ -57,6 +61,9 @@ method: Y2Storage::EncryptionMethod::LUKS2, pbkd_function: Y2Storage::PbkdFunction::ARGON2ID ), + space: an_object_having_attributes( + actions: { "/dev/sda" => :force_delete, "/dev/sdb1" => :resize } + ), volumes: [] ) end diff --git a/service/test/agama/storage/proposal_test.rb b/service/test/agama/storage/proposal_test.rb index d276a2f8c8..c56237b394 100644 --- a/service/test/agama/storage/proposal_test.rb +++ b/service/test/agama/storage/proposal_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -155,13 +155,10 @@ volumes: contain_exactly( an_object_having_attributes(mount_path: "/") ), - # Checking space settings explicitly here because the settings converter cannot infer the - # space settings from the Y2Storage settings. The space settings are directly recovered - # from the original settings passed to #calculate. - space: an_object_having_attributes( - policy: :custom, - actions: { "/dev/sda" => :force_delete } - ) + # Checking space policy explicitly here because the settings converter cannot infer the + # space policy from the Y2Storage settings. The space policy is directly recovered from + # the original settings passed to #calculate. + space: an_object_having_attributes(policy: :custom) ) end From 0028356faebca25c8b0939cc6b19e7b55dcc2a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 7 Feb 2024 17:07:12 +0000 Subject: [PATCH 02/38] [web] Add space actions to storage client --- web/src/client/storage.js | 44 +++++++++++++++++++++++++++++++--- web/src/client/storage.test.js | 43 ++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index ba74801644..3a35b45de3 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -241,10 +241,15 @@ class ProposalManager { * @property {string} encryptionMethod * @property {boolean} lvm * @property {string} spacePolicy + * @property {SpaceAction[]} spaceActions * @property {string[]} systemVGDevices * @property {Volume[]} volumes * @property {StorageDevice[]} installationDevices * + * @typedef {object} SpaceAction + * @property {string} device + * @property {string} action + * * @typedef {object} Volume * @property {string} mountPath * @property {string} fsType @@ -338,6 +343,13 @@ class ProposalManager { const systemDevices = await this.system.getDevices(); const buildResult = (proxy) => { + const buildSpaceAction = dbusSpaceAction => { + return { + device: dbusSpaceAction.Device.v, + action: dbusSpaceAction.Action.v + }; + }; + const buildAction = dbusAction => { return { text: dbusAction.Text.v, @@ -365,6 +377,7 @@ class ProposalManager { bootDevice: proxy.BootDevice, lvm: proxy.LVM, spacePolicy: proxy.SpacePolicy, + spaceActions: proxy.SpaceActions.map(buildSpaceAction), systemVGDevices: proxy.SystemVGDevices, encryptionPassword: proxy.EncryptionPassword, encryptionMethod: proxy.EncryptionMethod, @@ -388,7 +401,31 @@ class ProposalManager { * @param {ProposalSettings} settings * @returns {Promise} 0 on success, 1 on failure */ - async calculate({ bootDevice, encryptionPassword, encryptionMethod, lvm, spacePolicy, systemVGDevices, volumes }) { + async calculate(settings) { + const { + bootDevice, + encryptionPassword, + encryptionMethod, + lvm, + spacePolicy, + spaceActions, + systemVGDevices, + volumes + } = settings; + + const dbusSpaceActions = () => { + const dbusSpaceAction = (spaceAction) => { + return { + Device: { t: "s", v: spaceAction.device }, + Action: { t: "s", v: spaceAction.action } + }; + }; + + if (spacePolicy !== "custom") return; + + return spaceActions?.map(dbusSpaceAction); + }; + const dbusVolume = (volume) => { return removeUndefinedCockpitProperties({ MountPath: { t: "s", v: volume.mountPath }, @@ -401,18 +438,19 @@ class ProposalManager { }); }; - const settings = removeUndefinedCockpitProperties({ + const dbusSettings = removeUndefinedCockpitProperties({ BootDevice: { t: "s", v: bootDevice }, EncryptionPassword: { t: "s", v: encryptionPassword }, EncryptionMethod: { t: "s", v: encryptionMethod }, LVM: { t: "b", v: lvm }, SpacePolicy: { t: "s", v: spacePolicy }, + SpaceActions: { t: "aa{sv}", v: dbusSpaceActions() }, SystemVGDevices: { t: "as", v: systemVGDevices }, Volumes: { t: "aa{sv}", v: volumes?.map(dbusVolume) } }); const proxy = await this.proxies.proposalCalculator; - return proxy.Calculate(settings); + return proxy.Calculate(dbusSettings); } /** diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 8c3826221f..6dc2c1437a 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -155,7 +155,17 @@ const contexts = { LVM: true, SystemVGDevices: ["/dev/sda", "/dev/sdb"], EncryptionPassword: "00000", - SpacePolicy: "delete", + SpacePolicy: "custom", + SpaceActions: [ + { + Device: { t: "s", v: "/dev/sda" }, + Action: { t: "s", v: "force_delete" } + }, + { + Device: { t: "s", v: "/dev/sdb" }, + Action: { t: "s", v: "resize" } + } + ], Volumes: [ { MountPath: { t: "s", v: "/" }, @@ -825,7 +835,11 @@ describe("#proposal", () => { lvm: true, systemVGDevices: ["/dev/sda", "/dev/sdb"], encryptionPassword: "00000", - spacePolicy: "delete", + spacePolicy: "custom", + spaceActions: [ + { device: "/dev/sda", action: "force_delete" }, + { device: "/dev/sdb", action: "resize" } + ], volumes: [ { mountPath: "/", @@ -895,6 +909,8 @@ describe("#proposal", () => { encryptionPassword: "12345", lvm: true, systemVGDevices: ["/dev/sdc"], + spacePolicy: "custom", + spaceActions: [{ device: "/dev/sda", action: "resize" }], volumes: [ { mountPath: "/test1", @@ -916,6 +932,16 @@ describe("#proposal", () => { EncryptionPassword: { t: "s", v: "12345" }, LVM: { t: "b", v: true }, SystemVGDevices: { t: "as", v: ["/dev/sdc"] }, + SpacePolicy: { t: "s", v: "custom" }, + SpaceActions: { + t: "aa{sv}", + v: [ + { + Device: { t: "s", v: "/dev/sda" }, + Action: { t: "s", v: "resize" } + } + ] + }, Volumes: { t: "aa{sv}", v: [ @@ -935,6 +961,17 @@ describe("#proposal", () => { } }); }); + + it("calculates a proposal without space actions if the policy is not custom", async () => { + await client.proposal.calculate({ + spacePolicy: "delete", + spaceActions: [{ device: "/dev/sda", action: "resize" }], + }); + + expect(cockpitProxies.proposalCalculator.Calculate).toHaveBeenCalledWith({ + SpacePolicy: { t: "s", v: "delete" } + }); + }); }); }); From 63da546d2c5c8b64c74014efb2c488a961f306d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 8 Feb 2024 09:34:46 +0000 Subject: [PATCH 03/38] [web] Add section for the space policy --- web/src/components/storage/ProposalPage.jsx | 13 +- .../storage/ProposalSettingsSection.jsx | 176 ++++-------------- .../storage/ProposalSpacePolicySection.jsx | 148 +++++++++++++++ web/src/components/storage/index.js | 3 +- 4 files changed, 195 insertions(+), 145 deletions(-) create mode 100644 web/src/components/storage/ProposalSpacePolicySection.jsx diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index cbeb1d84ec..fad35cf99d 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -27,7 +27,12 @@ import { useInstallerClient } from "~/context/installer"; import { toValidationError, useCancellablePromise } from "~/utils"; import { Icon } from "~/components/layout"; import { Page } from "~/components/core"; -import { ProposalActionsSection, ProposalPageMenu, ProposalSettingsSection } from "~/components/storage"; +import { + ProposalActionsSection, + ProposalPageMenu, + ProposalSettingsSection, + ProposalSpacePolicySection, +} from "~/components/storage"; import { IDLE } from "~/client/status"; const initialState = { @@ -218,6 +223,10 @@ export default function ProposalPage() { onChange={changeSettings} isLoading={state.loading} /> + { - const [spacePolicy, setSpacePolicy] = useState(policy); - - const onSubmit = (e) => { - e.preventDefault(); - onSubmitProp(spacePolicy); - }; - - return ( -
- - - ); -}; - -/** - * Allows to select SpacePolicy. - * @component - * - * @param {object} props - * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. - * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading. - * @param {onChangeFn} [props.onChange=noop] - On change callback. - * - * @callback onChangeFn - * @param {string} policy - */ -const SpacePolicyField = ({ - settings, - isLoading = false, - onChange = noop -}) => { - const [isFormOpen, setIsFormOpen] = useState(false); - const [spacePolicy, setSpacePolicy] = useState(settings.spacePolicy); - - const openForm = () => setIsFormOpen(true); - const closeForm = () => setIsFormOpen(false); - - const onSubmitForm = (policy) => { - onChange(policy); - setSpacePolicy(policy); - closeForm(); - }; - - if (isLoading) return ; - - const description = _("Select how to make free space in the disks selected for allocating the \ - file systems."); - - return ( -
- {/* TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" */} - {_("Find space")} - - -
- - -
- - - {_("Accept")} - - - -
-
- ); -}; - /** * Section for editing the proposal settings * @component @@ -688,10 +587,6 @@ export default function ProposalSettingsSection({ onChange({ encryptionPassword: password, encryptionMethod: method }); }; - const changeSpacePolicy = (policy) => { - onChange({ spacePolicy: policy }); - }; - const changeVolumes = (volumes) => { onChange({ volumes }); }; @@ -700,40 +595,37 @@ export default function ProposalSettingsSection({ const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0; return ( -
- - - - - -
+ <> +
+ + + + +
+ ); } diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx new file mode 100644 index 0000000000..82bfc5e1b3 --- /dev/null +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -0,0 +1,148 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React, { useState } from "react"; +import { Form, Skeleton } from "@patternfly/react-core"; + +import { _ } from "~/i18n"; +import { Section, Popup } from "~/components/core"; +import { SpacePolicyButton, SpacePolicySelector, SpacePolicyDisksHint } from "~/components/storage"; +import { noop } from "~/utils"; + +/** + * Form for configuring the space policy. + * @component + * + * @param {object} props + * @param {string} props.id - Form ID. + * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. + * @param {onSubmitFn} [props.onSubmit=noop] - On submit callback. + * + * @callback onSubmitFn + * @param {string} policy - Name of the selected policy. + */ +const SpacePolicyForm = ({ + id, + policy, + onSubmit: onSubmitProp = noop +}) => { + const [spacePolicy, setSpacePolicy] = useState(policy); + + const onSubmit = (e) => { + e.preventDefault(); + onSubmitProp(spacePolicy); + }; + + return ( +
+ + + ); +}; + +/** + * Allows to select SpacePolicy. + * @component + * + * @param {object} props + * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. + * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading. + * @param {onChangeFn} [props.onChange=noop] - On change callback. + * + * @callback onChangeFn + * @param {string} policy + */ +const SpacePolicyField = ({ + settings, + isLoading = false, + onChange = noop +}) => { + const [isFormOpen, setIsFormOpen] = useState(false); + const [spacePolicy, setSpacePolicy] = useState(settings.spacePolicy); + + const openForm = () => setIsFormOpen(true); + const closeForm = () => setIsFormOpen(false); + + const onSubmitForm = (policy) => { + onChange(policy); + setSpacePolicy(policy); + closeForm(); + }; + + if (isLoading) return ; + + const description = _("Select how to make free space in the disks selected for allocating the \ + file systems."); + + return ( +
+ {/* TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" */} + {_("Find space")} + + +
+ + +
+ + + {_("Accept")} + + + +
+
+ ); +}; + +/** + * Section for configuring the space policy. + * @component + */ +export default function ProposalSpacePolicySection({ + settings, + onChange = noop +}) { + const changeSpacePolicy = (policy) => { + onChange({ spacePolicy: policy }); + }; + + return ( +
+ +
+ ); +} diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.js index e43354e10e..f048b4d8f7 100644 --- a/web/src/components/storage/index.js +++ b/web/src/components/storage/index.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -22,6 +22,7 @@ export { default as ProposalPage } from "./ProposalPage"; export { default as ProposalPageMenu } from "./ProposalPageMenu"; export { default as ProposalSettingsSection } from "./ProposalSettingsSection"; +export { default as ProposalSpacePolicySection } from "./ProposalSpacePolicySection"; export { default as ProposalActionsSection } from "./ProposalActionsSection"; export { default as ProposalVolumes } from "./ProposalVolumes"; export { default as DASDPage } from "./DASDPage"; From fd424441081c2f29f1c532753153a2fe8580f6c9 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, 9 Feb 2024 15:12:47 +0000 Subject: [PATCH 04/38] [web] Improvements for storage client - Read recoverable size from D-Bus. - Create partition objects --- web/src/client/storage.js | 20 +++++----- web/src/client/storage.test.js | 68 ++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 3a35b45de3..ece1ede48a 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -125,6 +125,7 @@ class DevicesManager { * @property {boolean} [active] * @property {string} [name] - Block device name * @property {number} [size] + * @property {number} [recoverableSize] * @property {string[]} [systems] - Name of the installed systems * @property {string[]} [udevIds] * @property {string[]} [udevPaths] @@ -134,7 +135,7 @@ class DevicesManager { * @property {string} type */ async getDevices() { - const buildDevice = (path, dbusDevice) => { + const buildDevice = (path, dbusDevices) => { const addDriveProperties = (device, dbusProperties) => { device.type = dbusProperties.Type.v; device.vendor = dbusProperties.Vendor.v; @@ -166,6 +167,7 @@ class DevicesManager { device.active = blockProperties.Active.v; device.name = blockProperties.Name.v; device.size = blockProperties.Size.v; + device.recoverableSize = blockProperties.RecoverableSize.v; device.systems = blockProperties.Systems.v; device.udevIds = blockProperties.UdevIds.v; device.udevPaths = blockProperties.UdevPaths.v; @@ -174,7 +176,7 @@ class DevicesManager { const addPtableProperties = (device, ptableProperties) => { device.partitionTable = { type: ptableProperties.Type.v, - partitions: ptableProperties.Partitions.v + partitions: ptableProperties.Partitions.v.map(p => buildDevice(p, dbusDevices)) }; }; @@ -183,22 +185,22 @@ class DevicesManager { type: "" }; - const driveProperties = dbusDevice["org.opensuse.Agama.Storage1.Drive"]; + const driveProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Drive"]; if (driveProperties !== undefined) addDriveProperties(device, driveProperties); - const raidProperties = dbusDevice["org.opensuse.Agama.Storage1.RAID"]; + const raidProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.RAID"]; if (raidProperties !== undefined) addRAIDProperties(device, raidProperties); - const multipathProperties = dbusDevice["org.opensuse.Agama.Storage1.Multipath"]; + const multipathProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Multipath"]; if (multipathProperties !== undefined) addMultipathProperties(device, multipathProperties); - const mdProperties = dbusDevice["org.opensuse.Agama.Storage1.MD"]; + const mdProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.MD"]; if (mdProperties !== undefined) addMDProperties(device, mdProperties); - const blockProperties = dbusDevice["org.opensuse.Agama.Storage1.Block"]; + const blockProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Block"]; if (blockProperties !== undefined) addBlockProperties(device, blockProperties); - const ptableProperties = dbusDevice["org.opensuse.Agama.Storage1.PartitionTable"]; + const ptableProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.PartitionTable"]; if (ptableProperties !== undefined) addPtableProperties(device, ptableProperties); return device; @@ -214,7 +216,7 @@ class DevicesManager { const dbusObjects = managedObjects.shift(); const systemPaths = Object.keys(dbusObjects).filter(k => k.startsWith(this.rootPath)); - return systemPaths.map(p => buildDevice(p, dbusObjects[p])); + return systemPaths.map(p => buildDevice(p, dbusObjects)); } } diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 6dc2c1437a..43c8958e3c 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -33,6 +33,30 @@ const cockpitCallbacks = {}; let managedObjects = {}; +const sda1 = { + sid: "66", + type: "", + active: true, + name: "/dev/sda1", + size: 512, + recoverableSize: 128, + systems : ["Windows"], + udevIds: [], + udevPaths: [] +}; + +const sda2 = { + sid: "67", + type: "", + active: true, + name: "/dev/sda2", + size: 512, + recoverableSize: 0, + systems : ["openSUSE Leap 15.2"], + udevIds: [], + udevPaths: [] +}; + const systemDevices = { sda: { sid: "59", @@ -48,12 +72,13 @@ const systemDevices = { active: true, name: "/dev/sda", size: 1024, + recoverableSize: 0, systems : ["Windows", "openSUSE Leap 15.2"], udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], partitionTable: { type: "gpt", - partitions: ["/dev/sda1", "/dev/sda2"] + partitions: [sda1, sda2] } }, sdb: { @@ -70,6 +95,7 @@ const systemDevices = { active: true, name: "/dev/sdb", size: 2048, + recoverableSize: 0, systems : [], udevIds: [], udevPaths: ["pci-0000:00-19"] @@ -83,6 +109,7 @@ const systemDevices = { active: true, name: "/dev/md0", size: 2048, + recoverableSize: 0, systems : [], udevIds: [], udevPaths: [] @@ -102,6 +129,7 @@ const systemDevices = { active: true, name: "/dev/mapper/isw_ddgdcbibhd_244", size: 2048, + recoverableSize: 0, systems : [], udevIds: [], udevPaths: [] @@ -121,6 +149,7 @@ const systemDevices = { active: true, name: "/dev/mapper/36005076305ffc73a00000000000013b4", size: 2048, + recoverableSize: 0, systems : [], udevIds: [], udevPaths: [] @@ -139,10 +168,12 @@ const systemDevices = { active: true, name: "/dev/dasda", size: 2048, + recoverableSize: 0, systems : [], udevIds: [], udevPaths: [] - } + }, + sda1, sda2 }; const contexts = { @@ -345,13 +376,17 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/sda" }, Size: { t: "x", v: 1024 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: ["Windows", "openSUSE Leap 15.2"] }, UdevIds: { t: "as", v: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"] }, UdevPaths: { t: "as", v: ["pci-0000:00-12", "pci-0000:00-12-ata"] } }, "org.opensuse.Agama.Storage1.PartitionTable": { Type: { t: "s", v: "gpt" }, - Partitions: { t: "as", v: ["/dev/sda1", "/dev/sda2"] } + Partitions: { + t: "as", + v: ["/org/opensuse/Agama/Storage1/system/66", "/org/opensuse/Agama/Storage1/system/67"] + } } }; managedObjects["/org/opensuse/Agama/Storage1/system/60"] = { @@ -369,6 +404,7 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/sdb" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: ["pci-0000:00-19"] } @@ -384,6 +420,7 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/md0" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } @@ -407,6 +444,7 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/mapper/isw_ddgdcbibhd_244" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } @@ -430,6 +468,7 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/mapper/36005076305ffc73a00000000000013b4" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } @@ -450,11 +489,34 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/dasda" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } } }; + managedObjects["/org/opensuse/Agama/Storage1/system/66"] = { + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sda1" }, + Size: { t: "x", v: 512 }, + RecoverableSize: { t: "x", v: 128 }, + Systems: { t: "as", v: ["Windows"] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/67"] = { + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sda2" }, + Size: { t: "x", v: 512 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: ["openSUSE Leap 15.2"] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + } + }; } }; From 421ff9d2e14a0fed46ef270e2a0604a5f497a21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 14 Feb 2024 11:49:27 +0000 Subject: [PATCH 05/38] [web] Add space actions table --- web/src/assets/styles/utilities.scss | 4 + web/src/components/storage/ProposalPage.jsx | 3 +- .../storage/ProposalSpacePolicySection.jsx | 450 +++++++++++++++--- 3 files changed, 380 insertions(+), 77 deletions(-) diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss index 4452ec0c22..ae12b38719 100644 --- a/web/src/assets/styles/utilities.scss +++ b/web/src/assets/styles/utilities.scss @@ -34,6 +34,10 @@ font-weight: bold; } +.fs-small { + font-size: var(--fs-small); +} + // Utility classes for sizing icons .icon-xxxs { block-size: var(--icon-size-xxxs); diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index fad35cf99d..5269e7fcd2 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -221,11 +221,12 @@ export default function ProposalPage() { encryptionMethods={state.encryptionMethods} settings={state.settings} onChange={changeSettings} - isLoading={state.loading} + isLoading={!state.settings} /> Object.keys(device).includes("vendor"); + +/** + * Column content with the description of a device. * @component * * @param {object} props - * @param {string} props.id - Form ID. - * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. - * @param {onSubmitFn} [props.onSubmit=noop] - On submit callback. + * @param {StorageDevice} props.device + */ +const DeviceDescriptionColumn = ({ device }) => { + return ( + <> +
{device.name}
+ {`${device.vendor} ${device.model}`}} + /> + + ); +}; + +/** + * Column content with information about the current content of the device. + * @component * - * @callback onSubmitFn - * @param {string} policy - Name of the selected policy. + * @param {object} props + * @param {StorageDevice} props.device */ -const SpacePolicyForm = ({ - id, - policy, - onSubmit: onSubmitProp = noop -}) => { - const [spacePolicy, setSpacePolicy] = useState(policy); +const DeviceContentColumn = ({ device }) => { + const PartitionTableContent = () => { + const numPartitions = device.partitionTable.partitions.length; - const onSubmit = (e) => { - e.preventDefault(); - onSubmitProp(spacePolicy); + return ( + <> +
+ {sprintf(n_("%d partition", "%d partitions", numPartitions), numPartitions)} +
+
+ {sprintf(_("%s partition table"), device.partitionTable.type.toUpperCase())} +
+ + ); + }; + + const BlockContent = () => { + const systems = device.systems; + + if (systems.length > 0) + return {systems.join(", ")}; + else + return {_("Not identified")}; + }; + + return (device.partitionTable ? : ); +}; + +/** + * Column content with information about the size of the device. + * @component + * + * @param {object} props + * @param {StorageDevice} props.device + */ +const DeviceSizeColumn = ({ device }) => { + const UnusedSize = () => { + const used = device.partitionTable?.partitions.reduce((s, p) => s + p.size, 0) || 0; + const unused = device.size - used; + + if (unused === 0) return null; + + return ( +
+ {sprintf(_("%s unused"), deviceSize(unused))} +
+ ); + }; + + const RecoverableSize = () => { + const size = device.recoverableSize; + let text; + + if (size === 0) + text = _("No recoverable space"); + else + text = sprintf(_("%s recoverable"), deviceSize(device.recoverableSize)); + + return
{text}
; }; return ( -
- - + <> +
{deviceSize(device.size)}
+ } else={ } /> + ); }; /** - * Allows to select SpacePolicy. + * Column content with the space action for a device. * @component * * @param {object} props - * @param {ProposalSettings} props.settings - Settings used for calculating a proposal. - * @param {boolean} [props.isLoading=false] - Whether to show the selector as loading. - * @param {onChangeFn} [props.onChange=noop] - On change callback. + * @param {StorageDevice} props.device + * @param {string} props.action - Possible values: "force_delete", "resize" or "keep". + * @param {boolean} [props.isDisabled=false] + * @param {(action: SpaceAction) => void} [props.onChange] + */ +const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noop }) => { + const changeAction = (action) => onChange({ device: device.name, action }); + + return ( + + ); +}; + +/** + * Row for configuring the space action of a device. + * @component * - * @callback onChangeFn - * @param {string} policy + * @param {object} props + * @param {StorageDevice} props.device + * @param {ProposalSettings} props.settings + * @param {number} props.rowIndex - @see {@link https://www.patternfly.org/components/table/#tree-table} + * @param {number} [props.level=1] - @see {@link https://www.patternfly.org/components/table/#tree-table} + * @param {number} [props.setSize=0] - @see {@link https://www.patternfly.org/components/table/#tree-table} + * @param {number} [props.posInSet=0] - @see {@link https://www.patternfly.org/components/table/#tree-table} + * @param {boolean} [props.isExpanded=false] - @see {@link https://www.patternfly.org/components/table/#tree-table} + * @param {boolean} [props.isHidden=false] - @see {@link https://www.patternfly.org/components/table/#tree-table} + * @param {function} [props.onCollapse] - @see {@link https://www.patternfly.org/components/table/#tree-table} + * @param {(action: SpaceAction) => void} [props.onChange] */ -const SpacePolicyField = ({ +const DeviceRow = ({ + device, settings, - isLoading = false, + rowIndex, + level = 1, + setSize = 0, + posInSet = 1, + isExpanded = false, + isHidden = false, + onCollapse = noop, onChange = noop }) => { - const [isFormOpen, setIsFormOpen] = useState(false); - const [spacePolicy, setSpacePolicy] = useState(settings.spacePolicy); + const treeRow = { + onCollapse, + rowIndex, + props: { + isExpanded, + isHidden, + 'aria-level': level, + 'aria-posinset': posInSet, + 'aria-setsize': setSize + } + }; - const openForm = () => setIsFormOpen(true); - const closeForm = () => setIsFormOpen(false); + const spaceAction = settings.spaceActions.find(a => a.device === device.name); + const isDisabled = settings.spacePolicy !== "custom"; - const onSubmitForm = (policy) => { - onChange(policy); - setSpacePolicy(policy); - closeForm(); - }; + return ( + + + + + + + + + } + /> + + + ); +}; - if (isLoading) return ; +/** + * Table for configuring the space actions. + * @component + * + * @param {object} props + * @param {ProposalSettings} props.settings + * @param {(action: SpaceAction) => void} [props.onChange] + */ +const SpaceActionsTable = ({ settings, onChange = noop }) => { + const [expandedDevices, setExpandedDevices] = useLocalStorage("storage-space-actions-expanded", []); + const [autoExpanded, setAutoExpanded] = useLocalStorage("storage-space-actions-auto-expanded", false); - const description = _("Select how to make free space in the disks selected for allocating the \ - file systems."); + useEffect(() => { + const policy = settings.spacePolicy; + const devices = settings.installationDevices.map(d => d.name); + let currentExpanded = devices.filter(d => expandedDevices.includes(d)); - return ( -
- {/* TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" */} - {_("Find space")} - - -
- - { + const rows = []; + + settings.installationDevices?.forEach((device, index) => { + const isExpanded = expandedDevices.includes(device.name); + const children = device.partitionTable?.partitions; + + const onCollapse = () => { + const otherExpandedDevices = expandedDevices.filter(name => name !== device.name); + const expanded = isExpanded ? otherExpandedDevices : [...otherExpandedDevices, device.name]; + setExpandedDevices(expanded); + }; + + rows.push( + + ); + + children?.forEach((child, index) => { + rows.push( + -
- - - {_("Accept")} - - - -
-
+ ); + }); + }); + + return rows; + }; + + return ( + + + + + + + + + + {renderRows()} +
{columnNames.device}{columnNames.content}{columnNames.size}{columnNames.action}
+ ); +}; + +/** + * Space policy selector. + * @component + * + * @param {object} props + * @param {SpacePolicy} props.currentPolicy + * @param {(policy: string) => void} [props.onChange] + */ +const SpacePolicySelector = ({ currentPolicy, onChange = noop }) => { + return ( + <> +

+ {_("Select how to make free space in the selected disks for allocating the file systems.")} +

+ +
+ {SPACE_POLICIES.map((policy) => { + const isChecked = policy.name === currentPolicy.name; + + return ( + onChange(policy.name)} + /> + ); + })} +
+ +

{currentPolicy.description}

+ ); }; /** * Section for configuring the space policy. * @component + * + * @param {ProposalSettings} settings + * @param {boolean} [isLoading=false] + * @param {(settings: ProposalSettings) => void} [onChange] */ export default function ProposalSpacePolicySection({ settings, + isLoading = false, onChange = noop }) { const changeSpacePolicy = (policy) => { onChange({ spacePolicy: policy }); }; + const changeSpaceActions = (spaceAction) => { + const spaceActions = settings.spaceActions.filter(a => a.device !== spaceAction.device); + if (spaceAction.action !== "keep") spaceActions.push(spaceAction); + + onChange({ spaceActions }); + }; + + const currentPolicy = SPACE_POLICIES.find(p => p.name === settings.spacePolicy) || SPACE_POLICIES[0]; + return (
- } + else={ + <> + + 0} + then={} + /> + + } />
); From 1435add1d78eab7e53e5e3332fb52120cf771991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 14 Feb 2024 13:14:30 +0000 Subject: [PATCH 06/38] [service] Add Filesystem D-Bus interface --- service/lib/agama/dbus/storage/device.rb | 11 +++- service/lib/agama/dbus/storage/interfaces.rb | 3 +- .../dbus/storage/interfaces/filesystem.rb | 61 +++++++++++++++++++ .../test/agama/dbus/storage/device_test.rb | 3 + .../storage/interfaces/filesystem_examples.rb | 42 +++++++++++++ 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 service/lib/agama/dbus/storage/interfaces/filesystem.rb create mode 100644 service/test/agama/dbus/storage/interfaces/filesystem_examples.rb diff --git a/service/lib/agama/dbus/storage/device.rb b/service/lib/agama/dbus/storage/device.rb index 5001dd25e0..c622e14eb8 100644 --- a/service/lib/agama/dbus/storage/device.rb +++ b/service/lib/agama/dbus/storage/device.rb @@ -27,6 +27,7 @@ require "agama/dbus/storage/interfaces/md" require "agama/dbus/storage/interfaces/block" require "agama/dbus/storage/interfaces/partition_table" +require "agama/dbus/storage/interfaces/filesystem" module Agama module DBus @@ -77,7 +78,7 @@ def storage_device=(value) attr_reader :tree # Adds the required interfaces according to the storage object - def add_interfaces # rubocop:disable Metrics/CyclomaticComplexity + def add_interfaces # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity interfaces = [] interfaces << Interfaces::Drive if drive? interfaces << Interfaces::Raid if storage_device.is?(:dm_raid) @@ -85,6 +86,7 @@ def add_interfaces # rubocop:disable Metrics/CyclomaticComplexity interfaces << Interfaces::Multipath if storage_device.is?(:multipath) interfaces << Interfaces::Block if storage_device.is?(:blk_device) interfaces << Interfaces::PartitionTable if partition_table? + interfaces << Interfaces::Filesystem if filesystem? interfaces.each { |i| singleton_class.include(i) } end @@ -110,6 +112,13 @@ def partition_table? storage_device.respond_to?(:partition_table?) && storage_device.partition_table? end + + # Whether the storage device is formatted. + # + # @return [Boolean] + def filesystem? + !storage_device.filesystem.nil? + end end end end diff --git a/service/lib/agama/dbus/storage/interfaces.rb b/service/lib/agama/dbus/storage/interfaces.rb index 7ac6c81c0f..95ce2e08a2 100644 --- a/service/lib/agama/dbus/storage/interfaces.rb +++ b/service/lib/agama/dbus/storage/interfaces.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -35,4 +35,5 @@ module Interfaces require "agama/dbus/storage/interfaces/md" require "agama/dbus/storage/interfaces/block" require "agama/dbus/storage/interfaces/partition_table" +require "agama/dbus/storage/interfaces/filesystem" require "agama/dbus/storage/interfaces/dasd_manager" diff --git a/service/lib/agama/dbus/storage/interfaces/filesystem.rb b/service/lib/agama/dbus/storage/interfaces/filesystem.rb new file mode 100644 index 0000000000..51e69d0a39 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/filesystem.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + # Interface for file systems. + # + # @note This interface is intended to be included by {Device} if needed. + module Filesystem + FILESYSTEM_INTERFACE = "org.opensuse.Agama.Storage1.Filesystem" + private_constant :FILESYSTEM_INTERFACE + + # File system type. + # + # @return [String] e.g., "ext4" + def filesystem_type + storage_device.filesystem.type.to_s + end + + # Whether the file system contains an EFI. + # + # @return [Boolean] + def filesystem_efi? + storage_device.filesystem.efi? + end + + def self.included(base) + base.class_eval do + dbus_interface FILESYSTEM_INTERFACE do + dbus_reader :filesystem_type, "s", dbus_name: "Type" + dbus_reader :filesystem_efi?, "b", dbus_name: "EFI" + end + end + end + end + end + end + end +end diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index 6fa03e64d4..7b49f3d9c1 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -27,6 +27,7 @@ require_relative "./interfaces/block_examples" require_relative "./interfaces/md_examples" require_relative "./interfaces/partition_table_examples" +require_relative "./interfaces/filesystem_examples" require "agama/dbus/storage/device" require "agama/dbus/storage/devices_tree" require "dbus" @@ -142,6 +143,8 @@ include_examples "PartitionTable interface" + include_examples "Filesystem interface" + describe "#storage_device=" do before do allow(subject).to receive(:dbus_properties_changed) diff --git a/service/test/agama/dbus/storage/interfaces/filesystem_examples.rb b/service/test/agama/dbus/storage/interfaces/filesystem_examples.rb new file mode 100644 index 0000000000..f2323e662c --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/filesystem_examples.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../../../test_helper" + +shared_examples "Filesystem interface" do + describe "Filesystem D-Bus interface" do + let(:scenario) { "multipath-formatted.xml" } + + let(:device) { devicegraph.find_by_name("/dev/mapper/0QEMU_QEMU_HARDDISK_mpath1") } + + describe "#filesystem_type" do + it "returns the file system type" do + expect(subject.filesystem_type).to eq("ext4") + end + end + + describe "#filesystem_efi?" do + it "returns whether the file system is an EFI" do + expect(subject.filesystem_efi?).to eq(false) + end + end + end +end From 09c5be77b7dfb456e8aafc227413a537071cc2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 14 Feb 2024 13:34:13 +0000 Subject: [PATCH 07/38] [web] Read file system properties --- web/src/client/storage.js | 20 ++++++++++++++++++-- web/src/client/storage.test.js | 13 +++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index ece1ede48a..3fe6a0383f 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -129,10 +129,16 @@ class DevicesManager { * @property {string[]} [systems] - Name of the installed systems * @property {string[]} [udevIds] * @property {string[]} [udevPaths] - * @property {PartitionTableData} [partitionTable] + * @property {PartitionTable} [partitionTable] + * @property {Filesystem} [filesystem] * - * @typedef {object} PartitionTableData + * @typedef {object} PartitionTable * @property {string} type + * @property {StorageDevice[]} partitions + * + * @typedef {object} Filesystem + * @property {string} type + * @property {boolean} isEFI */ async getDevices() { const buildDevice = (path, dbusDevices) => { @@ -180,6 +186,13 @@ class DevicesManager { }; }; + const addFilesystemProperties = (device, filesystemProperties) => { + device.filesystem = { + type: filesystemProperties.Type.v, + isEFI: filesystemProperties.EFI.v + }; + }; + const device = { sid: path.split("/").pop(), type: "" @@ -203,6 +216,9 @@ class DevicesManager { const ptableProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.PartitionTable"]; if (ptableProperties !== undefined) addPtableProperties(device, ptableProperties); + const filesystemProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Filesystem"]; + if (filesystemProperties !== undefined) addFilesystemProperties(device, filesystemProperties); + return device; }; diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 43c8958e3c..2f4416f94c 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -42,7 +42,11 @@ const sda1 = { recoverableSize: 128, systems : ["Windows"], udevIds: [], - udevPaths: [] + udevPaths: [], + filesystem: { + type: "ntfs", + isEFI: false + } }; const sda2 = { @@ -173,7 +177,8 @@ const systemDevices = { udevIds: [], udevPaths: [] }, - sda1, sda2 + sda1, + sda2 }; const contexts = { @@ -504,6 +509,10 @@ const contexts = { Systems: { t: "as", v: ["Windows"] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Filesystem": { + Type: { t: "s", v: "ntfs" }, + EFI: { t: "b", v: false } } }; managedObjects["/org/opensuse/Agama/Storage1/system/67"] = { From bd2e0050387e4c1354cdb2f182bc9a924cf191eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Wed, 14 Feb 2024 13:35:01 +0000 Subject: [PATCH 08/38] [web] Show file system properties --- .../storage/ProposalSpacePolicySection.jsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 732f59a0db..5271528869 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -129,11 +129,28 @@ const DeviceContentColumn = ({ device }) => { const BlockContent = () => { const systems = device.systems; + const filesystem = device.filesystem; - if (systems.length > 0) - return {systems.join(", ")}; - else - return {_("Not identified")}; + const content = () => { + if (systems.length > 0) return systems.join(", "); + if (device.filesystem?.isEFI) return _("EFI"); + + return _("Not identified"); + }; + + return ( + <> +
{content()}
+ + {sprintf(_("%s file system"), filesystem?.type)} + + } + /> + + ); }; return (device.partitionTable ? : ); From bae57b1996dd8a678ea8ccd310c45cafb624da4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 15 Feb 2024 08:51:28 +0000 Subject: [PATCH 09/38] [web] Several improvements - Wording, use FormSelect, adapt loading state --- web/src/components/storage/ProposalPage.jsx | 4 +- .../storage/ProposalSettingsSection.jsx | 2 +- .../storage/ProposalSpacePolicySection.jsx | 69 +++++++++++-------- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/web/src/components/storage/ProposalPage.jsx b/web/src/components/storage/ProposalPage.jsx index 5269e7fcd2..4d730cce93 100644 --- a/web/src/components/storage/ProposalPage.jsx +++ b/web/src/components/storage/ProposalPage.jsx @@ -221,12 +221,12 @@ export default function ProposalPage() { encryptionMethods={state.encryptionMethods} settings={state.settings} onChange={changeSettings} - isLoading={!state.settings} + isLoading={state.loading} /> diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 5271528869..8d38c91eba 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -20,7 +20,7 @@ */ import React, { useEffect } from "react"; -import { Radio } from "@patternfly/react-core"; +import { FormSelect, FormSelectOption, Radio } from "@patternfly/react-core"; import { _, n_, N_ } from "~/i18n"; import { deviceSize } from '~/components/storage/utils'; @@ -64,7 +64,7 @@ Only the space that is not assigned to any partition will be used.") { name: "custom", label: N_("Custom"), - description: N_("Indicate what to do with each partition.") + description: N_("Select what to do with each partition.") } ]; @@ -208,14 +208,19 @@ const DeviceSizeColumn = ({ device }) => { * @param {(action: SpaceAction) => void} [props.onChange] */ const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noop }) => { - const changeAction = (action) => onChange({ device: device.name, action }); + const changeAction = (_, action) => onChange({ device: device.name, action }); return ( - + + + + + ); }; @@ -389,30 +394,34 @@ const SpacePolicySelector = ({ currentPolicy, onChange = noop }) => { return ( <>

- {_("Select how to make free space in the selected disks for allocating the file systems.")} + {_("Indicate how to make free space in the selected disks for allocating the file systems:")}

-
- {SPACE_POLICIES.map((policy) => { - const isChecked = policy.name === currentPolicy.name; - - return ( - onChange(policy.name)} - /> - ); - })} -
+
+
+ {SPACE_POLICIES.map((policy) => { + const isChecked = policy.name === currentPolicy.name; + + return ( + onChange(policy.name)} + /> + ); + })} +
-

{currentPolicy.description}

+
+ {currentPolicy.description} +
+
); }; @@ -446,7 +455,7 @@ export default function ProposalSpacePolicySection({ return (
} else={ <> From 30d9d15b0d3dae96d7fc20bdbf5e8d77c32374d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 15 Feb 2024 09:35:01 +0000 Subject: [PATCH 10/38] [web] CSS styles for tree table --- .../assets/styles/patternfly-overrides.scss | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index 5e2d2f9fe5..13f297b85a 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -245,9 +245,28 @@ ul { } } +// Styles for tree table used by storage page. + +.pf-v5-c-table tbody { + border-block-end: var(--pf-v5-c-table--border-width--base) solid var(--pf-v5-c-table--BorderColor); +} + +.pf-v5-c-table td > .pf-v5-c-form-control { + inline-size: max-content; + margin: 0 auto; +} + +.pf-v5-c-table tr[aria-level="1"] { + border-block-end: 0; + border-block-start: var(--pf-v5-c-table--border-width--base) solid var(--pf-v5-c-table--BorderColor); +} + +.pf-v5-c-table tr[aria-level="2"] { + border-block-end: 0; +} + @media screen and (width <= 768px) { .pf-m-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr):not(.pf-v5-c-table__expandable-row) { padding-inline: 0; } - } From c9def51ede2660ce010e6606371f3fc79e367d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= Date: Thu, 15 Feb 2024 12:22:37 +0000 Subject: [PATCH 11/38] [web] Adapt existing tests --- .../components/storage/ProposalPage.test.jsx | 5 +- .../storage/ProposalSettingsSection.test.jsx | 166 +----------------- 2 files changed, 4 insertions(+), 167 deletions(-) diff --git a/web/src/components/storage/ProposalPage.test.jsx b/web/src/components/storage/ProposalPage.test.jsx index 424b3fb747..66ab734f59 100644 --- a/web/src/components/storage/ProposalPage.test.jsx +++ b/web/src/components/storage/ProposalPage.test.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -92,10 +92,11 @@ it("renders a warning about modified devices", async () => { await screen.findByText(/Devices will not be modified/); }); -it("renders the settings and actions sections", async () => { +it("renders the settings, find space and actions sections", async () => { installerRender(); await screen.findByText(/Settings/); + await screen.findByText(/Find Space/); await screen.findByText(/Planned Actions/); }); diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index ec9d1b08cb..027e1d7d36 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -503,167 +503,3 @@ describe("Encryption field", () => { }); }); }); - -describe("Space policy field", () => { - beforeEach(() => { - props.settings = { installationDevices: [vda] }; - }); - - describe("if there is no space policy", () => { - beforeEach(() => { - props.settings.spacePolicy = undefined; - }); - - it("renders loading content", async () => { - plainRender(); - - await waitFor(() => ( - expect(screen.queryByText(/Find space/)).not.toBeInTheDocument()) - ); - screen.getAllByText("PFSkeleton"); - }); - }); - - describe("if the space policy is set to 'delete'", () => { - beforeEach(() => { - props.settings.spacePolicy = "delete"; - }); - - it("renders the text about deleting content", () => { - plainRender(); - - screen.getByText("Find space"); - screen.getByRole("button", { name: "deleting all content of the installation device" }); - }); - - describe("if there are more than one disk", () => { - beforeEach(() => { - props.settings.installationDevices = [vda, md0, md1]; - }); - - it("indicates the number of disks", () => { - plainRender(); - - screen.getByText("Find space"); - screen.getByRole("button", { name: "deleting all content of the 3 selected disks" }); - }); - }); - }); - - describe("if the space policy is set to 'resize'", () => { - beforeEach(() => { - props.settings.spacePolicy = "resize"; - }); - - it("renders the text about resizing content", () => { - plainRender(); - - screen.getByText("Find space"); - screen.getByRole("button", { name: "shrinking partitions of the installation device" }); - }); - - describe("if there are more than one disk", () => { - beforeEach(() => { - props.settings.installationDevices = [vda, md0, md1]; - }); - - it("indicates the number of disks", () => { - plainRender(); - - screen.getByText("Find space"); - screen.getByRole("button", { name: "shrinking partitions of the 3 selected disks" }); - }); - }); - }); - - describe("if the space policy is set to 'keep'", () => { - beforeEach(() => { - props.settings.spacePolicy = "keep"; - }); - - it("renders the text about keeping content", () => { - plainRender(); - - screen.getByText("Find space"); - screen.getByRole("button", { name: "without modifying any partition" }); - }); - }); - - describe("when the button for changing the space policy is clicked", () => { - beforeEach(() => { - props.settings.installationDevices = [vda, md0]; - props.settings.spacePolicy = "keep"; - props.onChange = jest.fn(); - }); - - it("opens a popup for selecting the space policy", async () => { - const { user } = plainRender(); - - const button = screen.getByRole("button", { name: /without modifying/ }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - within(popup).getByText("Space Policy"); - within(popup).getByRole("option", { name: /Delete current content/ }); - within(popup).getByRole("option", { name: /Shrink existing partitions/ }); - within(popup).getByRole("option", { name: /Use available space/, selected: true }); - }); - - it("allows to show the installation devices", async () => { - const { user } = plainRender(); - - const button = screen.getByRole("button", { name: /without modifying/ }); - await user.click(button); - const popup = await screen.findByRole("dialog"); - - await waitFor(() => ( - expect(within(popup).queryByText("/dev/vda")).not.toBeVisible() && - expect(within(popup).queryByText("/dev/md0")).not.toBeVisible() - )); - - const toggle = within(popup).getByRole("button", { name: /This will affect/ }); - await user.click(toggle); - - expect(within(popup).getByText("/dev/vda")).toBeVisible(); - expect(within(popup).getByText("/dev/md0")).toBeVisible(); - }); - - describe("if the popup is canceled", () => { - it("closes the popup without selecting a new space policy", async () => { - const { user } = plainRender(); - - const button = screen.getByRole("button", { name: /without modifying/ }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("option", { name: /Shrink/ }); - - await user.click(option); - const cancel = within(popup).getByRole("button", { name: "Cancel" }); - await user.click(cancel); - - expect(props.onChange).not.toHaveBeenCalled(); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); - - describe("if the popup is accepted", () => { - it("closes the popup selecting the new space policy", async () => { - const { user } = plainRender(); - - const button = screen.getByRole("button", { name: /without modifying/ }); - await user.click(button); - - const popup = await screen.findByRole("dialog"); - const option = within(popup).getByRole("option", { name: /Shrink/ }); - - await user.click(option); - const accept = within(popup).getByRole("button", { name: "Accept" }); - await user.click(accept); - - expect(props.onChange).toHaveBeenCalledWith({ spacePolicy: "resize" }); - expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); - }); - }); - }); -}); From 0203531926ff464245f03611ba235c186c56e1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= Date: Thu, 15 Feb 2024 13:08:48 +0000 Subject: [PATCH 12/38] [web] Drop space-policy-utils.jsx file No longer needed with the new approach in which the space policy selection has its own section. --- web/src/components/storage/index.js | 1 - .../components/storage/space-policy-utils.jsx | 185 ------------------ 2 files changed, 186 deletions(-) delete mode 100644 web/src/components/storage/space-policy-utils.jsx diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.js index f048b4d8f7..14717598a8 100644 --- a/web/src/components/storage/index.js +++ b/web/src/components/storage/index.js @@ -32,5 +32,4 @@ export { default as ZFCPPage } from "./ZFCPPage"; export { default as ZFCPDiskForm } from "./ZFCPDiskForm"; export { default as ISCSIPage } from "./ISCSIPage"; export { DeviceList, DeviceSelector } from "./device-utils"; -export { SpacePolicyButton, SpacePolicySelector, SpacePolicyDisksHint } from "./space-policy-utils"; export { default as VolumeForm } from "./VolumeForm"; diff --git a/web/src/components/storage/space-policy-utils.jsx b/web/src/components/storage/space-policy-utils.jsx deleted file mode 100644 index 6d3259aeb9..0000000000 --- a/web/src/components/storage/space-policy-utils.jsx +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (c) [2023-2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React, { useState } from "react"; - -import { _, n_ } from "~/i18n"; -import { sprintf } from "sprintf-js"; -import { noop } from "~/utils"; -import { Button, ExpandableSection, Hint, HintBody } from "@patternfly/react-core"; -import { Selector } from "~/components/core"; -import { DeviceList } from "~/components/storage"; - -/** - * Content for a space policy item - * @component - * - * @param {Object} props - * @param {Locale} props.locale - */ -const PolicyItem = ({ policy }) => { - const Title = () => { - let text; - - switch (policy) { - case "delete": - // TRANSLATORS: automatic actions to find space for installation in the target disk(s) - text = _("Delete current content"); - break; - case "resize": - // TRANSLATORS: automatic actions to find space for installation in the target disk(s) - text = _("Shrink existing partitions"); - break; - case "keep": - // TRANSLATORS: automatic actions to find space for installation in the target disk(s) - text = _("Use available space"); - break; - } - - return
{text}
; - }; - - const Description = () => { - let text; - - switch (policy) { - case "delete": - text = _("All partitions will be removed and any data in the disks will be lost."); - break; - case "resize": - text = _("The data is kept, but the current partitions will be resized as needed to make enough space."); - break; - case "keep": - text = _("The data is kept and existing partitions will not be modified. \ -Only the space that is not assigned to any partition will be used."); - break; - } - - return

{text}

; - }; - - return ( -
- - <Description /> - </div> - ); -}; - -const renderPolicyOption = ({ id }) => <PolicyItem policy={id} />; - -/** - * Component for selecting a policy to make space. - * @component - * - * @param {Object} props - * @param {string} [props.value] - Id of the currently selected policy. - * @param {(id: string) => void} [props.onChange] - Callback to be called when the selected policy - * changes. - */ -const SpacePolicySelector = ({ value, onChange = noop }) => { - const onSelectionChange = (selection) => onChange(selection[0]); - const options = [ - { id: "delete" }, - { id: "resize" }, - { id: "keep" } - ]; - - return ( - <Selector - aria-label={_("Select a mechanism to make space")} - options={options} - renderOption={renderPolicyOption} - selectedIds={[value]} - onSelectionChange={onSelectionChange} - /> - ); -}; - -const SpacePolicyButton = ({ policy, devices, onClick = noop }) => { - const Text = () => { - const num = devices.length; - - switch (policy) { - case "delete": - return sprintf( - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space deleting all content[...]" - n_( - "deleting all content of the installation device", - "deleting all content of the %d selected disks", - num), - num - ); - case "resize": - return sprintf( - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space shrinking partitions[...]" - n_( - "shrinking partitions of the installation device", - "shrinking partitions of the %d selected disks", - num), - num - ); - case "keep": - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space without modifying any partition". - return _("without modifying any partition"); - } - console.log("Unsupported value " + policy); - return "error"; - }; - - return <Button variant="link" isInline onClick={onClick}><Text /></Button>; -}; - -const SpacePolicyDisksHint = ({ devices }) => { - const [isExpanded, setIsExpanded] = useState(false); - - const label = (num) => { - return sprintf( - n_( - "This will only affect the installation device", - "This will affect the %d disks selected for installation", - num - ), - num - ); - }; - - const num = devices.length; - - return ( - <Hint> - <HintBody> - <ExpandableSection - isExpanded={isExpanded} - onToggle={() => setIsExpanded(!isExpanded)} - toggleText={label(num)} - > - <DeviceList devices={devices} /> - </ExpandableSection> - </HintBody> - </Hint> - ); -}; - -export { SpacePolicyButton, SpacePolicySelector, SpacePolicyDisksHint }; From 43cd45f2b129dba1ccd9fa791234f9a89c35ebe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Thu, 15 Feb 2024 14:39:27 +0000 Subject: [PATCH 13/38] [service] Fix test --- service/test/agama/dbus/storage/devices_tree_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/test/agama/dbus/storage/devices_tree_test.rb b/service/test/agama/dbus/storage/devices_tree_test.rb index 2f38bd69fe..42e0024a33 100644 --- a/service/test/agama/dbus/storage/devices_tree_test.rb +++ b/service/test/agama/dbus/storage/devices_tree_test.rb @@ -149,7 +149,7 @@ context "if an exported D-Bus object does not represent any of the current devices" do let(:dbus_objects) { [dbus_object1] } let(:dbus_object1) { Agama::DBus::Storage::Device.new(sdd, subject.path_for(sdd), subject) } - let(:sdd) { instance_double(Y2Storage::Disk, sid: 1, is?: false) } + let(:sdd) { instance_double(Y2Storage::Disk, sid: 1, is?: false, filesystem: false) } it "unexports the D-Bus object" do expect(service).to unexport_object("#{root_path}/1") From 0a0e05f317095ca5ef9a658a15654b5ec6a9821b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Thu, 15 Feb 2024 14:39:51 +0000 Subject: [PATCH 14/38] [service] Use D-Bus path instead of device name --- service/lib/agama/dbus/storage/interfaces/md.rb | 11 ++++------- .../lib/agama/dbus/storage/interfaces/multipath.rb | 10 ++++------ service/lib/agama/dbus/storage/interfaces/raid.rb | 10 ++++------ .../test/agama/dbus/storage/interfaces/md_examples.rb | 10 +++++++--- .../dbus/storage/interfaces/multipath_examples.rb | 10 +++++++--- .../agama/dbus/storage/interfaces/raid_examples.rb | 10 +++++++--- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/service/lib/agama/dbus/storage/interfaces/md.rb b/service/lib/agama/dbus/storage/interfaces/md.rb index ed94477fec..8aaf0f5f2a 100644 --- a/service/lib/agama/dbus/storage/interfaces/md.rb +++ b/service/lib/agama/dbus/storage/interfaces/md.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -46,14 +46,11 @@ def md_level storage_device.md_level.to_s end - # Member devices of the MD RAID - # - # TODO: return object paths once all possible members (e.g., partitions) are exported - # on D-Bus. + # Paths of the D-Bus objects representing the member devices of the MD RAID. # # @return [Array<String>] def md_members - storage_device.plain_devices.map(&:name) + storage_device.plain_devices.map { |p| tree.path_for(p) } end def self.included(base) @@ -61,7 +58,7 @@ def self.included(base) dbus_interface MD_INTERFACE do dbus_reader :md_uuid, "s", dbus_name: "UUID" dbus_reader :md_level, "s", dbus_name: "Level" - dbus_reader :md_members, "as", dbus_name: "Members" + dbus_reader :md_members, "ao", dbus_name: "Members" end end end diff --git a/service/lib/agama/dbus/storage/interfaces/multipath.rb b/service/lib/agama/dbus/storage/interfaces/multipath.rb index 29fc9279d1..06841520da 100644 --- a/service/lib/agama/dbus/storage/interfaces/multipath.rb +++ b/service/lib/agama/dbus/storage/interfaces/multipath.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -32,19 +32,17 @@ module Multipath MULTIPATH_INTERFACE = "org.opensuse.Agama.Storage1.Multipath" private_constant :MULTIPATH_INTERFACE - # Multipath wires - # - # TODO: return object paths + # Paths of the D-Bus objects representing the multipath wires. # # @return [Array<String>] def multipath_wires - storage_device.parents.map(&:name) + storage_device.parents.map { |p| tree.path_for(p) } end def self.included(base) base.class_eval do dbus_interface MULTIPATH_INTERFACE do - dbus_reader :multipath_wires, "as", dbus_name: "Wires" + dbus_reader :multipath_wires, "ao", dbus_name: "Wires" end end end diff --git a/service/lib/agama/dbus/storage/interfaces/raid.rb b/service/lib/agama/dbus/storage/interfaces/raid.rb index 5767759e33..7a66fb3556 100644 --- a/service/lib/agama/dbus/storage/interfaces/raid.rb +++ b/service/lib/agama/dbus/storage/interfaces/raid.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -32,19 +32,17 @@ module Raid RAID_INTERFACE = "org.opensuse.Agama.Storage1.RAID" private_constant :RAID_INTERFACE - # Devices used by the DM RAID - # - # TODO: return object paths + # Paths of the D-Bus objects representing the devices used by the DM RAID. # # @return [Array<String>] def raid_devices - storage_device.parents.map(&:name) + storage_device.parents.map { |p| tree.path_for(p) } end def self.included(base) base.class_eval do dbus_interface RAID_INTERFACE do - dbus_reader :raid_devices, "as", dbus_name: "Devices" + dbus_reader :raid_devices, "ao", dbus_name: "Devices" end end end diff --git a/service/test/agama/dbus/storage/interfaces/md_examples.rb b/service/test/agama/dbus/storage/interfaces/md_examples.rb index 2816200979..3a8b769c70 100644 --- a/service/test/agama/dbus/storage/interfaces/md_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/md_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -46,8 +46,12 @@ end describe "#md_members" do - it "returns the name of the MD members" do - expect(subject.md_members).to contain_exactly("/dev/sda1", "/dev/sda2") + it "returns the D-Bus path of the MD members" do + sda1 = devicegraph.find_by_name("/dev/sda1") + sda2 = devicegraph.find_by_name("/dev/sda2") + + expect(subject.md_members) + .to contain_exactly(tree.path_for(sda1), tree.path_for(sda2)) end end end diff --git a/service/test/agama/dbus/storage/interfaces/multipath_examples.rb b/service/test/agama/dbus/storage/interfaces/multipath_examples.rb index f46209a9c3..f134a06872 100644 --- a/service/test/agama/dbus/storage/interfaces/multipath_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/multipath_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -28,8 +28,12 @@ let(:device) { devicegraph.multipaths.first } describe "#multipath_wires" do - it "returns the name of the Multipath wires" do - expect(subject.multipath_wires).to contain_exactly("/dev/sda", "/dev/sdb") + it "returns the D-Bus path of the Multipath wires" do + sda = devicegraph.find_by_name("/dev/sda") + sdb = devicegraph.find_by_name("/dev/sdb") + + expect(subject.multipath_wires) + .to contain_exactly(tree.path_for(sda), tree.path_for(sdb)) end end end diff --git a/service/test/agama/dbus/storage/interfaces/raid_examples.rb b/service/test/agama/dbus/storage/interfaces/raid_examples.rb index c8ac7bd28a..4a30d575d2 100644 --- a/service/test/agama/dbus/storage/interfaces/raid_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/raid_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -28,8 +28,12 @@ let(:device) { devicegraph.dm_raids.first } describe "#raid_devices" do - it "returns the name of the RAID devices" do - expect(subject.raid_devices).to contain_exactly("/dev/sdb", "/dev/sdc") + it "returns the D-Bus path of the RAID devices" do + sdb = devicegraph.find_by_name("/dev/sdb") + sdc = devicegraph.find_by_name("/dev/sdc") + + expect(subject.raid_devices) + .to contain_exactly(tree.path_for(sdb), tree.path_for(sdc)) end end end From 19245ee612ee6f5b6c6efab94f59576923c9d64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= <dgonzalez@suse.de> Date: Fri, 16 Feb 2024 08:38:27 +0000 Subject: [PATCH 15/38] [web] Adjust space policies descriptions According to @ancorgs's suggestion. --- web/src/components/storage/ProposalSpacePolicySection.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 8d38c91eba..1f972d8fe8 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -53,13 +53,12 @@ const SPACE_POLICIES = [ { name: "resize", label: N_("Shrink existing partitions"), - description: N_("The data is kept, but the current partitions will be resized as needed to make enough space.") + description: N_("The data is kept, but the current partitions will be resized as needed.") }, { name: "keep", label: N_("Use available space"), - description: N_("The data is kept and existing partitions will not be modified. \ -Only the space that is not assigned to any partition will be used.") + description: N_("Existing partitions and data will not be modified. Only the space not assigned to any partition will be used.") }, { name: "custom", From 8d432fb47ab1cfce05765aeca8f2fc069dff202b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= <dgonzalez@suse.de> Date: Fri, 16 Feb 2024 08:43:29 +0000 Subject: [PATCH 16/38] [web] Adds a core/OptionPicker component Which helps rendering a set of options with a tile look&feel. Not using PatternFly/Tile instead because it looks like it's going to be deprecated [1] and the PatternFly/Card#isSelectable component is a bit complex for this first use case. [1] https://github.com/patternfly/patternfly-react/issues/8542 --- web/src/assets/styles/blocks.scss | 26 +++++++ web/src/components/core/OptionsPicker.jsx | 71 ++++++++++++++++++ .../components/core/OptionsPicker.test.jsx | 75 +++++++++++++++++++ web/src/components/core/index.js | 1 + .../storage/ProposalSpacePolicySection.jsx | 58 ++++++-------- 5 files changed, 195 insertions(+), 36 deletions(-) create mode 100644 web/src/components/core/OptionsPicker.jsx create mode 100644 web/src/components/core/OptionsPicker.test.jsx diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index bafe238f42..e30dd7e91f 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -431,6 +431,32 @@ ul[data-type="agama/list"][role="grid"] { } } +[data-type="agama/options-picker"] { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--spacer-smaller); + + [role="option"] { + cursor: pointer; + border: 1px solid var(--color-gray); + padding: var(--spacer-small); + border-block-end-width: 4px; + + &[aria-selected="true"] { + background: var(--color-gray-light); + border-block-end-color: var(--color-primary); + } + + >:first-child { + margin-block-end: var(--spacer-small); + } + + >:last-child { + font-size: var(--fs-small); + } + } +} + [role="dialog"] { section:not([class^="pf-c"]) { > svg:first-child { diff --git a/web/src/components/core/OptionsPicker.jsx b/web/src/components/core/OptionsPicker.jsx new file mode 100644 index 0000000000..55cc44f850 --- /dev/null +++ b/web/src/components/core/OptionsPicker.jsx @@ -0,0 +1,71 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; + +/** + * Wrapper for OptionsPicker options + * @component + * + * @param {object} props + * @param {string} [props.title] - Text to be used as option title + * @param {string} [props.body] - Text to be used as option body + * @param {boolean} [props.isSelected=false] - Whether the option should be set as select of not + * @param {object} [props.props] - Other props sent to div#option node + */ +const Option = ({ title, body, isSelected = false, ...props }) => { + return ( + <div + {...props} + role="option" + aria-selected={isSelected} + > + <div><b>{title}</b></div> + <div>{body}</div> + </div> + ); +}; + +/** + * Helper component to build rich options picker + * @component + * + * @param {object} props + * @param {string} [props.ariaLabel] - Text to be used as accessible label + * @param {Array<Option>} props.children - A collection of Option + * @param {object} [props.props] - Other props sent to div#listbox node + */ +const OptionsPicker = ({ "aria-label": ariaLabel, children, ...props }) => { + return ( + <div + {...props} + role="listbox" + data-type="agama/options-picker" + aria-label={ariaLabel} + > + {children} + </div> + ); +}; + +OptionsPicker.Option = Option; + +export default OptionsPicker; diff --git a/web/src/components/core/OptionsPicker.test.jsx b/web/src/components/core/OptionsPicker.test.jsx new file mode 100644 index 0000000000..718b875e6e --- /dev/null +++ b/web/src/components/core/OptionsPicker.test.jsx @@ -0,0 +1,75 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { OptionsPicker } from "~/components/core"; + +describe("OptionsPicker", () => { + it("renders a node with listbox role", () => { + plainRender(<OptionsPicker />); + screen.getByRole("listbox"); + }); +}); + +describe("OptionsPicker.Option", () => { + it("renders a node with option role", () => { + plainRender(<OptionsPicker.Option />); + screen.getByRole("option"); + }); + + it("renders given title", () => { + plainRender(<OptionsPicker.Option title="Custom" />); + screen.getByRole("option", { name: "Custom" }); + }); + + it("renders given body", () => { + plainRender(<OptionsPicker.Option body="More freedom for user" />); + screen.getByRole("option", { name: "More freedom for user" }); + }); + + it("triggers given onClick callback when user clicks on it", async () => { + const onClick = jest.fn(); + const { user } = plainRender(<OptionsPicker.Option title="Custom" onClick={onClick} />); + const option = screen.getByRole("option", { name: "Custom" }); + await user.click(option); + expect(onClick).toHaveBeenCalled(); + }); + + it("sets as selected if isSelected is given", () => { + plainRender(<OptionsPicker.Option isSelected />); + const option = screen.getByRole("option"); + expect(option).toHaveAttribute("aria-selected", "true"); + }); + + it("sets as not selected if isSelected is not given", () => { + plainRender(<OptionsPicker.Option />); + const option = screen.getByRole("option"); + expect(option).toHaveAttribute("aria-selected", "false"); + }); + + it("sets as not selected if isSelected=false", () => { + plainRender(<OptionsPicker.Option isSelected={false} />); + const option = screen.getByRole("option"); + expect(option).toHaveAttribute("aria-selected", "false"); + }); +}); diff --git a/web/src/components/core/index.js b/web/src/components/core/index.js index b29309fcfb..0213c66215 100644 --- a/web/src/components/core/index.js +++ b/web/src/components/core/index.js @@ -56,3 +56,4 @@ export { default as NumericTextInput } from "./NumericTextInput"; export { default as PasswordInput } from "./PasswordInput"; export { default as DevelopmentInfo } from "./DevelopmentInfo"; export { default as Selector } from "./Selector"; +export { default as OptionsPicker } from "./OptionsPicker"; diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 1f972d8fe8..9f67cfd852 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -20,11 +20,11 @@ */ import React, { useEffect } from "react"; -import { FormSelect, FormSelectOption, Radio } from "@patternfly/react-core"; +import { FormSelect, FormSelectOption } from "@patternfly/react-core"; import { _, n_, N_ } from "~/i18n"; import { deviceSize } from '~/components/storage/utils'; -import { If, Section, SectionSkeleton } from "~/components/core"; +import { If, OptionsPicker, Section, SectionSkeleton } from "~/components/core"; import { noop, useLocalStorage } from "~/utils"; import { sprintf } from "sprintf-js"; import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/react-table'; @@ -382,46 +382,28 @@ const SpaceActionsTable = ({ settings, onChange = noop }) => { }; /** - * Space policy selector. + * Widget to allow user picking desired policy to make space * @component * * @param {object} props * @param {SpacePolicy} props.currentPolicy * @param {(policy: string) => void} [props.onChange] */ -const SpacePolicySelector = ({ currentPolicy, onChange = noop }) => { +const SpacePolicyPicker = ({ currentPolicy, onChange = noop }) => { return ( - <> - <p> - {_("Indicate how to make free space in the selected disks for allocating the file systems:")} - </p> - - <div> - <div className="split radio-group"> - {SPACE_POLICIES.map((policy) => { - const isChecked = policy.name === currentPolicy.name; - - return ( - <Radio - id={`space-policy-option-${policy.name}`} - key={`space-policy-${policy.name}`} - // eslint-disable-next-line agama-i18n/string-literals - label={_(policy.label)} - value={policy.name} - name="space-policies" - className={isChecked && "selected"} - isChecked={isChecked} - onChange={() => onChange(policy.name)} - /> - ); - })} - </div> - - <div aria-live="polite" className="highlighted-live-region"> - {currentPolicy.description} - </div> - </div> - </> + <OptionsPicker> + {SPACE_POLICIES.map((policy) => { + return ( + <OptionsPicker.Option + key={policy.name} + title={policy.label} + body={policy.description} + onClick={() => onChange(policy.name)} + isSelected={currentPolicy?.name === policy.name} + /> + ); + })} + </OptionsPicker> ); }; @@ -453,12 +435,16 @@ export default function ProposalSpacePolicySection({ return ( <Section title={_("Find Space")} className="flex-stack"> + <If condition={isLoading && settings.spacePolicy === undefined} then={<SectionSkeleton numRows={4} />} else={ <> - <SpacePolicySelector currentPolicy={currentPolicy} onChange={changeSpacePolicy} /> + <p> + {_("Indicate how to make free space in the selected disks for allocating the file systems:")} + </p> + <SpacePolicyPicker currentPolicy={currentPolicy} onChange={changeSpacePolicy} /> <If condition={settings.installationDevices?.length > 0} then={<SpaceActionsTable settings={settings} onChange={changeSpaceActions} />} From c0b3fa1a777eb3f8cbf0d6a84db2f777543455fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Fri, 16 Feb 2024 11:37:40 +0000 Subject: [PATCH 17/38] [web] Improvements in the table of space actions --- .../storage/ProposalSpacePolicySection.jsx | 101 +++++++++--------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 9f67cfd852..f278d24c31 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -22,7 +22,7 @@ import React, { useEffect } from "react"; import { FormSelect, FormSelectOption } from "@patternfly/react-core"; -import { _, n_, N_ } from "~/i18n"; +import { _, N_ } from "~/i18n"; import { deviceSize } from '~/components/storage/utils'; import { If, OptionsPicker, Section, SectionSkeleton } from "~/components/core"; import { noop, useLocalStorage } from "~/utils"; @@ -72,6 +72,7 @@ const columnNames = { device: N_("Used device"), content: N_("Current content"), size: N_("Size"), + details: N_("Details"), action: N_("Action") }; @@ -112,44 +113,34 @@ const DeviceDescriptionColumn = ({ device }) => { */ const DeviceContentColumn = ({ device }) => { const PartitionTableContent = () => { - const numPartitions = device.partitionTable.partitions.length; - return ( - <> - <div> - {sprintf(n_("%d partition", "%d partitions", numPartitions), numPartitions)} - </div> - <div className="fs-small"> - {sprintf(_("%s partition table"), device.partitionTable.type.toUpperCase())} - </div> - </> + <div> + {sprintf(_("%s partition table"), device.partitionTable.type.toUpperCase())} + </div> ); }; const BlockContent = () => { - const systems = device.systems; - const filesystem = device.filesystem; - const content = () => { + const systems = device.systems; if (systems.length > 0) return systems.join(", "); - if (device.filesystem?.isEFI) return _("EFI"); - return _("Not identified"); + const filesystem = device.filesystem; + if (filesystem?.isEFI) return _("EFI"); + if (filesystem) return sprintf(_("%s file system"), filesystem?.type); + + const component = device.component; + switch (component?.type) { + case "physical_volume": + return sprintf(_("LVM physical volume of %s"), component.deviceNames[0]); + case "md_device": + return sprintf(_("Member of RAID %s"), component.deviceNames[0]); + default: + return _("Not identified"); + } }; - return ( - <> - <div>{content()}</div> - <If - condition={filesystem} - then={ - <div className="fs-small"> - {sprintf(_("%s file system"), filesystem?.type)} - </div> - } - /> - </> - ); + return <div>{content()}</div>; }; return (device.partitionTable ? <PartitionTableContent /> : <BlockContent />); @@ -163,14 +154,19 @@ const DeviceContentColumn = ({ device }) => { * @param {StorageDevice} props.device */ const DeviceSizeColumn = ({ device }) => { + return <div>{deviceSize(device.size)}</div>; +}; + +const DeviceDetailsColumn = ({ device }) => { const UnusedSize = () => { - const used = device.partitionTable?.partitions.reduce((s, p) => s + p.size, 0) || 0; - const unused = device.size - used; + const partitioned = device.partitionTable?.partitions.reduce((s, p) => s + p.size, 0) || 0; + + if (device.filesystem) return null; - if (unused === 0) return null; + const unused = device.size - partitioned; return ( - <div className="fs-small"> + <div> {sprintf(_("%s unused"), deviceSize(unused))} </div> ); @@ -178,21 +174,18 @@ const DeviceSizeColumn = ({ device }) => { const RecoverableSize = () => { const size = device.recoverableSize; - let text; - if (size === 0) - text = _("No recoverable space"); - else - text = sprintf(_("%s recoverable"), deviceSize(device.recoverableSize)); + if (size === 0) return null; - return <div className="fs-small">{text}</div>; + return ( + <div> + {sprintf(_("Shrinkable by %s"), deviceSize(device.recoverableSize))} + </div> + ); }; return ( - <> - <div>{deviceSize(device.size)}</div> - <If condition={isDrive(device)} then={<UnusedSize />} else={<RecoverableSize /> } /> - </> + <If condition={isDrive(device)} then={<UnusedSize />} else={<RecoverableSize /> } /> ); }; @@ -209,15 +202,20 @@ const DeviceSizeColumn = ({ device }) => { const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noop }) => { const changeAction = (_, action) => onChange({ device: device.name, action }); + const value = (isDrive(device) && action === "resize") ? "keep" : action; + return ( <FormSelect - value={action} + value={value} isDisabled={isDisabled} onChange={changeAction} aria-label="Space action selector" > <FormSelectOption value="force_delete" label={_("Delete")} /> - <FormSelectOption value="resize" label={_("Allow resize")} /> + <If + condition={!isDrive(device)} + then={<FormSelectOption value="resize" label={_("Allow resize")} />} + /> <FormSelectOption value="keep" label={_("Do not modify")} /> </FormSelect> ); @@ -265,17 +263,19 @@ const DeviceRow = ({ const spaceAction = settings.spaceActions.find(a => a.device === device.name); const isDisabled = settings.spacePolicy !== "custom"; + const showAction = !device.partitionTable; return ( <TreeRowWrapper row={{ props: treeRow.props }}> <Td dataLabel={columnNames.device} treeRow={treeRow}> <DeviceDescriptionColumn device={device} /> </Td> - <Td dataLabel={columnNames.content} textCenter><DeviceContentColumn device={device} /></Td> - <Td dataLabel={columnNames.size} textCenter><DeviceSizeColumn device={device} /></Td> + <Td dataLabel={columnNames.content}><DeviceContentColumn device={device} /></Td> + <Td dataLabel={columnNames.size}><DeviceSizeColumn device={device} /></Td> + <Td dataLabel={columnNames.details}><DeviceDetailsColumn device={device} /></Td> <Td dataLabel={columnNames.action} textCenter> <If - condition={!isDrive(device)} + condition={showAction} then={ <DeviceActionColumn device={device} @@ -371,8 +371,9 @@ const SpaceActionsTable = ({ settings, onChange = noop }) => { <Thead> <Tr> <Th>{columnNames.device}</Th> - <Th textCenter>{columnNames.content}</Th> - <Th textCenter>{columnNames.size}</Th> + <Th>{columnNames.content}</Th> + <Th>{columnNames.size}</Th> + <Th>{columnNames.details}</Th> <Th textCenter>{columnNames.action}</Th> </Tr> </Thead> From c60afd2e1565878295738e07c3f7e04df57a1d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Fri, 16 Feb 2024 12:30:53 +0000 Subject: [PATCH 18/38] [web] Minor improvements in space policy section --- web/src/assets/styles/blocks.scss | 2 +- web/src/assets/styles/patternfly-overrides.scss | 1 - web/src/components/storage/ProposalSpacePolicySection.jsx | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index e30dd7e91f..b82d0b811b 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -444,7 +444,7 @@ ul[data-type="agama/list"][role="grid"] { &[aria-selected="true"] { background: var(--color-gray-light); - border-block-end-color: var(--color-primary); + border-color: var(--color-primary); } >:first-child { diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index 13f297b85a..90fa1bb699 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -253,7 +253,6 @@ ul { .pf-v5-c-table td > .pf-v5-c-form-control { inline-size: max-content; - margin: 0 auto; } .pf-v5-c-table tr[aria-level="1"] { diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index f278d24c31..3b7703714d 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -254,6 +254,7 @@ const DeviceRow = ({ rowIndex, props: { isExpanded, + isDetailsExpanded: true, isHidden, 'aria-level': level, 'aria-posinset': posInSet, @@ -273,7 +274,7 @@ const DeviceRow = ({ <Td dataLabel={columnNames.content}><DeviceContentColumn device={device} /></Td> <Td dataLabel={columnNames.size}><DeviceSizeColumn device={device} /></Td> <Td dataLabel={columnNames.details}><DeviceDetailsColumn device={device} /></Td> - <Td dataLabel={columnNames.action} textCenter> + <Td dataLabel={columnNames.action}> <If condition={showAction} then={ @@ -374,7 +375,7 @@ const SpaceActionsTable = ({ settings, onChange = noop }) => { <Th>{columnNames.content}</Th> <Th>{columnNames.size}</Th> <Th>{columnNames.details}</Th> - <Th textCenter>{columnNames.action}</Th> + <Th>{columnNames.action}</Th> </Tr> </Thead> <Tbody>{renderRows()}</Tbody> From 8c7612c3b56400ce00bfd71354d0bb4ca7ab38fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Fri, 16 Feb 2024 13:06:30 +0000 Subject: [PATCH 19/38] [service] Add Component D-Bus interface --- service/lib/agama/dbus/storage/device.rb | 11 ++- service/lib/agama/dbus/storage/interfaces.rb | 1 + .../dbus/storage/interfaces/component.rb | 89 +++++++++++++++++++ .../lib/agama/dbus/storage/interfaces/md.rb | 6 +- .../test/agama/dbus/storage/device_test.rb | 3 + .../storage/interfaces/component_examples.rb | 46 ++++++++++ .../dbus/storage/interfaces/md_examples.rb | 6 +- 7 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 service/lib/agama/dbus/storage/interfaces/component.rb create mode 100644 service/test/agama/dbus/storage/interfaces/component_examples.rb diff --git a/service/lib/agama/dbus/storage/device.rb b/service/lib/agama/dbus/storage/device.rb index c622e14eb8..d4367c0457 100644 --- a/service/lib/agama/dbus/storage/device.rb +++ b/service/lib/agama/dbus/storage/device.rb @@ -28,6 +28,7 @@ require "agama/dbus/storage/interfaces/block" require "agama/dbus/storage/interfaces/partition_table" require "agama/dbus/storage/interfaces/filesystem" +require "agama/dbus/storage/interfaces/component" module Agama module DBus @@ -87,6 +88,7 @@ def add_interfaces # rubocop:disable Metrics/CyclomaticComplexity, Metrics/Perce interfaces << Interfaces::Block if storage_device.is?(:blk_device) interfaces << Interfaces::PartitionTable if partition_table? interfaces << Interfaces::Filesystem if filesystem? + interfaces << Interfaces::Component if component? interfaces.each { |i| singleton_class.include(i) } end @@ -117,7 +119,14 @@ def partition_table? # # @return [Boolean] def filesystem? - !storage_device.filesystem.nil? + storage_device.is?(:blk_device) && !storage_device.filesystem.nil? + end + + # Whether the storage device is component of other devices. + # + # @return [Boolean] + def component? + storage_device.is?(:blk_device) && storage_device.component_of.any? end end end diff --git a/service/lib/agama/dbus/storage/interfaces.rb b/service/lib/agama/dbus/storage/interfaces.rb index 95ce2e08a2..7dac58df88 100644 --- a/service/lib/agama/dbus/storage/interfaces.rb +++ b/service/lib/agama/dbus/storage/interfaces.rb @@ -36,4 +36,5 @@ module Interfaces require "agama/dbus/storage/interfaces/block" require "agama/dbus/storage/interfaces/partition_table" require "agama/dbus/storage/interfaces/filesystem" +require "agama/dbus/storage/interfaces/component" require "agama/dbus/storage/interfaces/dasd_manager" diff --git a/service/lib/agama/dbus/storage/interfaces/component.rb b/service/lib/agama/dbus/storage/interfaces/component.rb new file mode 100644 index 0000000000..757784dd9b --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/component.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + # Interface for devices that are used as component of other device (e.g., physical volume, + # MD RAID device, etc). + # + # @note This interface is intended to be included by {Device} if needed. + module Component + COMPONENT_INTERFACE = "org.opensuse.Agama.Storage1.Component" + private_constant :COMPONENT_INTERFACE + + # Type of component. + # + # @return [String] Possible values: + # "physical_volume" + # "md_device" + # "raid_device" + # "multipath_wire" + # "bcache_device" + # "bcache_cset_device" + # "md_btrfs_device" + def component_type + types = { + lvm_vg: "physical_volume", + md: "md_device", + dm_raid: "raid_device", + multipath: "multipath_wire", + bcache: "bcache_device", + bcache_cset: "bcache_cset_device", + btrfs: "md_btrfs_device" + } + + device = storage_device.component_of.first + + types.find { |k, _v| device.is?(k) }&.last || "" + end + + # Name of the devices for which this devices is component of. + # + # @return [Array<String>] + def component_device_names + storage_device.component_of.map(&:display_name).compact + end + + # Paths of the D-Bus objects representing the devices. + # + # @return [Array<::DBus::ObjectPath>] + def component_devices + storage_device.component_of.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface COMPONENT_INTERFACE do + dbus_reader :component_type, "s", dbus_name: "Type" + dbus_reader :component_device_names, "as", dbus_name: "DeviceNames" + dbus_reader :component_devices, "ao", dbus_name: "Devices" + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/md.rb b/service/lib/agama/dbus/storage/interfaces/md.rb index 8aaf0f5f2a..8bc996c4b2 100644 --- a/service/lib/agama/dbus/storage/interfaces/md.rb +++ b/service/lib/agama/dbus/storage/interfaces/md.rb @@ -46,10 +46,10 @@ def md_level storage_device.md_level.to_s end - # Paths of the D-Bus objects representing the member devices of the MD RAID. + # Paths of the D-Bus objects representing the devices of the MD RAID. # # @return [Array<String>] - def md_members + def md_devices storage_device.plain_devices.map { |p| tree.path_for(p) } end @@ -58,7 +58,7 @@ def self.included(base) dbus_interface MD_INTERFACE do dbus_reader :md_uuid, "s", dbus_name: "UUID" dbus_reader :md_level, "s", dbus_name: "Level" - dbus_reader :md_members, "ao", dbus_name: "Members" + dbus_reader :md_devices, "ao", dbus_name: "Devices" end end end diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index 7b49f3d9c1..63cf459b0e 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -28,6 +28,7 @@ require_relative "./interfaces/md_examples" require_relative "./interfaces/partition_table_examples" require_relative "./interfaces/filesystem_examples" +require_relative "./interfaces/component_examples" require "agama/dbus/storage/device" require "agama/dbus/storage/devices_tree" require "dbus" @@ -145,6 +146,8 @@ include_examples "Filesystem interface" + include_examples "Component interface" + describe "#storage_device=" do before do allow(subject).to receive(:dbus_properties_changed) diff --git a/service/test/agama/dbus/storage/interfaces/component_examples.rb b/service/test/agama/dbus/storage/interfaces/component_examples.rb new file mode 100644 index 0000000000..60b7f4aad1 --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/component_examples.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../../../test_helper" + +shared_examples "Component interface" do + describe "Component D-Bus interface" do + let(:scenario) { "empty-dm_raids.xml" } + + let(:device) { devicegraph.find_by_name("/dev/sdb") } + + describe "#component_type" do + it "returns the type of component" do + expect(subject.component_type).to eq("raid_device") + end + end + + describe "#component_devices" do + it "returns the D-Bus path of the devices for which the device is component" do + raid1 = devicegraph.find_by_name("/dev/mapper/isw_ddgdcbibhd_test1") + raid2 = devicegraph.find_by_name("/dev/mapper/isw_ddgdcbibhd_test2") + + expect(subject.component_devices) + .to contain_exactly(tree.path_for(raid1), tree.path_for(raid2)) + end + end + end +end diff --git a/service/test/agama/dbus/storage/interfaces/md_examples.rb b/service/test/agama/dbus/storage/interfaces/md_examples.rb index 3a8b769c70..bc04dbc7c4 100644 --- a/service/test/agama/dbus/storage/interfaces/md_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/md_examples.rb @@ -45,12 +45,12 @@ end end - describe "#md_members" do - it "returns the D-Bus path of the MD members" do + describe "#md_devices" do + it "returns the D-Bus path of the MD components" do sda1 = devicegraph.find_by_name("/dev/sda1") sda2 = devicegraph.find_by_name("/dev/sda2") - expect(subject.md_members) + expect(subject.md_devices) .to contain_exactly(tree.path_for(sda1), tree.path_for(sda2)) end end From 41d2966e6aa94fe42a39bf7f71b910ee22aed291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Fri, 16 Feb 2024 14:15:39 +0000 Subject: [PATCH 20/38] [web] Adapt storage client to changes in D-Bus API --- web/src/client/storage.js | 36 ++- web/src/client/storage.test.js | 526 +++++++++++++++++++++++---------- 2 files changed, 387 insertions(+), 175 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index 3fe6a0383f..e84f62cbb2 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -117,11 +117,10 @@ class DevicesManager { * @property {string} [transport] * @property {boolean} [sdCard] * @property {boolean} [dellBOOS] - * @property {string[]} [devices] - RAID devices (only for "raid" type) + * @property {string[]} [devices] - RAID devices (only for "raid" and "md" types) * @property {string[]} [wires] - Multipath wires (only for "multipath" type) * @property {string} [level] - MD RAID level (only for "md" type) * @property {string} [uuid] - * @property {string[]} [members] - Member devices for a MD RAID (only for "md" type) * @property {boolean} [active] * @property {string} [name] - Block device name * @property {number} [size] @@ -155,18 +154,18 @@ class DevicesManager { }; const addRAIDProperties = (device, raidProperties) => { - device.devices = raidProperties.Devices.v; + device.devices = raidProperties.Devices.v.map(d => buildDevice(d, dbusDevices)); }; const addMultipathProperties = (device, multipathProperties) => { - device.wires = multipathProperties.Wires.v; + device.wires = multipathProperties.Wires.v.map(d => buildDevice(d, dbusDevices)); }; const addMDProperties = (device, mdProperties) => { device.type = "md"; device.level = mdProperties.Level.v; device.uuid = mdProperties.UUID.v; - device.members = mdProperties.Members.v; + device.devices = mdProperties.Devices.v.map(d => buildDevice(d, dbusDevices)); }; const addBlockProperties = (device, blockProperties) => { @@ -193,32 +192,45 @@ class DevicesManager { }; }; + const addComponentProperties = (device, componentProperties) => { + device.component = { + type: componentProperties.Type.v, + deviceNames: componentProperties.DeviceNames.v + }; + }; + const device = { sid: path.split("/").pop(), type: "" }; - const driveProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Drive"]; + const dbusDevice = dbusDevices[path]; + if (!dbusDevice) return device; + + const driveProperties = dbusDevice["org.opensuse.Agama.Storage1.Drive"]; if (driveProperties !== undefined) addDriveProperties(device, driveProperties); - const raidProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.RAID"]; + const raidProperties = dbusDevice["org.opensuse.Agama.Storage1.RAID"]; if (raidProperties !== undefined) addRAIDProperties(device, raidProperties); - const multipathProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Multipath"]; + const multipathProperties = dbusDevice["org.opensuse.Agama.Storage1.Multipath"]; if (multipathProperties !== undefined) addMultipathProperties(device, multipathProperties); - const mdProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.MD"]; + const mdProperties = dbusDevice["org.opensuse.Agama.Storage1.MD"]; if (mdProperties !== undefined) addMDProperties(device, mdProperties); - const blockProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Block"]; + const blockProperties = dbusDevice["org.opensuse.Agama.Storage1.Block"]; if (blockProperties !== undefined) addBlockProperties(device, blockProperties); - const ptableProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.PartitionTable"]; + const ptableProperties = dbusDevice["org.opensuse.Agama.Storage1.PartitionTable"]; if (ptableProperties !== undefined) addPtableProperties(device, ptableProperties); - const filesystemProperties = dbusDevices[path]["org.opensuse.Agama.Storage1.Filesystem"]; + const filesystemProperties = dbusDevice["org.opensuse.Agama.Storage1.Filesystem"]; if (filesystemProperties !== undefined) addFilesystemProperties(device, filesystemProperties); + const componentProperties = dbusDevice["org.opensuse.Agama.Storage1.Component"]; + if (componentProperties !== undefined) addComponentProperties(device, componentProperties); + return device; }; diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 2f4416f94c..c16b80dd7c 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -33,154 +33,252 @@ const cockpitCallbacks = {}; let managedObjects = {}; +// Define devices + +const sda = { + sid: "59", + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + dellBOSS: false, + sdCard: true, + active: true, + name: "/dev/sda", + size: 1024, + recoverableSize: 0, + systems : [], + udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], +}; + const sda1 = { - sid: "66", + sid: "60", type: "", active: true, name: "/dev/sda1", size: 512, recoverableSize: 128, - systems : ["Windows"], + systems : [], udevIds: [], - udevPaths: [], - filesystem: { - type: "ntfs", - isEFI: false - } + udevPaths: [] }; const sda2 = { - sid: "67", + sid: "61", type: "", active: true, name: "/dev/sda2", size: 512, recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sdb = { + sid: "62", + type: "disk", + vendor: "Samsung", + model: "Samsung Evo 8 Pro", + driver: ["ahci"], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdb", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: ["pci-0000:00-19"] +}; + +const sdc = { + sid: "63", + type: "disk", + vendor: "Disk", + model: "", + driver: [], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdc", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sdd = { + sid: "64", + type: "disk", + vendor: "Disk", + model: "", + driver: [], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdd", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sde = { + sid: "65", + type: "disk", + vendor: "Disk", + model: "", + driver: [], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sde", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const md0 = { + sid: "66", + type: "md", + level: "raid0", + uuid: "12345:abcde", + active: true, + name: "/dev/md0", + size: 2048, + recoverableSize: 0, systems : ["openSUSE Leap 15.2"], udevIds: [], + udevPaths: [], + filesystem: { type: "ext4", isEFI: false } +}; + +const raid = { + sid: "67", + type: "raid", + vendor: "Dell", + model: "Dell BOSS-N1 Modular", + driver: [], + bus: "", + busId: "", + transport: "", + dellBOSS: true, + sdCard: false, + active: true, + name: "/dev/mapper/isw_ddgdcbibhd_244", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], udevPaths: [] }; -const systemDevices = { - sda: { - sid: "59", - type: "disk", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, - name: "/dev/sda", - size: 1024, - recoverableSize: 0, - systems : ["Windows", "openSUSE Leap 15.2"], - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], - partitionTable: { - type: "gpt", - partitions: [sda1, sda2] - } - }, - sdb: { - sid: "60", - type: "disk", - vendor: "Samsung", - model: "Samsung Evo 8 Pro", - driver: ["ahci"], - bus: "IDE", - busId: "", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/sdb", - size: 2048, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: ["pci-0000:00-19"] - }, - md0: { - sid: "62", - type: "md", - level: "raid0", - uuid: "12345:abcde", - members: ["/dev/sdb"], - active: true, - name: "/dev/md0", - size: 2048, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: [] - }, - raid: { - sid: "63", - type: "raid", - devices: ["/dev/sda", "/dev/sdb"], - vendor: "Dell", - model: "Dell BOSS-N1 Modular", - driver: [], - bus: "", - busId: "", - transport: "", - dellBOSS: true, - sdCard: false, - active: true, - name: "/dev/mapper/isw_ddgdcbibhd_244", - size: 2048, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: [] - }, - multipath: { - sid: "64", - type: "multipath", - wires: ["/dev/sdc", "/dev/sdd"], - vendor: "", - model: "", - driver: [], - bus: "", - busId: "", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/mapper/36005076305ffc73a00000000000013b4", - size: 2048, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: [] - }, - dasd: { - sid: "65", - type: "dasd", - vendor: "IBM", - model: "IBM", - driver: [], - bus: "", - busId: "0.0.0150", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/dasda", - size: 2048, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: [] - }, - sda1, - sda2 +const multipath = { + sid: "68", + type: "multipath", + vendor: "", + model: "", + driver: [], + bus: "", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/mapper/36005076305ffc73a00000000000013b4", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const dasd = { + sid: "69", + type: "dasd", + vendor: "IBM", + model: "IBM", + driver: [], + bus: "", + busId: "0.0.0150", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/dasda", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +// Define relationship between devices + +sda.partitionTable = { + type: "gpt", + partitions: [sda1, sda2] +}; + +sda1.component = { + type: "md_device", + deviceNames: ["/dev/md0"] +}; + +sda2.component = { + type: "md_device", + deviceNames: ["/dev/md0"] +}; + +sdb.component = { + type: "raid_device", + deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"] +}; + +sdc.component = { + type: "raid_device", + deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"] +}; + +sdd.component = { + type: "multipath_wire", + deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"] +}; + +sde.component = { + type: "multipath_wire", + deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"] }; +md0.devices = [sda1, sda2]; + +raid.devices = [sdb, sdc]; + +multipath.wires = [sdd, sde]; + +const systemDevices = { sda, sda1, sda2, sdb, sdc, sdd, sde, md0, raid, multipath, dasd }; + const contexts = { withoutProposal: () => { cockpitProxies.proposal = null; @@ -256,7 +354,7 @@ const contexts = { withAvailableDevices: () => { cockpitProxies.proposalCalculator.AvailableDevices = [ "/org/opensuse/Agama/Storage1/system/59", - "/org/opensuse/Agama/Storage1/system/60" + "/org/opensuse/Agama/Storage1/system/62" ]; }, withoutIssues: () => { @@ -382,7 +480,7 @@ const contexts = { Name: { t: "s", v: "/dev/sda" }, Size: { t: "x", v: 1024 }, RecoverableSize: { t: "x", v: 0 }, - Systems: { t: "as", v: ["Windows", "openSUSE Leap 15.2"] }, + Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"] }, UdevPaths: { t: "as", v: ["pci-0000:00-12", "pci-0000:00-12-ata"] } }, @@ -390,11 +488,43 @@ const contexts = { Type: { t: "s", v: "gpt" }, Partitions: { t: "as", - v: ["/org/opensuse/Agama/Storage1/system/66", "/org/opensuse/Agama/Storage1/system/67"] + v: ["/org/opensuse/Agama/Storage1/system/60", "/org/opensuse/Agama/Storage1/system/61"] } } }; managedObjects["/org/opensuse/Agama/Storage1/system/60"] = { + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sda1" }, + Size: { t: "x", v: 512 }, + RecoverableSize: { t: "x", v: 128 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "md_device" }, + DeviceNames: { t: "as", v: ["/dev/md0"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/66"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/61"] = { + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sda2" }, + Size: { t: "x", v: 512 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "md_device" }, + DeviceNames: { t: "as", v: ["/dev/md0"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/66"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/62"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "disk" }, Vendor: { t: "s", v: "Samsung" }, @@ -413,25 +543,115 @@ const contexts = { Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: ["pci-0000:00-19"] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "raid_device" }, + DeviceNames: { t: "as", v: ["/dev/mapper/isw_ddgdcbibhd_244"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/67"] } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/62"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/63"] = { + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Disk" }, + Model: { t: "s", v: "" }, + Driver: { t: "as", v: [] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sdc" }, + Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "raid_device" }, + DeviceNames: { t: "as", v: ["/dev/mapper/isw_ddgdcbibhd_244"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/67"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/64"] = { + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Disk" }, + Model: { t: "s", v: "" }, + Driver: { t: "as", v: [] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sdd" }, + Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "multipath_wire" }, + DeviceNames: { t: "as", v: ["/dev/mapper/36005076305ffc73a00000000000013b4"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/68"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/65"] = { + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Disk" }, + Model: { t: "s", v: "" }, + Driver: { t: "as", v: [] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sde" }, + Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "multipath_wire" }, + DeviceNames: { t: "as", v: ["/dev/mapper/36005076305ffc73a00000000000013b4"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/68"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/66"] = { "org.opensuse.Agama.Storage1.MD": { Level: { t: "s", v: "raid0" }, UUID: { t: "s", v: "12345:abcde" }, - Members: { t: "as", v: ["/dev/sdb"] } + Devices: { + t: "ao", + v: ["/org/opensuse/Agama/Storage1/system/60", "/org/opensuse/Agama/Storage1/system/61"] + } }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/md0" }, Size: { t: "x", v: 2048 }, RecoverableSize: { t: "x", v: 0 }, - Systems: { t: "as", v: [] }, + Systems: { t: "as", v: ["openSUSE Leap 15.2"] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Filesystem": { + Type: { t: "s", v: "ext4" }, + EFI: { t: "b", v: false } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/63"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/67"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "raid" }, Vendor: { t: "s", v: "Dell" }, @@ -443,7 +663,10 @@ const contexts = { Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: true }, SDCard: { t: "b", v: false } } }, }, "org.opensuse.Agama.Storage1.RAID" : { - Devices: { t: "as", v: ["/dev/sda", "/dev/sdb"] } + Devices: { + t: "ao", + v: ["/org/opensuse/Agama/Storage1/system/62", "/org/opensuse/Agama/Storage1/system/63"] + } }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, @@ -455,7 +678,7 @@ const contexts = { UdevPaths: { t: "as", v: [] } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/64"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/68"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "multipath" }, Vendor: { t: "s", v: "" }, @@ -467,7 +690,10 @@ const contexts = { Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, }, "org.opensuse.Agama.Storage1.Multipath" : { - Wires: { t: "as", v: ["/dev/sdc", "/dev/sdd"] } + Wires: { + t: "ao", + v: ["/org/opensuse/Agama/Storage1/system/64", "/org/opensuse/Agama/Storage1/system/65"] + } }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, @@ -479,7 +705,7 @@ const contexts = { UdevPaths: { t: "as", v: [] } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/65"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/69"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "dasd" }, Vendor: { t: "s", v: "IBM" }, @@ -500,32 +726,6 @@ const contexts = { UdevPaths: { t: "as", v: [] } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/66"] = { - "org.opensuse.Agama.Storage1.Block": { - Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sda1" }, - Size: { t: "x", v: 512 }, - RecoverableSize: { t: "x", v: 128 }, - Systems: { t: "as", v: ["Windows"] }, - UdevIds: { t: "as", v: [] }, - UdevPaths: { t: "as", v: [] } - }, - "org.opensuse.Agama.Storage1.Filesystem": { - Type: { t: "s", v: "ntfs" }, - EFI: { t: "b", v: false } - } - }; - managedObjects["/org/opensuse/Agama/Storage1/system/67"] = { - "org.opensuse.Agama.Storage1.Block": { - Active: { t: "b", v: true }, - Name: { t: "s", v: "/dev/sda2" }, - Size: { t: "x", v: 512 }, - RecoverableSize: { t: "x", v: 0 }, - Systems: { t: "as", v: ["openSUSE Leap 15.2"] }, - UdevIds: { t: "as", v: [] }, - UdevPaths: { t: "as", v: [] } - } - }; } }; From 8e51494fa12cb2e1c0800131f3e0f0efc6352e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= <dgonzalez@suse.de> Date: Sun, 18 Feb 2024 00:07:16 +0000 Subject: [PATCH 21/38] [web] Add tests for ProposalSpacePolicySection --- .../storage/ProposalSettingsSection.test.jsx | 2 +- .../storage/ProposalSpacePolicySection.jsx | 5 +- .../ProposalSpacePolicySection.test.jsx | 263 ++++++++++++++++++ 3 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 web/src/components/storage/ProposalSpacePolicySection.test.jsx diff --git a/web/src/components/storage/ProposalSettingsSection.test.jsx b/web/src/components/storage/ProposalSettingsSection.test.jsx index 1f55fe94cf..a3687f6047 100644 --- a/web/src/components/storage/ProposalSettingsSection.test.jsx +++ b/web/src/components/storage/ProposalSettingsSection.test.jsx @@ -20,7 +20,7 @@ */ import React from "react"; -import { screen, waitFor, within } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; import { ProposalSettingsSection } from "~/components/storage"; diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 3b7703714d..d75be84627 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -209,7 +209,7 @@ const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noo value={value} isDisabled={isDisabled} onChange={changeAction} - aria-label="Space action selector" + aria-label={sprintf(_("Space action selector for %s"), device.name)} > <FormSelectOption value="force_delete" label={_("Delete")} /> <If @@ -436,8 +436,7 @@ export default function ProposalSpacePolicySection({ const currentPolicy = SPACE_POLICIES.find(p => p.name === settings.spacePolicy) || SPACE_POLICIES[0]; return ( - <Section title={_("Find Space")} className="flex-stack"> - + <Section title={_("Find Space")}> <If condition={isLoading && settings.spacePolicy === undefined} then={<SectionSkeleton numRows={4} />} diff --git a/web/src/components/storage/ProposalSpacePolicySection.test.jsx b/web/src/components/storage/ProposalSpacePolicySection.test.jsx new file mode 100644 index 0000000000..e8a2585c93 --- /dev/null +++ b/web/src/components/storage/ProposalSpacePolicySection.test.jsx @@ -0,0 +1,263 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, contact SUSE LLC. + * + * To contact SUSE LLC about this file by physical or electronic mail, you may + * find current contact information at www.suse.com. + */ + +import React from "react"; +import { screen, within } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import { ProposalSpacePolicySection } from "~/components/storage"; + +const sda = { + sid: "59", + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + dellBOSS: false, + sdCard: true, + active: true, + name: "/dev/sda", + size: 1024, + recoverableSize: 0, + systems : [], + udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], +}; + +const sda1 = { + sid: "60", + type: "", + active: true, + name: "/dev/sda1", + size: 512, + recoverableSize: 128, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sda2 = { + sid: "61", + type: "", + active: true, + name: "/dev/sda2", + size: 512, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +sda.partitionTable = { + type: "gpt", + partitions: [sda1, sda2] +}; + +const sdb = { + sid: "62", + type: "disk", + vendor: "Samsung", + model: "Samsung Evo 8 Pro", + driver: ["ahci"], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdb", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: ["pci-0000:00-19"] +}; + +let settings; + +beforeEach(() => { + settings = { + installationDevices: [sda, sdb], + spacePolicy: "keep", + spaceActions: [ + { device: "/dev/sda1", action: "force_delete" }, + { device: "/dev/sda2", action: "resize" } + ], + }; + + // Reset localStorage to avoid keeping the SpaceActionsTable state between + // examples. + window.localStorage.clear(); +}); + +describe("ProposalSpacePolicySection", () => { + it("renders the space policy picker", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + const picker = screen.getByRole("listbox"); + within(picker).getByRole("option", { name: /delete/i }); + within(picker).getByRole("option", { name: /resize/i }); + within(picker).getByRole("option", { name: /available/i }); + within(picker).getByRole("option", { name: /custom/i }); + }); + + it("triggers the onChange callback when user changes selected space policy", async () => { + const onChangeFn = jest.fn(); + const { user } = plainRender(<ProposalSpacePolicySection settings={settings} onChange={onChangeFn} />); + const picker = screen.getByRole("listbox"); + await user.selectOptions( + picker, + within(picker).getByRole("option", { name: /custom/i }) + ); + expect(onChangeFn).toHaveBeenCalledWith({ spacePolicy: "custom" }); + }); + + describe("when there are no installation devices", () => { + beforeEach(() => { + settings.installationDevices = []; + }); + + it("does not render the policy actions", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + const actions = screen.queryByRole("treegrid", { name: "Actions to find space" }); + expect(actions).toBeNull(); + }); + }); + + describe("when there are installation devices", () => { + it("renders the policy actions", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + screen.getByRole("treegrid", { name: "Actions to find space" }); + }); + }); + + describe.each([ + { id: 'delete', nameRegexp: /delete/i }, + { id: 'resize', nameRegexp: /shrink/i }, + { id: 'keep', nameRegexp: /not be modified/i } + ])("when space policy is '$id'", ({ id, nameRegexp }) => { + beforeEach(() => { + settings.spacePolicy = id; + }); + + it("only renders '$id' option as selected", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + const picker = screen.getByRole("listbox"); + within(picker).getByRole("option", { name: nameRegexp, selected: true }); + expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); + }); + + it("does not allow to modify the space actions", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + // NOTE: HTML `disabled` attribute removes the element from the a11y tree. + // That's why the test is using `hidden: true` here to look for disabled actions. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled + // https://testing-library.com/docs/queries/byrole/#hidden + // TODO: use a more inclusive way to disable the actions. + // https://css-tricks.com/making-disabled-buttons-more-inclusive/ + const spaceActions = screen.getAllByRole("combobox", { name: /Space action selector/, hidden: true }); + expect(spaceActions.length).toEqual(3); + }); + }); + + describe("when space policy is 'custom'", () => { + beforeEach(() => { + settings.spacePolicy = "custom"; + }); + + it("only renders 'custom' option as selected", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + const picker = screen.getByRole("listbox"); + within(picker).getByRole("option", { name: /custom/i, selected: true }); + expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); + }); + + it("allows to modify the space actions", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + const spaceActions = screen.getAllByRole("combobox", { name: /Space action selector/ }); + expect(spaceActions.length).toEqual(3); + }); + }); + + describe("DeviceActionColumn", () => { + it("renders the space actions selector for devices without partition table", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + const sdaRow = screen.getByRole("row", { name: /sda/ }); + const sdaActionsSelector = within(sdaRow).queryByRole("combobox", { name: "Space action selector for /dev/sda" }); + // sda has partition table, the selector shouldn't be found + expect(sdaActionsSelector).toBeNull(); + const sdbRow = screen.getByRole("row", { name: /sdb/ }); + // sdb does not have partition table, selector should be there + within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); + }); + + it("does not renders the 'resize' option for drives", () => { + plainRender(<ProposalSpacePolicySection settings={settings} />); + const sdbRow = screen.getByRole("row", { name: /sdb/ }); + const spaceActionsSelector = within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); + const resizeOption = within(spaceActionsSelector).queryByRole("option", { name: /resize/ }); + expect(resizeOption).toBeNull(); + }); + + it("renders the 'resize' option for devices other than drives", async () => { + const { user } = plainRender(<ProposalSpacePolicySection settings={settings} />); + const sdaRow = screen.getByRole("row", { name: /sda/ }); + const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); + await user.click(sdaToggler); + const sda1Row = screen.getByRole("row", { name: /sda1/ }); + const spaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); + within(spaceActionsSelector).getByRole("option", { name: /resize/ }); + }); + + it("renders as selected the option matching the given device space action", async () => { + const { user } = plainRender(<ProposalSpacePolicySection settings={settings} />); + const sdaRow = screen.getByRole("row", { name: /sda/ }); + const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); + await user.click(sdaToggler); + const sda1Row = screen.getByRole("row", { name: /sda1/ }); + const sda1SpaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); + within(sda1SpaceActionsSelector).getByRole("option", { name: /delete/i, selected: true }); + const sda2Row = screen.getByRole("row", { name: /sda2/ }); + const sda2SpaceActionsSelector = within(sda2Row).getByRole("combobox", { name: "Space action selector for /dev/sda2" }); + within(sda2SpaceActionsSelector).getByRole("option", { name: /resize/i, selected: true }); + }); + + it("triggers the onChange callback when user changes space action", async () => { + const onChangeFn = jest.fn(); + const { user } = plainRender( + <ProposalSpacePolicySection settings={{ ...settings, spacePolicy: "custom" }} onChange={onChangeFn} /> + ); + + const sda1Row = screen.getByRole("row", { name: /sda1/ }); + const selector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); + await user.selectOptions( + selector, + within(selector).getByRole("option", { name: /resize/i, selected: false }) + ); + expect(onChangeFn).toHaveBeenCalledWith( + expect.objectContaining({ + spaceActions: expect.arrayContaining([{ action: "resize", device: "/dev/sda1" }]) + }) + ); + }); + }); +}); From fc908acbc8ffafaa75e3507066b608c9b796cefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Mon, 19 Feb 2024 15:12:13 +0000 Subject: [PATCH 22/38] [web] Reduce text - Remove sentence at the beginning of the Find Space section. - Reduce text of keep policy. --- web/src/components/storage/ProposalSpacePolicySection.jsx | 5 +---- .../components/storage/ProposalSpacePolicySection.test.jsx | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index d75be84627..30eec0e1ba 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -58,7 +58,7 @@ const SPACE_POLICIES = [ { name: "keep", label: N_("Use available space"), - description: N_("Existing partitions and data will not be modified. Only the space not assigned to any partition will be used.") + description: N_("The data is kept. Only the space not assigned to any partition will be used.") }, { name: "custom", @@ -442,9 +442,6 @@ export default function ProposalSpacePolicySection({ then={<SectionSkeleton numRows={4} />} else={ <> - <p> - {_("Indicate how to make free space in the selected disks for allocating the file systems:")} - </p> <SpacePolicyPicker currentPolicy={currentPolicy} onChange={changeSpacePolicy} /> <If condition={settings.installationDevices?.length > 0} diff --git a/web/src/components/storage/ProposalSpacePolicySection.test.jsx b/web/src/components/storage/ProposalSpacePolicySection.test.jsx index e8a2585c93..34338c94cf 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.test.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.test.jsx @@ -153,7 +153,7 @@ describe("ProposalSpacePolicySection", () => { describe.each([ { id: 'delete', nameRegexp: /delete/i }, { id: 'resize', nameRegexp: /shrink/i }, - { id: 'keep', nameRegexp: /not be modified/i } + { id: 'keep', nameRegexp: /the space not assigned/i } ])("when space policy is '$id'", ({ id, nameRegexp }) => { beforeEach(() => { settings.spacePolicy = id; From 89d4130c77f2488011f206c2009080fa515d79b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Mon, 19 Feb 2024 16:01:37 +0000 Subject: [PATCH 23/38] [web] Add properties to storage objects - Add #isDrive and #unpartitionedSize. --- web/src/client/storage.js | 8 +++++++- web/src/client/storage.test.js | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/web/src/client/storage.js b/web/src/client/storage.js index e84f62cbb2..66a92956bf 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -108,6 +108,7 @@ class DevicesManager { * * @typedef {object} StorageDevice * @property {string} sid - Internal id that is used as D-Bus object basename + * @property {boolean} isDrive - Whether the device is a drive * @property {string} type - Type of device ("disk", "raid", "multipath", "dasd", "md") * @property {string} [vendor] * @property {string} [model] @@ -134,6 +135,7 @@ class DevicesManager { * @typedef {object} PartitionTable * @property {string} type * @property {StorageDevice[]} partitions + * @property {number} unpartitionedSize - Total size not assigned to any partition * * @typedef {object} Filesystem * @property {string} type @@ -142,6 +144,7 @@ class DevicesManager { async getDevices() { const buildDevice = (path, dbusDevices) => { const addDriveProperties = (device, dbusProperties) => { + device.isDrive = true; device.type = dbusProperties.Type.v; device.vendor = dbusProperties.Vendor.v; device.model = dbusProperties.Model.v; @@ -179,9 +182,11 @@ class DevicesManager { }; const addPtableProperties = (device, ptableProperties) => { + const partitions = ptableProperties.Partitions.v.map(p => buildDevice(p, dbusDevices)); device.partitionTable = { type: ptableProperties.Type.v, - partitions: ptableProperties.Partitions.v.map(p => buildDevice(p, dbusDevices)) + partitions, + unpartitionedSize: device.size - partitions.reduce((s, p) => s + p.size, 0) }; }; @@ -201,6 +206,7 @@ class DevicesManager { const device = { sid: path.split("/").pop(), + isDrive: false, type: "" }; diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index c16b80dd7c..73610f9a1b 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -37,6 +37,7 @@ let managedObjects = {}; const sda = { sid: "59", + isDrive: true, type: "disk", vendor: "Micron", model: "Micron 1100 SATA", @@ -57,6 +58,7 @@ const sda = { const sda1 = { sid: "60", + isDrive: false, type: "", active: true, name: "/dev/sda1", @@ -69,10 +71,11 @@ const sda1 = { const sda2 = { sid: "61", + isDrive: false, type: "", active: true, name: "/dev/sda2", - size: 512, + size: 256, recoverableSize: 0, systems : [], udevIds: [], @@ -81,6 +84,7 @@ const sda2 = { const sdb = { sid: "62", + isDrive: true, type: "disk", vendor: "Samsung", model: "Samsung Evo 8 Pro", @@ -101,6 +105,7 @@ const sdb = { const sdc = { sid: "63", + isDrive: true, type: "disk", vendor: "Disk", model: "", @@ -121,6 +126,7 @@ const sdc = { const sdd = { sid: "64", + isDrive: true, type: "disk", vendor: "Disk", model: "", @@ -141,6 +147,7 @@ const sdd = { const sde = { sid: "65", + isDrive: true, type: "disk", vendor: "Disk", model: "", @@ -161,6 +168,7 @@ const sde = { const md0 = { sid: "66", + isDrive: false, type: "md", level: "raid0", uuid: "12345:abcde", @@ -176,6 +184,7 @@ const md0 = { const raid = { sid: "67", + isDrive: true, type: "raid", vendor: "Dell", model: "Dell BOSS-N1 Modular", @@ -196,6 +205,7 @@ const raid = { const multipath = { sid: "68", + isDrive: true, type: "multipath", vendor: "", model: "", @@ -216,6 +226,7 @@ const multipath = { const dasd = { sid: "69", + isDrive: true, type: "dasd", vendor: "IBM", model: "IBM", @@ -238,7 +249,8 @@ const dasd = { sda.partitionTable = { type: "gpt", - partitions: [sda1, sda2] + partitions: [sda1, sda2], + unpartitionedSize: 256 }; sda1.component = { @@ -512,7 +524,7 @@ const contexts = { "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/sda2" }, - Size: { t: "x", v: 512 }, + Size: { t: "x", v: 256 }, RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, From f1616ebfb1dd033cfb4e04bf68d32db989ebb9b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Mon, 19 Feb 2024 16:05:09 +0000 Subject: [PATCH 24/38] [web] Get #isDrive and #upartitionedSize from object --- .../storage/ProposalSpacePolicySection.jsx | 21 +++++-------------- .../ProposalSpacePolicySection.test.jsx | 7 ++++++- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 30eec0e1ba..b11c2925a4 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -76,15 +76,6 @@ const columnNames = { action: N_("Action") }; -/** - * Indicates whether a device is a drive (disk, RAID). - * @function - * - * @param {StorageDevice} device - * @returns {boolean} - */ -const isDrive = (device) => Object.keys(device).includes("vendor"); - /** * Column content with the description of a device. * @component @@ -97,7 +88,7 @@ const DeviceDescriptionColumn = ({ device }) => { <> <div>{device.name}</div> <If - condition={isDrive(device)} + condition={device.isDrive} then={<div className="fs-small">{`${device.vendor} ${device.model}`}</div>} /> </> @@ -159,11 +150,9 @@ const DeviceSizeColumn = ({ device }) => { const DeviceDetailsColumn = ({ device }) => { const UnusedSize = () => { - const partitioned = device.partitionTable?.partitions.reduce((s, p) => s + p.size, 0) || 0; - if (device.filesystem) return null; - const unused = device.size - partitioned; + const unused = device.partitionTable?.unpartitionedSize || 0; return ( <div> @@ -185,7 +174,7 @@ const DeviceDetailsColumn = ({ device }) => { }; return ( - <If condition={isDrive(device)} then={<UnusedSize />} else={<RecoverableSize /> } /> + <If condition={device.isDrive} then={<UnusedSize />} else={<RecoverableSize /> } /> ); }; @@ -202,7 +191,7 @@ const DeviceDetailsColumn = ({ device }) => { const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noop }) => { const changeAction = (_, action) => onChange({ device: device.name, action }); - const value = (isDrive(device) && action === "resize") ? "keep" : action; + const value = (device.isDrive && action === "resize") ? "keep" : action; return ( <FormSelect @@ -213,7 +202,7 @@ const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noo > <FormSelectOption value="force_delete" label={_("Delete")} /> <If - condition={!isDrive(device)} + condition={!device.isDrive} then={<FormSelectOption value="resize" label={_("Allow resize")} />} /> <FormSelectOption value="keep" label={_("Do not modify")} /> diff --git a/web/src/components/storage/ProposalSpacePolicySection.test.jsx b/web/src/components/storage/ProposalSpacePolicySection.test.jsx index 34338c94cf..1f42884d73 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.test.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.test.jsx @@ -26,6 +26,7 @@ import { ProposalSpacePolicySection } from "~/components/storage"; const sda = { sid: "59", + isDrive: true, type: "disk", vendor: "Micron", model: "Micron 1100 SATA", @@ -46,6 +47,7 @@ const sda = { const sda1 = { sid: "60", + isDrive: false, type: "", active: true, name: "/dev/sda1", @@ -58,6 +60,7 @@ const sda1 = { const sda2 = { sid: "61", + isDrive: false, type: "", active: true, name: "/dev/sda2", @@ -70,11 +73,13 @@ const sda2 = { sda.partitionTable = { type: "gpt", - partitions: [sda1, sda2] + partitions: [sda1, sda2], + unpartitionedSize: 512 }; const sdb = { sid: "62", + isDrive: true, type: "disk", vendor: "Samsung", model: "Samsung Evo 8 Pro", From 43fff645fb40eab94b53488df6aad0f0ff6b89a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Mon, 19 Feb 2024 16:06:06 +0000 Subject: [PATCH 25/38] [web] Use better names for attributes and methods --- .../storage/ProposalSpacePolicySection.jsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index b11c2925a4..297f64a6a7 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -38,7 +38,7 @@ import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/rea /** * @typedef SpacePolicy * @type {object} - * @property {string} name + * @property {string} id * @property {string} label * @property {string} description */ @@ -46,22 +46,22 @@ import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/rea /** @type {SpacePolicy[]} */ const SPACE_POLICIES = [ { - name: "delete", + id: "delete", label: N_("Delete current content"), description: N_("All partitions will be removed and any data in the disks will be lost.") }, { - name: "resize", + id: "resize", label: N_("Shrink existing partitions"), description: N_("The data is kept, but the current partitions will be resized as needed.") }, { - name: "keep", + id: "keep", label: N_("Use available space"), description: N_("The data is kept. Only the space not assigned to any partition will be used.") }, { - name: "custom", + id: "custom", label: N_("Custom"), description: N_("Select what to do with each partition.") } @@ -112,7 +112,7 @@ const DeviceContentColumn = ({ device }) => { }; const BlockContent = () => { - const content = () => { + const renderContent = () => { const systems = device.systems; if (systems.length > 0) return systems.join(", "); @@ -131,7 +131,7 @@ const DeviceContentColumn = ({ device }) => { } }; - return <div>{content()}</div>; + return <div>{renderContent()}</div>; }; return (device.partitionTable ? <PartitionTableContent /> : <BlockContent />); @@ -386,11 +386,11 @@ const SpacePolicyPicker = ({ currentPolicy, onChange = noop }) => { {SPACE_POLICIES.map((policy) => { return ( <OptionsPicker.Option - key={policy.name} + key={policy.id} title={policy.label} body={policy.description} - onClick={() => onChange(policy.name)} - isSelected={currentPolicy?.name === policy.name} + onClick={() => onChange(policy.id)} + isSelected={currentPolicy?.id === policy.id} /> ); })} @@ -422,7 +422,7 @@ export default function ProposalSpacePolicySection({ onChange({ spaceActions }); }; - const currentPolicy = SPACE_POLICIES.find(p => p.name === settings.spacePolicy) || SPACE_POLICIES[0]; + const currentPolicy = SPACE_POLICIES.find(p => p.id === settings.spacePolicy) || SPACE_POLICIES[0]; return ( <Section title={_("Find Space")}> From 7e354a962a93f1c0b5bc20584005957ca6dd649a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Mon, 19 Feb 2024 16:10:42 +0000 Subject: [PATCH 26/38] [web] Cspell --- web/cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/web/cspell.json b/web/cspell.json index adf47ce25f..86eef9bb7f 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -75,6 +75,7 @@ "textinput", "tkip", "udev", + "unpartitioned", "wwpn", "xxxs", "zfcp" From 7841cb42f5e21f3984829cbe47b5ccce546855e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Mon, 19 Feb 2024 16:13:04 +0000 Subject: [PATCH 27/38] [service] Typo --- service/lib/agama/dbus/storage/interfaces/component.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/lib/agama/dbus/storage/interfaces/component.rb b/service/lib/agama/dbus/storage/interfaces/component.rb index 757784dd9b..9a9a7ba9e4 100644 --- a/service/lib/agama/dbus/storage/interfaces/component.rb +++ b/service/lib/agama/dbus/storage/interfaces/component.rb @@ -59,7 +59,7 @@ def component_type types.find { |k, _v| device.is?(k) }&.last || "" end - # Name of the devices for which this devices is component of. + # Name of the devices for which this device is component of. # # @return [Array<String>] def component_device_names From d73aeb7ff4b3d24ec6080b7e6e049321e3c944a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Mon, 19 Feb 2024 17:02:32 +0000 Subject: [PATCH 28/38] [web] Increase margin of sections --- web/src/assets/styles/blocks.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index b82d0b811b..a6d42c2e68 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -15,7 +15,7 @@ margin-inline-end: var(--section-icon-size); &:not(:last-child) { - margin-block-end: var(--spacer-normal); + margin-block-end: var(--spacer-medium); } > h2 { From 9485f351fda97c64c93c8dcb8e96398f70ee7119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= <dgonzalez@suse.de> Date: Tue, 20 Feb 2024 09:25:15 +0000 Subject: [PATCH 29/38] [web] Allow checking if a value is an object --- web/src/utils.js | 20 +++++++++++++++++++ web/src/utils.test.js | 45 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/web/src/utils.js b/web/src/utils.js index 1260cb6c83..5969268cf1 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -21,6 +21,25 @@ import { useEffect, useRef, useCallback, useState } from "react"; +/** + * Returns true when given value is an + * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object Object} + * + * Borrowed from https://dev.to/alesm0101/how-to-check-if-a-value-is-an-object-in-javascript-3pin + * + * @param {any} value - the value to be checked + * @return {boolean} true when given value is an object; false otherwise + */ +const isObject = (value) => ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + !(value instanceof RegExp) && + !(value instanceof Date) && + !(value instanceof Set) && + !(value instanceof Map) +); + /** * Returns an empty function useful to be used as a default callback. * @@ -334,6 +353,7 @@ const timezoneUTCOffset = (timezone) => { export { noop, + isObject, partition, classNames, useCancellablePromise, diff --git a/web/src/utils.test.js b/web/src/utils.test.js index 545b87eb2c..8be48355cb 100644 --- a/web/src/utils.test.js +++ b/web/src/utils.test.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -21,7 +21,7 @@ import { classNames, partition, noop, toValidationError, - localConnection, remoteConnection + localConnection, remoteConnection, isObject } from "./utils"; describe("noop", () => { @@ -108,3 +108,44 @@ describe("remoteConnection", () => { }); }); }); + +describe("isObject", () => { + it("returns true when called with an object", () => { + expect(isObject({ dummy: "object" })).toBe(true); + }); + + it("returns false when called with null", () => { + expect(isObject(null)).toBe(false); + }); + + it("returns false when called with undefined", () => { + expect(isObject()).toBe(false); + }); + + it("returns false when called with a string", () => { + expect(isObject("dummy string")).toBe(false); + }); + + it("returns false when called with an array", () => { + expect(isObject(["dummy", "array"])).toBe(false); + }); + + it("returns false when called with a date", () => { + expect(isObject(new Date())).toBe(false); + }); + + it("returns false when called with regexp", () => { + expect(isObject(/aRegExp/i)).toBe(false); + }); + + it("returns false when called with a set", () => { + expect(isObject(new Set(["dummy", "set"]))).toBe(false); + }); + + it("returns false when called with a map", () => { + const map = new Map([ + ["dummy", "map"] + ]); + expect(isObject(map)).toBe(false); + }); +}); From 8e247ff04b7f38d4f65158b86a3434b36c89bda3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20D=C3=ADaz=20Gonz=C3=A1lez?= <dgonzalez@suse.de> Date: Tue, 20 Feb 2024 09:42:56 +0000 Subject: [PATCH 30/38] [web] Add method to reset localStorage in tests Sometimes could be needed to reset the window.localStorage cache between test examples, as it was the case for recently added tests for the space policy section. Thus, instead of having such utility hidden in and repeated in needed tests, it has been moved to a test-utils.js file where, with a bit of luck it will be more discoverable for others. Additionally, that new helpers allows to set an initial cache too by passing it an object with a collection of items compatible with Web Storage API setItem method. --- .../ProposalSpacePolicySection.test.jsx | 6 +- web/src/test-utils.js | 21 ++++++- web/src/test-utils.test.js | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 web/src/test-utils.test.js diff --git a/web/src/components/storage/ProposalSpacePolicySection.test.jsx b/web/src/components/storage/ProposalSpacePolicySection.test.jsx index 1f42884d73..07f842ab8b 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.test.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.test.jsx @@ -21,7 +21,7 @@ import React from "react"; import { screen, within } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; +import { plainRender, resetLocalStorage } from "~/test-utils"; import { ProposalSpacePolicySection } from "~/components/storage"; const sda = { @@ -110,9 +110,7 @@ beforeEach(() => { ], }; - // Reset localStorage to avoid keeping the SpaceActionsTable state between - // examples. - window.localStorage.clear(); + resetLocalStorage(); }); describe("ProposalSpacePolicySection", () => { diff --git a/web/src/test-utils.js b/web/src/test-utils.js index ea41f8c730..4c843703fa 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -32,7 +32,7 @@ import { render } from "@testing-library/react"; import { createClient } from "~/client/index"; import { InstallerClientProvider } from "~/context/installer"; -import { noop } from "./utils"; +import { noop, isObject } from "./utils"; import cockpit from "./lib/cockpit"; import { InstallerL10nProvider } from "./context/installerL10n"; import { L10nProvider } from "./context/l10n"; @@ -176,11 +176,28 @@ const mockGettext = () => { cockpit.gettext.mockImplementation(gettextFn); }; +/** + * Helper for clearing window.localStorage and setting an initial state if needed. + * + * @param {Object.<string, string>} [initialState] - a collection of keys/values as + * expected by {@link https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem Web Storage API setItem method} + */ +const resetLocalStorage = (initialState) => { + window.localStorage.clear(); + + if (!isObject(initialState)) return; + + Object.entries(initialState).forEach(([key, value]) => { + window.localStorage.setItem(key, value); + }); +}; + export { plainRender, installerRender, createCallbackMock, mockGettext, mockNavigateFn, - mockRoutes + mockRoutes, + resetLocalStorage }; diff --git a/web/src/test-utils.test.js b/web/src/test-utils.test.js new file mode 100644 index 0000000000..a76ccfb583 --- /dev/null +++ b/web/src/test-utils.test.js @@ -0,0 +1,57 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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 { + resetLocalStorage +} from "./test-utils"; + +beforeAll(() => { + jest.spyOn(Storage.prototype, "clear"); + jest.spyOn(Storage.prototype, "setItem"); +}); + +afterAll(() => jest.clearAllMocks()); + +describe("resetLocalStorage", () => { + it("clears window.localStorage", () => { + resetLocalStorage(); + expect(window.localStorage.clear).toHaveBeenCalled(); + }); + + it("does not set an initial state if it is not given", () => { + resetLocalStorage(); + expect(window.localStorage.setItem).not.toHaveBeenCalled(); + }); + + it("does not set an initial state if given value is not an object", () => { + resetLocalStorage(["wrong", "initial state"]); + expect(window.localStorage.setItem).not.toHaveBeenCalled(); + }); + + it("sets an initial state if given value is an object", () => { + resetLocalStorage({ + storage: "something", + for: "later" + }); + expect(window.localStorage.setItem).toHaveBeenCalledWith("storage", "something"); + expect(window.localStorage.setItem).toHaveBeenCalledWith("for", "later"); + }); +}); From 411664bd550ef7d0586a95a441304c3f86ec965b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 12:36:23 +0000 Subject: [PATCH 31/38] [service] Improve code for dynamically adding D-Bus interfaces --- service/lib/agama/dbus/storage/device.rb | 61 +------- service/lib/agama/dbus/storage/interfaces.rb | 12 +- .../agama/dbus/storage/interfaces/block.rb | 104 ------------- .../dbus/storage/interfaces/component.rb | 89 ----------- .../storage/interfaces/{raid.rb => device.rb} | 36 ++--- .../dbus/storage/interfaces/device/block.rb | 117 ++++++++++++++ .../storage/interfaces/device/component.rb | 97 ++++++++++++ .../dbus/storage/interfaces/device/drive.rb | 145 ++++++++++++++++++ .../storage/interfaces/device/filesystem.rb | 74 +++++++++ .../dbus/storage/interfaces/device/md.rb | 82 ++++++++++ .../storage/interfaces/device/multipath.rb | 66 ++++++++ .../interfaces/device/partition_table.rb | 76 +++++++++ .../dbus/storage/interfaces/device/raid.rb | 66 ++++++++ .../agama/dbus/storage/interfaces/drive.rb | 126 --------------- .../dbus/storage/interfaces/filesystem.rb | 61 -------- .../lib/agama/dbus/storage/interfaces/md.rb | 69 --------- .../dbus/storage/interfaces/multipath.rb | 53 ------- .../storage/interfaces/partition_table.rb | 61 -------- .../test/agama/dbus/storage/device_test.rb | 22 +-- .../interfaces/{ => device}/block_examples.rb | 4 +- .../{ => device}/component_examples.rb | 2 +- .../interfaces/{ => device}/drive_examples.rb | 4 +- .../{ => device}/filesystem_examples.rb | 2 +- .../interfaces/{ => device}/md_examples.rb | 2 +- .../{ => device}/multipath_examples.rb | 2 +- .../{ => device}/partition_table_examples.rb | 2 +- .../interfaces/{ => device}/raid_examples.rb | 2 +- 27 files changed, 765 insertions(+), 672 deletions(-) delete mode 100644 service/lib/agama/dbus/storage/interfaces/block.rb delete mode 100644 service/lib/agama/dbus/storage/interfaces/component.rb rename service/lib/agama/dbus/storage/interfaces/{raid.rb => device.rb} (52%) create mode 100644 service/lib/agama/dbus/storage/interfaces/device/block.rb create mode 100644 service/lib/agama/dbus/storage/interfaces/device/component.rb create mode 100644 service/lib/agama/dbus/storage/interfaces/device/drive.rb create mode 100644 service/lib/agama/dbus/storage/interfaces/device/filesystem.rb create mode 100644 service/lib/agama/dbus/storage/interfaces/device/md.rb create mode 100644 service/lib/agama/dbus/storage/interfaces/device/multipath.rb create mode 100644 service/lib/agama/dbus/storage/interfaces/device/partition_table.rb create mode 100644 service/lib/agama/dbus/storage/interfaces/device/raid.rb delete mode 100644 service/lib/agama/dbus/storage/interfaces/drive.rb delete mode 100644 service/lib/agama/dbus/storage/interfaces/filesystem.rb delete mode 100644 service/lib/agama/dbus/storage/interfaces/md.rb delete mode 100644 service/lib/agama/dbus/storage/interfaces/multipath.rb delete mode 100644 service/lib/agama/dbus/storage/interfaces/partition_table.rb rename service/test/agama/dbus/storage/interfaces/{ => device}/block_examples.rb (97%) rename service/test/agama/dbus/storage/interfaces/{ => device}/component_examples.rb (97%) rename service/test/agama/dbus/storage/interfaces/{ => device}/drive_examples.rb (98%) rename service/test/agama/dbus/storage/interfaces/{ => device}/filesystem_examples.rb (96%) rename service/test/agama/dbus/storage/interfaces/{ => device}/md_examples.rb (97%) rename service/test/agama/dbus/storage/interfaces/{ => device}/multipath_examples.rb (96%) rename service/test/agama/dbus/storage/interfaces/{ => device}/partition_table_examples.rb (96%) rename service/test/agama/dbus/storage/interfaces/{ => device}/raid_examples.rb (96%) diff --git a/service/lib/agama/dbus/storage/device.rb b/service/lib/agama/dbus/storage/device.rb index d4367c0457..4e1b07a206 100644 --- a/service/lib/agama/dbus/storage/device.rb +++ b/service/lib/agama/dbus/storage/device.rb @@ -21,14 +21,7 @@ require "dbus" require "agama/dbus/base_object" -require "agama/dbus/storage/interfaces/drive" -require "agama/dbus/storage/interfaces/raid" -require "agama/dbus/storage/interfaces/multipath" -require "agama/dbus/storage/interfaces/md" -require "agama/dbus/storage/interfaces/block" -require "agama/dbus/storage/interfaces/partition_table" -require "agama/dbus/storage/interfaces/filesystem" -require "agama/dbus/storage/interfaces/component" +require "agama/dbus/storage/interfaces/device" module Agama module DBus @@ -78,56 +71,14 @@ def storage_device=(value) # @return [DevicesTree] attr_reader :tree - # Adds the required interfaces according to the storage object - def add_interfaces # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity - interfaces = [] - interfaces << Interfaces::Drive if drive? - interfaces << Interfaces::Raid if storage_device.is?(:dm_raid) - interfaces << Interfaces::Md if storage_device.is?(:md) - interfaces << Interfaces::Multipath if storage_device.is?(:multipath) - interfaces << Interfaces::Block if storage_device.is?(:blk_device) - interfaces << Interfaces::PartitionTable if partition_table? - interfaces << Interfaces::Filesystem if filesystem? - interfaces << Interfaces::Component if component? + # Adds the required interfaces according to the storage object. + def add_interfaces + interfaces = Interfaces::Device.constants + .map { |c| Interfaces::Device.const_get(c) } + .select { |c| c.is_a?(Module) && c.respond_to?(:apply?) && c.apply?(storage_device) } interfaces.each { |i| singleton_class.include(i) } end - - # Whether the storage device is a drive - # - # Drive and disk device are very close concepts, but there are subtle differences. For - # example, a MD RAID is never considered as a drive. - # - # TODO: Revisit the defintion of drive. Maybe some MD devices could implement the drive - # interface if hwinfo provides useful information for them. - # - # @return [Boolean] - def drive? - storage_device.is?(:disk, :dm_raid, :multipath, :dasd) && storage_device.is?(:disk_device) - end - - # Whether the storage device has a partition table - # - # @return [Boolean] - def partition_table? - storage_device.is?(:blk_device) && - storage_device.respond_to?(:partition_table?) && - storage_device.partition_table? - end - - # Whether the storage device is formatted. - # - # @return [Boolean] - def filesystem? - storage_device.is?(:blk_device) && !storage_device.filesystem.nil? - end - - # Whether the storage device is component of other devices. - # - # @return [Boolean] - def component? - storage_device.is?(:blk_device) && storage_device.component_of.any? - end end end end diff --git a/service/lib/agama/dbus/storage/interfaces.rb b/service/lib/agama/dbus/storage/interfaces.rb index 7dac58df88..1c7b917c16 100644 --- a/service/lib/agama/dbus/storage/interfaces.rb +++ b/service/lib/agama/dbus/storage/interfaces.rb @@ -22,19 +22,13 @@ module Agama module DBus module Storage - # Module for storage specific D-Bus interfaces + # Module for D-Bus interfaces of storage. module Interfaces end end end end -require "agama/dbus/storage/interfaces/drive" -require "agama/dbus/storage/interfaces/raid" -require "agama/dbus/storage/interfaces/multipath" -require "agama/dbus/storage/interfaces/md" -require "agama/dbus/storage/interfaces/block" -require "agama/dbus/storage/interfaces/partition_table" -require "agama/dbus/storage/interfaces/filesystem" -require "agama/dbus/storage/interfaces/component" require "agama/dbus/storage/interfaces/dasd_manager" +require "agama/dbus/storage/interfaces/device" +require "agama/dbus/storage/interfaces/zfcp_manager" diff --git a/service/lib/agama/dbus/storage/interfaces/block.rb b/service/lib/agama/dbus/storage/interfaces/block.rb deleted file mode 100644 index 84648443f0..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/block.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for block devices - # - # @note This interface is intended to be included by {Device} if needed. - module Block - BLOCK_INTERFACE = "org.opensuse.Agama.Storage1.Block" - private_constant :BLOCK_INTERFACE - - # Name of the block device - # - # @return [String] e.g., "/dev/sda" - def block_name - storage_device.name - end - - # Whether the block device is currently active - # - # @return [Boolean] - def block_active - storage_device.active? - end - - # Name of the udev by-id links - # - # @return [Array<String>] - def block_udev_ids - storage_device.udev_ids - end - - # Name of the udev by-path links - # - # @return [Array<String>] - def block_udev_paths - storage_device.udev_paths - end - - # Size of the block device in bytes - # - # @return [Integer] - def block_size - storage_device.size.to_i - end - - # Size of the space that could be theoretically reclaimed by shrinking the device. - # - # @return [Integer] - def block_recoverable_size - storage_device.recoverable_size.to_i - end - - # Name of the currently installed systems - # - # @return [Array<String>] - def block_systems - return @systems if @systems - - filesystems = storage_device.descendants.select { |d| d.is?(:filesystem) } - @systems = filesystems.map(&:system_name).compact - end - - def self.included(base) - base.class_eval do - dbus_interface BLOCK_INTERFACE do - dbus_reader :block_name, "s", dbus_name: "Name" - dbus_reader :block_active, "b", dbus_name: "Active" - dbus_reader :block_udev_ids, "as", dbus_name: "UdevIds" - dbus_reader :block_udev_paths, "as", dbus_name: "UdevPaths" - dbus_reader :block_size, "t", dbus_name: "Size" - dbus_reader :block_recoverable_size, "t", dbus_name: "RecoverableSize" - dbus_reader :block_systems, "as", dbus_name: "Systems" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/component.rb b/service/lib/agama/dbus/storage/interfaces/component.rb deleted file mode 100644 index 9a9a7ba9e4..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/component.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -# 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 version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for devices that are used as component of other device (e.g., physical volume, - # MD RAID device, etc). - # - # @note This interface is intended to be included by {Device} if needed. - module Component - COMPONENT_INTERFACE = "org.opensuse.Agama.Storage1.Component" - private_constant :COMPONENT_INTERFACE - - # Type of component. - # - # @return [String] Possible values: - # "physical_volume" - # "md_device" - # "raid_device" - # "multipath_wire" - # "bcache_device" - # "bcache_cset_device" - # "md_btrfs_device" - def component_type - types = { - lvm_vg: "physical_volume", - md: "md_device", - dm_raid: "raid_device", - multipath: "multipath_wire", - bcache: "bcache_device", - bcache_cset: "bcache_cset_device", - btrfs: "md_btrfs_device" - } - - device = storage_device.component_of.first - - types.find { |k, _v| device.is?(k) }&.last || "" - end - - # Name of the devices for which this device is component of. - # - # @return [Array<String>] - def component_device_names - storage_device.component_of.map(&:display_name).compact - end - - # Paths of the D-Bus objects representing the devices. - # - # @return [Array<::DBus::ObjectPath>] - def component_devices - storage_device.component_of.map { |p| tree.path_for(p) } - end - - def self.included(base) - base.class_eval do - dbus_interface COMPONENT_INTERFACE do - dbus_reader :component_type, "s", dbus_name: "Type" - dbus_reader :component_device_names, "as", dbus_name: "DeviceNames" - dbus_reader :component_devices, "ao", dbus_name: "Devices" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/raid.rb b/service/lib/agama/dbus/storage/interfaces/device.rb similarity index 52% rename from service/lib/agama/dbus/storage/interfaces/raid.rb rename to service/lib/agama/dbus/storage/interfaces/device.rb index 7a66fb3556..98f09690c2 100644 --- a/service/lib/agama/dbus/storage/interfaces/raid.rb +++ b/service/lib/agama/dbus/storage/interfaces/device.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023-2024] SUSE LLC +# Copyright (c) [2024] SUSE LLC # # All Rights Reserved. # @@ -19,35 +19,23 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "dbus" - module Agama module DBus module Storage module Interfaces - # Interface for DM RAID devices - # - # @note This interface is intended to be included by {Device} if needed. - module Raid - RAID_INTERFACE = "org.opensuse.Agama.Storage1.RAID" - private_constant :RAID_INTERFACE - - # Paths of the D-Bus objects representing the devices used by the DM RAID. - # - # @return [Array<String>] - def raid_devices - storage_device.parents.map { |p| tree.path_for(p) } - end - - def self.included(base) - base.class_eval do - dbus_interface RAID_INTERFACE do - dbus_reader :raid_devices, "ao", dbus_name: "Devices" - end - end - end + # Module for D-Bus interfaces of a device. + module Device end end end end end + +require "agama/dbus/storage/interfaces/device/block" +require "agama/dbus/storage/interfaces/device/component" +require "agama/dbus/storage/interfaces/device/drive" +require "agama/dbus/storage/interfaces/device/filesystem" +require "agama/dbus/storage/interfaces/device/md" +require "agama/dbus/storage/interfaces/device/multipath" +require "agama/dbus/storage/interfaces/device/partition_table" +require "agama/dbus/storage/interfaces/device/raid" diff --git a/service/lib/agama/dbus/storage/interfaces/device/block.rb b/service/lib/agama/dbus/storage/interfaces/device/block.rb new file mode 100644 index 0000000000..b5c0e00017 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/block.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for block devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Block + # Whether this interface should be implemented for the given device. + # + # @note Block devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) + end + + BLOCK_INTERFACE = "org.opensuse.Agama.Storage1.Block" + private_constant :BLOCK_INTERFACE + + # Name of the block device + # + # @return [String] e.g., "/dev/sda" + def block_name + storage_device.name + end + + # Whether the block device is currently active + # + # @return [Boolean] + def block_active + storage_device.active? + end + + # Name of the udev by-id links + # + # @return [Array<String>] + def block_udev_ids + storage_device.udev_ids + end + + # Name of the udev by-path links + # + # @return [Array<String>] + def block_udev_paths + storage_device.udev_paths + end + + # Size of the block device in bytes + # + # @return [Integer] + def block_size + storage_device.size.to_i + end + + # Size of the space that could be theoretically reclaimed by shrinking the device. + # + # @return [Integer] + def block_recoverable_size + storage_device.recoverable_size.to_i + end + + # Name of the currently installed systems + # + # @return [Array<String>] + def block_systems + return @systems if @systems + + filesystems = storage_device.descendants.select { |d| d.is?(:filesystem) } + @systems = filesystems.map(&:system_name).compact + end + + def self.included(base) + base.class_eval do + dbus_interface BLOCK_INTERFACE do + dbus_reader :block_name, "s", dbus_name: "Name" + dbus_reader :block_active, "b", dbus_name: "Active" + dbus_reader :block_udev_ids, "as", dbus_name: "UdevIds" + dbus_reader :block_udev_paths, "as", dbus_name: "UdevPaths" + dbus_reader :block_size, "t", dbus_name: "Size" + dbus_reader :block_recoverable_size, "t", dbus_name: "RecoverableSize" + dbus_reader :block_systems, "as", dbus_name: "Systems" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/component.rb b/service/lib/agama/dbus/storage/interfaces/device/component.rb new file mode 100644 index 0000000000..c66422b2b4 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/component.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for devices that are used as component of other device (e.g., physical volume, + # MD RAID device, etc). + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Component + # Whether this interface should be implemented for the given device. + # + # @note Components of other devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) && storage_device.component_of.any? + end + + COMPONENT_INTERFACE = "org.opensuse.Agama.Storage1.Component" + private_constant :COMPONENT_INTERFACE + + # Type of component. + # + # @return ["physical_volume", "md_device", "raid_device", "multipath_wire", + # "bcache_device", "bcache_cset_device", "md_btrfs_device", ""] Empty if type is + # unknown. + def component_type + types = { + lvm_vg: "physical_volume", + md: "md_device", + dm_raid: "raid_device", + multipath: "multipath_wire", + bcache: "bcache_device", + bcache_cset: "bcache_cset_device", + btrfs: "md_btrfs_device" + } + + device = storage_device.component_of.first + + types.find { |k, _v| device.is?(k) }&.last || "" + end + + # Name of the devices for which this device is component of. + # + # @return [Array<String>] + def component_device_names + storage_device.component_of.map(&:display_name).compact + end + + # Paths of the D-Bus objects representing the devices. + # + # @return [Array<::DBus::ObjectPath>] + def component_devices + storage_device.component_of.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface COMPONENT_INTERFACE do + dbus_reader :component_type, "s", dbus_name: "Type" + dbus_reader :component_device_names, "as", dbus_name: "DeviceNames" + dbus_reader :component_devices, "ao", dbus_name: "Devices" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/drive.rb b/service/lib/agama/dbus/storage/interfaces/device/drive.rb new file mode 100644 index 0000000000..0466ad7984 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/drive.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for drive devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Drive + # Whether this interface should be implemented for the given device. + # + # @note Drive devices implement this interface. + # Drive and disk device are very close concepts, but there are subtle differences. For + # example, a MD RAID is never considered as a drive. + # + # TODO: Revisit the defintion of drive. Maybe some MD devices could implement the drive + # interface if hwinfo provides useful information for them. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:disk, :dm_raid, :multipath, :dasd) && + storage_device.is?(:disk_device) + end + + DRIVE_INTERFACE = "org.opensuse.Agama.Storage1.Drive" + private_constant :DRIVE_INTERFACE + + # Drive type + # + # @return ["disk", "raid", "multipath", "dasd", ""] Empty if type is unknown. + def drive_type + if storage_device.is?(:disk) + "disk" + elsif storage_device.is?(:dm_raid) + "raid" + elsif storage_device.is?(:multipath) + "multipath" + elsif storage_device.is?(:dasd) + "dasd" + else + "" + end + end + + # Vendor name + # + # @return [String] + def drive_vendor + storage_device.vendor || "" + end + + # Model name + # + # @return [String] + def drive_model + storage_device.model || "" + end + + # Bus name + # + # @return [String] + def drive_bus + storage_device.bus || "" + end + + # Bus Id for DASD + # + # @return [String] + def drive_bus_id + return "" unless storage_device.respond_to?(:bus_id) + + storage_device.bus_id + end + + # Kernel drivers used by the device + # + # @return [Array<String>] + def drive_driver + storage_device.driver + end + + # Data transport layer, if any + # + # @return [String] + def drive_transport + return "" unless storage_device.respond_to?(:transport) + + storage_device.transport.to_s + end + + # More info about the device + # + # @return [Hash] + def drive_info + { + "SDCard" => storage_device.sd_card?, + "DellBOSS" => storage_device.boss? + } + end + + def self.included(base) + base.class_eval do + dbus_interface DRIVE_INTERFACE do + dbus_reader :drive_type, "s", dbus_name: "Type" + dbus_reader :drive_vendor, "s", dbus_name: "Vendor" + dbus_reader :drive_model, "s", dbus_name: "Model" + dbus_reader :drive_bus, "s", dbus_name: "Bus" + dbus_reader :drive_bus_id, "s", dbus_name: "BusId" + dbus_reader :drive_driver, "as", dbus_name: "Driver" + dbus_reader :drive_transport, "s", dbus_name: "Transport" + dbus_reader :drive_info, "a{sv}", dbus_name: "Info" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb new file mode 100644 index 0000000000..36ee17c2ea --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for file systems. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Filesystem + # Whether this interface should be implemented for the given device. + # + # @note Formatted devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) && !storage_device.filesystem.nil? + end + + FILESYSTEM_INTERFACE = "org.opensuse.Agama.Storage1.Filesystem" + private_constant :FILESYSTEM_INTERFACE + + # File system type. + # + # @return [String] e.g., "ext4" + def filesystem_type + storage_device.filesystem.type.to_s + end + + # Whether the filesystem contains the directory layout of an ESP partition. + # + # @return [Boolean] + def filesystem_efi? + storage_device.filesystem.efi? + end + + def self.included(base) + base.class_eval do + dbus_interface FILESYSTEM_INTERFACE do + dbus_reader :filesystem_type, "s", dbus_name: "Type" + dbus_reader :filesystem_efi?, "b", dbus_name: "EFI" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/md.rb b/service/lib/agama/dbus/storage/interfaces/device/md.rb new file mode 100644 index 0000000000..6cf144489d --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/md.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for MD RAID devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Md + # Whether this interface should be implemented for the given device. + # + # @note MD RAIDs implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:md) + end + + MD_INTERFACE = "org.opensuse.Agama.Storage1.MD" + private_constant :MD_INTERFACE + + # UUID of the MD RAID + # + # @return [String] + def md_uuid + storage_device.uuid + end + + # RAID level + # + # @return [String] + def md_level + storage_device.md_level.to_s + end + + # Paths of the D-Bus objects representing the devices of the MD RAID. + # + # @return [Array<String>] + def md_devices + storage_device.plain_devices.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface MD_INTERFACE do + dbus_reader :md_uuid, "s", dbus_name: "UUID" + dbus_reader :md_level, "s", dbus_name: "Level" + dbus_reader :md_devices, "ao", dbus_name: "Devices" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/multipath.rb b/service/lib/agama/dbus/storage/interfaces/device/multipath.rb new file mode 100644 index 0000000000..3293ed376d --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/multipath.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for Multipath devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Multipath + # Whether this interface should be implemented for the given device. + # + # @note Multipath devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:multipath) + end + + MULTIPATH_INTERFACE = "org.opensuse.Agama.Storage1.Multipath" + private_constant :MULTIPATH_INTERFACE + + # Paths of the D-Bus objects representing the multipath wires. + # + # @return [Array<String>] + def multipath_wires + storage_device.parents.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface MULTIPATH_INTERFACE do + dbus_reader :multipath_wires, "ao", dbus_name: "Wires" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb b/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb new file mode 100644 index 0000000000..546ddfd934 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for devices that contain a partition table. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module PartitionTable + # Whether this interface should be implemented for the given device. + # + # @note Devices containing a partition table implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) && + storage_device.respond_to?(:partition_table?) && + storage_device.partition_table? + end + + PARTITION_TABLE_INTERFACE = "org.opensuse.Agama.Storage1.PartitionTable" + private_constant :PARTITION_TABLE_INTERFACE + + # Type of the partition table + # + # @return [String] + def partition_table_type + storage_device.partition_table.type.to_s + end + + # Paths of the D-Bus objects representing the partitions. + # + # @return [Array<::DBus::ObjectPath>] + def partition_table_partitions + storage_device.partition_table.partitions.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface PARTITION_TABLE_INTERFACE do + dbus_reader :partition_table_type, "s", dbus_name: "Type" + dbus_reader :partition_table_partitions, "ao", dbus_name: "Partitions" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/raid.rb b/service/lib/agama/dbus/storage/interfaces/device/raid.rb new file mode 100644 index 0000000000..228e389c4b --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/raid.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for DM RAID devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Raid + # Whether this interface should be implemented for the given device. + # + # @note DM RAIDs implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:dm_raid) + end + + RAID_INTERFACE = "org.opensuse.Agama.Storage1.RAID" + private_constant :RAID_INTERFACE + + # Paths of the D-Bus objects representing the devices used by the DM RAID. + # + # @return [Array<String>] + def raid_devices + storage_device.parents.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface RAID_INTERFACE do + dbus_reader :raid_devices, "ao", dbus_name: "Devices" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/drive.rb b/service/lib/agama/dbus/storage/interfaces/drive.rb deleted file mode 100644 index 6d87ef33a4..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/drive.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for drive devices - # - # @note This interface is intended to be included by {Device} if needed. - module Drive - DRIVE_INTERFACE = "org.opensuse.Agama.Storage1.Drive" - private_constant :DRIVE_INTERFACE - - # Drive type - # - # @return ["disk", "raid", "multipath", "dasd"] - def drive_type - if storage_device.is?(:disk) - "disk" - elsif storage_device.is?(:dm_raid) - "raid" - elsif storage_device.is?(:multipath) - "multipath" - elsif storage_device.is?(:dasd) - "dasd" - else - "" - end - end - - # Vendor name - # - # @return [String] - def drive_vendor - storage_device.vendor || "" - end - - # Model name - # - # @return [String] - def drive_model - storage_device.model || "" - end - - # Bus name - # - # @return [String] - def drive_bus - storage_device.bus || "" - end - - # Bus Id for DASD - # - # @return [String] - def drive_bus_id - return "" unless storage_device.respond_to?(:bus_id) - - storage_device.bus_id - end - - # Kernel drivers used by the device - # - # @return [Array<String>] - def drive_driver - storage_device.driver - end - - # Data transport layer, if any - # - # @return [String] - def drive_transport - return "" unless storage_device.respond_to?(:transport) - - storage_device.transport.to_s - end - - # More info about the device - # - # @return [Hash] - def drive_info - { - "SDCard" => storage_device.sd_card?, - "DellBOSS" => storage_device.boss? - } - end - - def self.included(base) - base.class_eval do - dbus_interface DRIVE_INTERFACE do - dbus_reader :drive_type, "s", dbus_name: "Type" - dbus_reader :drive_vendor, "s", dbus_name: "Vendor" - dbus_reader :drive_model, "s", dbus_name: "Model" - dbus_reader :drive_bus, "s", dbus_name: "Bus" - dbus_reader :drive_bus_id, "s", dbus_name: "BusId" - dbus_reader :drive_driver, "as", dbus_name: "Driver" - dbus_reader :drive_transport, "s", dbus_name: "Transport" - dbus_reader :drive_info, "a{sv}", dbus_name: "Info" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/filesystem.rb b/service/lib/agama/dbus/storage/interfaces/filesystem.rb deleted file mode 100644 index 51e69d0a39..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/filesystem.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -# 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 version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for file systems. - # - # @note This interface is intended to be included by {Device} if needed. - module Filesystem - FILESYSTEM_INTERFACE = "org.opensuse.Agama.Storage1.Filesystem" - private_constant :FILESYSTEM_INTERFACE - - # File system type. - # - # @return [String] e.g., "ext4" - def filesystem_type - storage_device.filesystem.type.to_s - end - - # Whether the file system contains an EFI. - # - # @return [Boolean] - def filesystem_efi? - storage_device.filesystem.efi? - end - - def self.included(base) - base.class_eval do - dbus_interface FILESYSTEM_INTERFACE do - dbus_reader :filesystem_type, "s", dbus_name: "Type" - dbus_reader :filesystem_efi?, "b", dbus_name: "EFI" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/md.rb b/service/lib/agama/dbus/storage/interfaces/md.rb deleted file mode 100644 index 8bc996c4b2..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/md.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for MD RAID devices - # - # @note This interface is intended to be included by {Device} if needed. - module Md - MD_INTERFACE = "org.opensuse.Agama.Storage1.MD" - private_constant :MD_INTERFACE - - # UUID of the MD RAID - # - # @return [String] - def md_uuid - storage_device.uuid - end - - # RAID level - # - # @return [String] - def md_level - storage_device.md_level.to_s - end - - # Paths of the D-Bus objects representing the devices of the MD RAID. - # - # @return [Array<String>] - def md_devices - storage_device.plain_devices.map { |p| tree.path_for(p) } - end - - def self.included(base) - base.class_eval do - dbus_interface MD_INTERFACE do - dbus_reader :md_uuid, "s", dbus_name: "UUID" - dbus_reader :md_level, "s", dbus_name: "Level" - dbus_reader :md_devices, "ao", dbus_name: "Devices" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/multipath.rb b/service/lib/agama/dbus/storage/interfaces/multipath.rb deleted file mode 100644 index 06841520da..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/multipath.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for Multipath devices - # - # @note This interface is intended to be included by {Device} if needed. - module Multipath - MULTIPATH_INTERFACE = "org.opensuse.Agama.Storage1.Multipath" - private_constant :MULTIPATH_INTERFACE - - # Paths of the D-Bus objects representing the multipath wires. - # - # @return [Array<String>] - def multipath_wires - storage_device.parents.map { |p| tree.path_for(p) } - end - - def self.included(base) - base.class_eval do - dbus_interface MULTIPATH_INTERFACE do - dbus_reader :multipath_wires, "ao", dbus_name: "Wires" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/partition_table.rb b/service/lib/agama/dbus/storage/interfaces/partition_table.rb deleted file mode 100644 index 635a331984..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/partition_table.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for devices that contain a partition table - # - # @note This interface is intended to be included by {Device} if needed. - module PartitionTable - PARTITION_TABLE_INTERFACE = "org.opensuse.Agama.Storage1.PartitionTable" - private_constant :PARTITION_TABLE_INTERFACE - - # Type of the partition table - # - # @return [String] - def partition_table_type - storage_device.partition_table.type.to_s - end - - # Paths of the D-Bus objects representing the partitions. - # - # @return [Array<::DBus::ObjectPath>] - def partition_table_partitions - storage_device.partition_table.partitions.map { |p| tree.path_for(p) } - end - - def self.included(base) - base.class_eval do - dbus_interface PARTITION_TABLE_INTERFACE do - dbus_reader :partition_table_type, "s", dbus_name: "Type" - dbus_reader :partition_table_partitions, "ao", dbus_name: "Partitions" - end - end - end - end - end - end - end -end diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index 63cf459b0e..649131a83f 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -19,19 +19,19 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../test_helper" -require_relative "../../storage/storage_helpers" -require_relative "./interfaces/drive_examples" -require_relative "./interfaces/raid_examples" -require_relative "./interfaces/multipath_examples" -require_relative "./interfaces/block_examples" -require_relative "./interfaces/md_examples" -require_relative "./interfaces/partition_table_examples" -require_relative "./interfaces/filesystem_examples" -require_relative "./interfaces/component_examples" require "agama/dbus/storage/device" require "agama/dbus/storage/devices_tree" require "dbus" +require_relative "../../../test_helper" +require_relative "../../storage/storage_helpers" +require_relative "./interfaces/device/block_examples" +require_relative "./interfaces/device/component_examples" +require_relative "./interfaces/device/drive_examples" +require_relative "./interfaces/device/filesystem_examples" +require_relative "./interfaces/device/md_examples" +require_relative "./interfaces/device/multipath_examples" +require_relative "./interfaces/device/partition_table_examples" +require_relative "./interfaces/device/raid_examples" describe Agama::DBus::Storage::Device do include Agama::RSpec::StorageHelpers @@ -101,7 +101,7 @@ expect(subject).to_not include_dbus_interface("org.opensuse.Agama.Storage1.Drive") end - it "defines the RAID interface" do + it "defines the MD interface" do expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.MD") end diff --git a/service/test/agama/dbus/storage/interfaces/block_examples.rb b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb similarity index 97% rename from service/test/agama/dbus/storage/interfaces/block_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/block_examples.rb index 5922038a3f..11590a3390 100644 --- a/service/test/agama/dbus/storage/interfaces/block_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "Block interface" do describe "Block D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/component_examples.rb b/service/test/agama/dbus/storage/interfaces/device/component_examples.rb similarity index 97% rename from service/test/agama/dbus/storage/interfaces/component_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/component_examples.rb index 60b7f4aad1..c7ea69f495 100644 --- a/service/test/agama/dbus/storage/interfaces/component_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/component_examples.rb @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "Component interface" do describe "Component D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/drive_examples.rb b/service/test/agama/dbus/storage/interfaces/device/drive_examples.rb similarity index 98% rename from service/test/agama/dbus/storage/interfaces/drive_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/drive_examples.rb index f27e243012..62595298c4 100644 --- a/service/test/agama/dbus/storage/interfaces/drive_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/drive_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" require "y2storage/disk_size" shared_examples "Drive interface" do diff --git a/service/test/agama/dbus/storage/interfaces/filesystem_examples.rb b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb similarity index 96% rename from service/test/agama/dbus/storage/interfaces/filesystem_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb index f2323e662c..c581bd7668 100644 --- a/service/test/agama/dbus/storage/interfaces/filesystem_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "Filesystem interface" do describe "Filesystem D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/md_examples.rb b/service/test/agama/dbus/storage/interfaces/device/md_examples.rb similarity index 97% rename from service/test/agama/dbus/storage/interfaces/md_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/md_examples.rb index bc04dbc7c4..a8fd1d1711 100644 --- a/service/test/agama/dbus/storage/interfaces/md_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/md_examples.rb @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "MD interface" do describe "MD D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/multipath_examples.rb b/service/test/agama/dbus/storage/interfaces/device/multipath_examples.rb similarity index 96% rename from service/test/agama/dbus/storage/interfaces/multipath_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/multipath_examples.rb index f134a06872..ddf59a7d73 100644 --- a/service/test/agama/dbus/storage/interfaces/multipath_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/multipath_examples.rb @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "Multipath interface" do describe "Multipath D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/partition_table_examples.rb b/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb similarity index 96% rename from service/test/agama/dbus/storage/interfaces/partition_table_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb index a902e3dbda..1af30111ab 100644 --- a/service/test/agama/dbus/storage/interfaces/partition_table_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "PartitionTable interface" do describe "PartitionTable D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/raid_examples.rb b/service/test/agama/dbus/storage/interfaces/device/raid_examples.rb similarity index 96% rename from service/test/agama/dbus/storage/interfaces/raid_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/raid_examples.rb index 4a30d575d2..7b53df1978 100644 --- a/service/test/agama/dbus/storage/interfaces/raid_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/raid_examples.rb @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "RAID interface" do describe "RAID D-Bus interface" do From cd387a6918ce7fdb6d33bd8ebea15229ce845b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 12:59:34 +0000 Subject: [PATCH 32/38] [web] Add notes for translators --- .../storage/ProposalSpacePolicySection.jsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 297f64a6a7..e5265e9935 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -106,6 +106,7 @@ const DeviceContentColumn = ({ device }) => { const PartitionTableContent = () => { return ( <div> + {/* TRANSLATORS: %s is replaced by partition table type (e.g., GPT) */} {sprintf(_("%s partition table"), device.partitionTable.type.toUpperCase())} </div> ); @@ -118,13 +119,18 @@ const DeviceContentColumn = ({ device }) => { const filesystem = device.filesystem; if (filesystem?.isEFI) return _("EFI"); - if (filesystem) return sprintf(_("%s file system"), filesystem?.type); + if (filesystem) { + // TRANSLATORS: %s is replaced by a file system type (e.g., btrfs). + return sprintf(_("%s file system"), filesystem?.type); + } const component = device.component; switch (component?.type) { case "physical_volume": + // TRANSLATORS: %s is replaced by a LVM volume group name (e.g., /dev/vg0). return sprintf(_("LVM physical volume of %s"), component.deviceNames[0]); case "md_device": + // TRANSLATORS: %s is replaced by a RAID name (e.g., /dev/md0). return sprintf(_("Member of RAID %s"), component.deviceNames[0]); default: return _("Not identified"); @@ -156,6 +162,7 @@ const DeviceDetailsColumn = ({ device }) => { return ( <div> + {/* TRANSLATORS: %s is replaced by a disk size (e.g., 20 GiB) */} {sprintf(_("%s unused"), deviceSize(unused))} </div> ); @@ -168,13 +175,14 @@ const DeviceDetailsColumn = ({ device }) => { return ( <div> + {/* TRANSLATORS: %s is replaced by a disk size (e.g., 2 GiB) */} {sprintf(_("Shrinkable by %s"), deviceSize(device.recoverableSize))} </div> ); }; return ( - <If condition={device.isDrive} then={<UnusedSize />} else={<RecoverableSize /> } /> + <If condition={device.isDrive} then={<UnusedSize />} else={<RecoverableSize />} /> ); }; @@ -198,7 +206,10 @@ const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noo value={value} isDisabled={isDisabled} onChange={changeAction} - aria-label={sprintf(_("Space action selector for %s"), device.name)} + aria-label={ + /* TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) */ + sprintf(_("Space action selector for %s"), device.name) + } > <FormSelectOption value="force_delete" label={_("Delete")} /> <If From 819cdacbfa3003e002ae64de2aa0a3b62b48b837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 13:01:24 +0000 Subject: [PATCH 33/38] [web] More precise EFI description --- web/src/components/storage/ProposalSpacePolicySection.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index e5265e9935..48683c20e7 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -118,7 +118,7 @@ const DeviceContentColumn = ({ device }) => { if (systems.length > 0) return systems.join(", "); const filesystem = device.filesystem; - if (filesystem?.isEFI) return _("EFI"); + if (filesystem?.isEFI) return _("EFI system partition"); if (filesystem) { // TRANSLATORS: %s is replaced by a file system type (e.g., btrfs). return sprintf(_("%s file system"), filesystem?.type); From 9fb92048a56a3964f26a21bb77340795e1e6ea7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 13:03:18 +0000 Subject: [PATCH 34/38] [web] Add missing documentation --- web/src/components/storage/ProposalSpacePolicySection.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 48683c20e7..18fbf62f10 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -154,6 +154,13 @@ const DeviceSizeColumn = ({ device }) => { return <div>{deviceSize(device.size)}</div>; }; +/** + * Column content with details about the device. + * @component + * + * @param {object} props + * @param {StorageDevice} props.device + */ const DeviceDetailsColumn = ({ device }) => { const UnusedSize = () => { if (device.filesystem) return null; From 0cf99cab0a42c60c9d0d816ddeb9029ad7eae964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 13:11:46 +0000 Subject: [PATCH 35/38] [web] Add comment for explaining corner case --- web/src/components/storage/ProposalSpacePolicySection.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/components/storage/ProposalSpacePolicySection.jsx b/web/src/components/storage/ProposalSpacePolicySection.jsx index 18fbf62f10..9c2d65fc45 100644 --- a/web/src/components/storage/ProposalSpacePolicySection.jsx +++ b/web/src/components/storage/ProposalSpacePolicySection.jsx @@ -206,6 +206,9 @@ const DeviceDetailsColumn = ({ device }) => { const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noop }) => { const changeAction = (_, action) => onChange({ device: device.name, action }); + // For a drive device (e.g., Disk, RAID) it does not make sense to offer the resize action. + // At this moment, the Agama backend generates a resize action for drives if the policy is set to + // 'resize'. In that cases, the action is converted here to 'keep'. const value = (device.isDrive && action === "resize") ? "keep" : action; return ( @@ -219,6 +222,7 @@ const DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noo } > <FormSelectOption value="force_delete" label={_("Delete")} /> + {/* Resize action does not make sense for drives, so it is filtered out. */} <If condition={!device.isDrive} then={<FormSelectOption value="resize" label={_("Allow resize")} />} From e037e5697e7faadf685c1045df733f2db577f42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 13:13:18 +0000 Subject: [PATCH 36/38] [web] Properly indent cspell --- web/cspell.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/cspell.json b/web/cspell.json index 86eef9bb7f..9c7619f4a9 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -75,7 +75,7 @@ "textinput", "tkip", "udev", - "unpartitioned", + "unpartitioned", "wwpn", "xxxs", "zfcp" From 2cb1298bb51d38f6564b57625d0833e2ded28c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 13:16:24 +0000 Subject: [PATCH 37/38] [service] Changelog --- service/package/rubygem-agama.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/service/package/rubygem-agama.changes b/service/package/rubygem-agama.changes index 4b2c8a6496..8c07ea0b36 100644 --- a/service/package/rubygem-agama.changes +++ b/service/package/rubygem-agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Feb 20 13:15:15 UTC 2024 - José Iván López González <jlopez@suse.com> + +- Add Filesystem and Component D-Bus interfaces + (gh#openSUSE/agama#1028). + ------------------------------------------------------------------- Wed Feb 7 11:49:02 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com> From 43e470c5d880fbe8d687e9fe37bb3d94fd12d3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Iv=C3=A1n=20L=C3=B3pez=20Gonz=C3=A1lez?= <jlopez@suse.com> Date: Tue, 20 Feb 2024 13:16:59 +0000 Subject: [PATCH 38/38] [web] Changelog --- web/package/cockpit-agama.changes | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/package/cockpit-agama.changes b/web/package/cockpit-agama.changes index bded8c6aff..43034076bc 100644 --- a/web/package/cockpit-agama.changes +++ b/web/package/cockpit-agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Feb 20 13:13:51 UTC 2024 - José Iván López González <jlopez@suse.com> + +- Add section for space policy to the storage page + (gh#openSUSE/agama#1028). + ------------------------------------------------------------------- Wed Feb 14 12:42:32 UTC 2024 - David Diaz <dgonzalez@suse.com>