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

Indicate transactional system #1063

Merged
merged 4 commits into from
Mar 1, 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 @@
-------------------------------------------------------------------
Fri Mar 1 10:56:35 UTC 2024 - José Iván López González <[email protected]>

- Indicate whether the system is transactional
(gh#openSUSE/agama/1063).

-------------------------------------------------------------------
Wed Feb 28 22:26:23 UTC 2024 - Balsa Asanovic <[email protected]>

Expand Down
7 changes: 7 additions & 0 deletions web/src/components/storage/ProposalPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ jest.mock("@patternfly/react-core", () => {
jest.mock("~/components/core/Sidebar", () => () => <div>Agama sidebar</div>);
jest.mock("~/components/storage/ProposalPageMenu", () => () => <div>ProposalPage Options</div>);

jest.mock("~/context/product", () => ({
...jest.requireActual("~/context/product"),
useProduct: () => ({
selectedProduct : { name: "Test" }
})
}));

const vda = {
sid: "59",
type: "disk",
Expand Down
33 changes: 28 additions & 5 deletions web/src/components/storage/ProposalSettingsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@

import React, { useEffect, useState } from "react";
import { Checkbox, Form, Skeleton, Switch, Tooltip } from "@patternfly/react-core";
import { sprintf } from "sprintf-js";

import { _ } from "~/i18n";
import { If, PasswordAndConfirmationInput, Section, Popup } from "~/components/core";
import { Icon } from "~/components/layout";
import { noop } from "~/utils";
import { hasFS } from "~/components/storage/utils";
import { hasFS, isTransactionalSystem } from "~/components/storage/utils";
import { useProduct } from "~/context/product";

/**
* @typedef {import ("~/client/storage").ProposalManager.ProposalSettings} ProposalSettings
Expand Down Expand Up @@ -147,7 +149,9 @@ const SnapshotsField = ({
const forcedSnapshots = !configurableSnapshots && hasFS(rootVolume, "Btrfs") && rootVolume.snapshots;

const SnapshotsToggle = () => {
const explanation = _("Uses Btrfs for the root file system allowing to boot to a previous version of the system after configuration changes or software upgrades.");
const explanation = _("Uses Btrfs for the root file system allowing to boot to a previous \
version of the system after configuration changes or software upgrades.");

return (
<>
<Switch
Expand Down Expand Up @@ -293,6 +297,8 @@ export default function ProposalSettingsSection({
encryptionMethods = [],
onChange = noop
}) {
const { selectedProduct } = useProduct();

const changeEncryption = ({ password, method }) => {
onChange({ encryptionPassword: password, encryptionMethod: method });
};
Expand All @@ -312,12 +318,29 @@ export default function ProposalSettingsSection({

const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0;

const transactional = isTransactionalSystem(settings?.volumes || []);

return (
<>
<Section title={_("Settings")}>
<SnapshotsField
settings={settings}
onChange={changeBtrfsSnapshots}
<If
condition={transactional}
then={
<div>
<label>{_("Transactional system")}</label>
<div>
{/* TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) */}
{sprintf(_("%s is an immutable system with atomic updates using a read-only Btrfs \
root file system."), selectedProduct.name)}
</div>
</div>
Comment on lines +329 to +336
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for a follow-up, create a component for this kind of structure (two rows, one for explanation). This can be done when swapping the position of the switchers to have them aligned.

}
else={
<SnapshotsField
settings={settings}
onChange={changeBtrfsSnapshots}
/>
}
/>
<EncryptionField
password={settings.encryptionPassword || ""}
Expand Down
33 changes: 33 additions & 0 deletions web/src/components/storage/ProposalSettingsSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,45 @@ jest.mock("@patternfly/react-core", () => {
};
});

jest.mock("~/context/product", () => ({
...jest.requireActual("~/context/product"),
useProduct: () => ({
selectedProduct : { name: "Test" }
})
}));

let props;

beforeEach(() => {
props = {};
});

const rootVolume = { mountPath: "/", fsType: "Btrfs", outline: { snapshotsConfigurable: true } };

describe("if the system is not transactional", () => {
beforeEach(() => {
props.settings = { volumes: [rootVolume] };
});

it("renders the snapshots switch", () => {
plainRender(<ProposalSettingsSection {...props} />);

screen.getByRole("checkbox", { name: "Use Btrfs Snapshots" });
});
});

describe("if the system is transactional", () => {
beforeEach(() => {
props.settings = { volumes: [{ ...rootVolume, transactional: true }] };
});

it("renders explanation about transactional system", () => {
plainRender(<ProposalSettingsSection {...props} />);

screen.getByText("Transactional system");
});
});

describe("Encryption field", () => {
describe("if encryption password setting is not set yet", () => {
beforeEach(() => {
Expand Down
10 changes: 5 additions & 5 deletions web/src/components/storage/ProposalVolumes.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022-2023] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -34,7 +34,7 @@ import { _ } from "~/i18n";
import { Em, If, Popup, RowActions, Tip } from '~/components/core';
import { Icon } from '~/components/layout';
import { VolumeForm } from '~/components/storage';
import { deviceSize } from '~/components/storage/utils';
import { deviceSize, hasSnapshots, isTransactionalRoot } from '~/components/storage/utils';
import { noop } from "~/utils";

/**
Expand Down Expand Up @@ -200,8 +200,8 @@ const VolumeRow = ({ columns, volume, options, isLoading, onEdit, onDelete }) =>
};

const Details = ({ volume, options }) => {
const hasSnapshots = volume.fsType === "Btrfs" && volume.snapshots;
const transactional = volume.fsType === "Btrfs" && volume.transactional;
const snapshots = hasSnapshots(volume);
const transactional = isTransactionalRoot(volume);

// TRANSLATORS: the filesystem uses a logical volume (LVM)
const text = `${volume.fsType} ${options.lvm ? _("logical volume") : _("partition")}`;
Expand All @@ -215,7 +215,7 @@ const VolumeRow = ({ columns, volume, options, isLoading, onEdit, onDelete }) =>
{/* TRANSLATORS: filesystem flag, it uses an encryption */}
<If condition={options.encryption} then={<Em icon={lockIcon}>{_("encrypted")}</Em>} />
{/* TRANSLATORS: filesystem flag, it allows creating snapshots */}
<If condition={hasSnapshots} then={<Em icon={snapshotsIcon}>{_("with snapshots")}</Em>} />
<If condition={snapshots && !transactional} then={<Em icon={snapshotsIcon}>{_("with snapshots")}</Em>} />
{/* TRANSLATORS: flag for transactional file system */}
<If condition={transactional} then={<Em icon={transactionalIcon}>{_("transactional")}</Em>} />
</div>
Expand Down
22 changes: 4 additions & 18 deletions web/src/components/storage/ProposalVolumes.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022-2023] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -217,9 +217,9 @@ describe("if there are volumes", () => {
within(popup).getByText("Edit file system");
});

describe("and there is transactional Btrfs volume", () => {
describe("and there is transactional Btrfs root volume", () => {
beforeEach(() => {
props.volumes = [{ ...volumes.root, transactional: true }];
props.volumes = [{ ...volumes.root, snapshots: true, transactional: true }];
});

it("renders 'transactional' legend as part of its information", async () => {
Expand All @@ -233,7 +233,7 @@ describe("if there are volumes", () => {

describe("and there is Btrfs volume using snapshots", () => {
beforeEach(() => {
props.volumes = [{ ...volumes.root, snapshots: true }];
props.volumes = [{ ...volumes.root, snapshots: true, transactional: false }];
});

it("renders 'with snapshots' legend as part of its information", async () => {
Expand All @@ -244,20 +244,6 @@ describe("if there are volumes", () => {
within(volumes).getByRole("row", { name: "/ Btrfs partition with snapshots 1 KiB - 2 KiB" });
});
});

describe("and there is a transactional Btrfs volume using snapshots", () => {
beforeEach(() => {
props.volumes = [{ ...volumes.root, transactional: true, snapshots: true }];
});

it("renders 'with snapshots' and 'transactional' legends as part of its information", async () => {
plainRender(<ProposalVolumes {...props} />);

const [, volumes] = await screen.findAllByRole("rowgroup");

within(volumes).getByRole("row", { name: "/ Btrfs partition with snapshots transactional 1 KiB - 2 KiB" });
});
});
});

describe("if there are not volumes", () => {
Expand Down
46 changes: 42 additions & 4 deletions web/src/components/storage/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2023] SUSE LLC
* Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -25,6 +25,11 @@ import xbytes from "xbytes";

import { N_ } from "~/i18n";

/**
* @typedef {import ("~/client/storage").Volume} Volume
* @typedef {import ("~/clients/storage").StorageDevice} StorageDevice
*/

/**
* @typedef {Object} SizeObject
*
Expand Down Expand Up @@ -125,7 +130,7 @@ const parseToBytes = (size) => {
/**
* Generates the label for the given device
*
* @param {import(~/clients/storage).StorageDevice} device
* @param {StorageDevice} device
* @returns {string}
*/
const deviceLabel = (device) => {
Expand All @@ -139,7 +144,7 @@ const deviceLabel = (device) => {
* Checks if volume uses given fs. This method works same as in backend
* case insensitive.
*
* @param {import(~/clients/storage).Volume} volume
* @param {Volume} volume
* @param {string} fs - Filesystem name to check.
* @returns {boolean} true when volume uses given fs
*/
Expand All @@ -149,6 +154,36 @@ const hasFS = (volume, fs) => {
return volFS.toLowerCase() === fs.toLocaleLowerCase();
};

/**
* Checks whether the given volume has snapshots.
*
* @param {Volume} volume
* @returns {boolean}
*/
const hasSnapshots = (volume) => {
return hasFS(volume, "btrfs") && volume.snapshots;
};

/**
* Checks whether the given volume defines a transactional root.
*
* @param {Volume} volume
* @returns {boolean}
*/
const isTransactionalRoot = (volume) => {
return volume.mountPath === "/" && volume.transactional;
};

/**
* Checks whether the given volumes defines a transactional system.
*
* @param {Volume[]} volumes
* @returns {boolean}
*/
const isTransactionalSystem = (volumes) => {
return volumes.find(v => isTransactionalRoot(v)) !== undefined;
};

export {
DEFAULT_SIZE_UNIT,
SIZE_METHODS,
Expand All @@ -157,5 +192,8 @@ export {
deviceSize,
parseToBytes,
splitSize,
hasFS
hasFS,
hasSnapshots,
isTransactionalRoot,
isTransactionalSystem
};
61 changes: 59 additions & 2 deletions web/src/components/storage/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2023] SUSE LLC
* Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -19,7 +19,16 @@
* find current contact information at www.suse.com.
*/

import { deviceSize, deviceLabel, parseToBytes, splitSize, hasFS } from "./utils";
import {
deviceSize,
deviceLabel,
parseToBytes,
splitSize,
hasFS,
hasSnapshots,
isTransactionalRoot,
isTransactionalSystem
} from "./utils";

describe("deviceSize", () => {
it("returns the size with units", () => {
Expand Down Expand Up @@ -100,3 +109,51 @@ describe("hasFS", () => {
expect(hasFS({ fsType: "Btrfs" }, "EXT4")).toBe(false);
});
});

describe("hasSnapshots", () => {
it("returns false if the volume has not Btrfs file system", () => {
expect(hasSnapshots({ fsType: "EXT4", snapshots: true })).toBe(false);
});

it("returns false if the volume has not snapshots enabled", () => {
expect(hasSnapshots({ fsType: "Btrfs", snapshots: false })).toBe(false);
});

it("returns true if the volume has Btrfs file system and snapshots enabled", () => {
expect(hasSnapshots({ fsType: "Btrfs", snapshots: true })).toBe(true);
});
});

describe("isTransactionalRoot", () => {
it("returns false if the volume is not root", () => {
expect(isTransactionalRoot({ mountPath: "/home", transactional: true })).toBe(false);
});

it("returns false if the volume has not transactional enabled", () => {
expect(isTransactionalRoot({ mountPath: "/", transactional: false })).toBe(false);
});

it("returns true if the volume is root and has transactional enabled", () => {
expect(isTransactionalRoot({ mountPath: "/", transactional: true })).toBe(true);
});
});

describe("isTransactionalSystem", () => {
it("returns false if volumes does not include a transactional root", () => {
expect(isTransactionalSystem([])).toBe(false);

const volumes = [
{ mountPath: "/" },
{ mountPath: "/home", transactional: true }
];
expect(isTransactionalSystem(volumes)).toBe(false);
});

it("returns true if volumes includes a transactional root", () => {
const volumes = [
{ mountPath: "EXT4" },
{ mountPath: "/", transactional: true }
];
expect(isTransactionalSystem(volumes)).toBe(true);
});
});
Loading