Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web: Allow changing file system location #1141

Merged
merged 6 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions web/package/cockpit-agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Thu Apr 11 15:16:42 UTC 2024 - José Iván López González <jlopez@suse.com>

- Allow changing the location of a file system
(gh#openSUSE/agama#1141).

-------------------------------------------------------------------
Mon Apr 8 14:17:45 UTC 2024 - José Iván López González <jlopez@suse.com>

Expand Down
2 changes: 1 addition & 1 deletion web/src/client/mixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ const WithStatus = (superclass, object_path) => class extends superclass {
* Register a callback to run when the "CurrentInstallationPhase" changes
*
* @param {function} handler - callback function
* @return {function} function to disable the callback
* @return {() => void} function to disable the callback
*/
onStatusChange(handler) {
return this.client.onObjectChanged(object_path, STATUS_IFACE, (changes) => {
Expand Down
87 changes: 78 additions & 9 deletions web/src/client/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk";
* @property {string} [busId] - DASD Bus ID (only for "dasd" type)
* @property {string} [transport]
* @property {boolean} [sdCard]
* @property {boolean} [dellBOOS]
* @property {boolean} [dellBOSS]
* @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)
Expand Down Expand Up @@ -110,8 +110,10 @@ const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk";
* @property {boolean} subvol
* @property {boolean} delete
*
* @todo Define an enum for space policies.
*
* @typedef {object} ProposalSettings
* @property {string} target
* @property {ProposalTarget} target
* @property {string} [targetDevice]
* @property {string[]} targetPVDevices
* @property {boolean} configureBoot
Expand All @@ -124,13 +126,15 @@ const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk";
* @property {Volume[]} volumes
* @property {StorageDevice[]} installationDevices
*
* @typedef {keyof ProposalTargets} ProposalTarget
*
* @typedef {object} SpaceAction
* @property {string} device
* @property {string} action
*
* @typedef {object} Volume
* @property {string} mountPath
* @property {string} target
* @property {VolumeTarget} target
* @property {StorageDevice} [targetDevice]
* @property {string} fsType
* @property {number} minSize
Expand All @@ -140,15 +144,44 @@ const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk";
* @property {boolean} transactional
* @property {VolumeOutline} outline
*
* @typedef {keyof VolumeTargets} VolumeTarget
*
* @todo Define an enum for file system types.
*
* @typedef {object} VolumeOutline
* @property {boolean} required
* @property {string[]} fsTypes
* @property {boolean} adjustByRam
* @property {boolean} supportAutoSize
* @property {boolean} snapshotsConfigurable
* @property {boolean} snapshotsAffectSizes
* @property {string[]} sizeRelevantVolumes
*/

/**
* Enum for the possible proposal targets.
*
* @readonly
*/
const ProposalTargets = Object.freeze({
DISK: "disk",
NEW_LVM_VG: "newLvmVg",
REUSED_LVM_VG: "reusedLvmVg"
});

/**
* Enum for the possible volume targets.
*
* @readonly
*/
dgdavid marked this conversation as resolved.
Show resolved Hide resolved
const VolumeTargets = Object.freeze({
DEFAULT: "default",
NEW_PARTITION: "new_partition",
NEW_VG: "new_vg",
DEVICE: "device",
FILESYSTEM: "filesystem"
});

/**
* Enum for the encryption method values
*
Expand Down Expand Up @@ -463,6 +496,23 @@ class ProposalManager {
};
};

/**
* Builds the proposal target from a D-Bus value.
*
* @param {string} dbusTarget
* @returns {ProposalTarget}
*/
const buildTarget = (dbusTarget) => {
switch (dbusTarget) {
case "disk": return "DISK";
case "newLvmVg": return "NEW_LVM_VG";
case "reusedLvmVg": return "REUSED_LVM_VG";
default:
console.info(`Unknown proposal target "${dbusTarget}", using "disk".`);
return "DISK";
}
};

const buildTargetPVDevices = dbusTargetPVDevices => {
if (!dbusTargetPVDevices) return [];

Expand All @@ -474,7 +524,7 @@ class ProposalManager {
const findDevice = (name) => {
const device = devices.find(d => d.name === name);

if (device === undefined) console.log("D-Bus object not found: ", name);
if (device === undefined) console.error("D-Bus object not found: ", name);

return device;
};
Expand All @@ -489,14 +539,14 @@ class ProposalManager {
const names = uniq(compact(values)).filter(d => d.length > 0);

// #findDevice returns undefined if no device is found with the given name.
return compact(names.map(n => findDevice(n)));
return compact(names.map(findDevice));
};

const dbusSettings = proxy.Settings;

return {
settings: {
target: dbusSettings.Target.v,
target: buildTarget(dbusSettings.Target.v),
targetDevice: dbusSettings.TargetDevice?.v,
targetPVDevices: buildTargetPVDevices(dbusSettings.TargetPVDevices),
configureBoot: dbusSettings.ConfigureBoot.v,
Expand Down Expand Up @@ -560,15 +610,15 @@ class ProposalManager {
MinSize: { t: "t", v: volume.minSize },
MaxSize: { t: "t", v: volume.maxSize },
AutoSize: { t: "b", v: volume.autoSize },
Target: { t: "s", v: volume.target },
Target: { t: "s", v: VolumeTargets[volume.target] },
TargetDevice: { t: "s", v: volume.targetDevice?.name },
Snapshots: { t: "b", v: volume.snapshots },
Transactional: { t: "b", v: volume.transactional },
});
};

const dbusSettings = removeUndefinedCockpitProperties({
Target: { t: "s", v: target },
Target: { t: "s", v: ProposalTargets[target] },
TargetDevice: { t: "s", v: targetDevice },
TargetPVDevices: { t: "as", v: targetPVDevices },
ConfigureBoot: { t: "b", v: configureBoot },
Expand Down Expand Up @@ -635,6 +685,25 @@ class ProposalManager {
* @returns {Volume}
*/
buildVolume(dbusVolume, devices) {
/**
* Builds a volume target from a D-Bus value.
*
* @param {string} dbusTarget
* @returns {VolumeTarget}
*/
const buildTarget = (dbusTarget) => {
switch (dbusTarget) {
case "default": return "DEFAULT";
case "new_partition": return "NEW_PARTITION";
case "new_vg": return "NEW_VG";
case "device": return "DEVICE";
case "filesystem": return "FILESYSTEM";
default:
console.info(`Unknown volume target "${dbusTarget}", using "default".`);
return "DEFAULT";
}
};

const buildOutline = (dbusOutline) => {
if (dbusOutline === undefined) return null;

Expand All @@ -650,7 +719,7 @@ class ProposalManager {
};

return {
target: dbusVolume.Target.v,
target: buildTarget(dbusVolume.Target.v),
targetDevice: devices.find(d => d.name === dbusVolume.TargetDevice?.v),
mountPath: dbusVolume.MountPath.v,
fsType: dbusVolume.FsType.v,
Expand Down
12 changes: 6 additions & 6 deletions web/src/client/storage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,7 @@ describe("#proposal", () => {

expect(home).toStrictEqual({
mountPath: "/home",
target: "default",
target: "DEFAULT",
targetDevice: undefined,
fsType: "XFS",
minSize: 2048,
Expand All @@ -1490,7 +1490,7 @@ describe("#proposal", () => {

expect(generic).toStrictEqual({
mountPath: "",
target: "default",
target: "DEFAULT",
targetDevice: undefined,
fsType: "Ext4",
minSize: 1024,
Expand Down Expand Up @@ -1535,7 +1535,7 @@ describe("#proposal", () => {
const { settings, actions } = await client.proposal.getResult();

expect(settings).toMatchObject({
target: "newLvmVg",
target: "NEW_LVM_VG",
targetPVDevices: ["/dev/sda", "/dev/sdb"],
configureBoot: true,
bootDevice: "/dev/sda",
Expand All @@ -1549,7 +1549,7 @@ describe("#proposal", () => {
volumes: [
{
mountPath: "/",
target: "default",
target: "DEFAULT",
targetDevice: undefined,
fsType: "Btrfs",
minSize: 1024,
Expand All @@ -1568,7 +1568,7 @@ describe("#proposal", () => {
},
{
mountPath: "/home",
target: "default",
target: "DEFAULT",
targetDevice: undefined,
fsType: "XFS",
minSize: 2048,
Expand Down Expand Up @@ -1615,7 +1615,7 @@ describe("#proposal", () => {

it("calculates a proposal with the given settings", async () => {
await client.proposal.calculate({
target: "disk",
target: "DISK",
targetDevice: "/dev/vdc",
configureBoot: true,
bootDevice: "/dev/vdb",
Expand Down
8 changes: 6 additions & 2 deletions web/src/components/core/Description.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@
* find current contact information at www.suse.com.
*/

// @ts-check

import React from "react";
import { Popover, Button } from "@patternfly/react-core";

/**
* Displays details popup after clicking the children elements
* @component
*
* @param {(JSX.Element|null)} description content displayed in a popup
* @param {JSX.Element} children the wrapped content
* @param {object} props
* @param {React.ReactElement} props.description - Content displayed in a popup.
* @param {React.ReactNode} props.children - The wrapped content.
* @param {import("@patternfly/react-core").PopoverProps} [props.otherProps]
*/
export default function Description ({ description, children, ...otherProps }) {
if (description) {
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/core/NumericTextInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* find current contact information at www.suse.com.
*/

// @ts-check

import React from "react";
import { TextInput } from "@patternfly/react-core";
import { noop } from "~/utils";
Expand All @@ -42,9 +44,7 @@ import { noop } from "~/utils";
* @param {object} props
* @param {string|number} props.value - the input value
* @param {onChangeFn} props.onChange - the callback to be called when the entered value match the input pattern
* @param {object} props.textInputProps - @see {@link https://www.patternfly.org/components/forms/text-input#props}
*
* @returns {ReactComponent}
* @param {import("@patternfly/react-core").TextInputProps} props.textInputProps
*/
export default function NumericTextInput({ value = "", onChange = noop, ...textInputProps }) {
// NOTE: Using \d* instead of \d+ at the beginning to allow empty
Expand Down
2 changes: 0 additions & 2 deletions web/src/components/core/RowActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ import { _ } from "~/i18n";
* @param {object} [props.rest]
*
* @typedef {import("@patternfly/react-table").IAction} Action
*
* @return {React.ActionsColumn}
*/
export default function RowActions({ id, actions, "aria-label": toggleAriaLabel, ...rest }) {
const actionsToggle = (props) => (
Expand Down
7 changes: 5 additions & 2 deletions web/src/components/core/Tip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* find current contact information at www.suse.com.
*/

// @ts-check

import React from "react";
import { Label } from "@patternfly/react-core";

Expand All @@ -31,8 +33,9 @@ import { Icon } from "~/components/layout";
* If the label is not defined or is empty it behaves like a plain label.
* @component
*
* @param {JSX.Element} description details displayed after clicking the label
* @param {JSX.Element} children the content of the label
* @param {object} props
* @param {React.ReactElement} props.description - Details displayed after clicking the label.
* @param {React.ReactNode} props.children - The content of the label.
*/
export default function Tip ({ description, children }) {
if (description) {
Expand Down
Loading
Loading