Skip to content

Commit

Permalink
Merge fb89fad into 76c930c
Browse files Browse the repository at this point in the history
  • Loading branch information
jreidinger committed Feb 26, 2024
2 parents 76c930c + fb89fad commit c2dcead
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 79 deletions.
76 changes: 76 additions & 0 deletions web/src/components/storage/ProposalSettingsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,65 @@ const EncryptionSettingsForm = ({
);
};

/**
* Allows to define snapshots enablement
* @component
*
* @param {object} props
* @param {ProposalSettings} props.settings - Settings used for calculating a proposal.
* @param {onChangeFn} [props.onChange=noop] - On change callback
*
* @callback onChangeFn
* @param {object} settings
*/
const SnapshotsField = ({
settings,
onChange = noop
}) => {
const rootVolume = (settings.volumes || []).find((i) => i.mountPath === "/");

const initialChecked = rootVolume !== undefined && rootVolume.fsType === "Btrfs" && rootVolume.snapshots;
const [isChecked, setIsChecked] = useState(initialChecked);

// no root volume is probably some error or still loading
if (rootVolume === undefined) {
return <Skeleton width="25%" />;
}

const switchState = (_, checked) => {
setIsChecked(checked);
onChange({ value: checked, settings });
};

const configurableSnapshots = rootVolume.outline.snapshotsConfigurable;
const forcedSnapshots = !configurableSnapshots && rootVolume.fsType === "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.");
return (
<>
<Switch
id="snapshots"
label={_("Use Btrfs Snapshots")}
isReversed
isChecked={isChecked}
onChange={switchState}
/>
<div>
{explanation}
</div>
</>
);
};

return (
<div>
<If condition={forcedSnapshots} then={_("Btrfs snapshots required by product.")} />
<If condition={configurableSnapshots} then={<SnapshotsToggle />} />
</div>
);
};

/**
* Allows to define encryption
* @component
Expand Down Expand Up @@ -237,11 +296,28 @@ export default function ProposalSettingsSection({
onChange({ encryptionPassword: password, encryptionMethod: method });
};

const changeBtrfsSnapshots = ({ value, settings }) => {
const rootVolume = settings.volumes.find((i) => i.mountPath === "/");

if (value) {
rootVolume.fsType = "Btrfs";
rootVolume.snapshots = true;
} else {
rootVolume.snapshots = false;
}

onChange({ volumes: settings.volumes });
};

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

return (
<>
<Section title={_("Settings")}>
<SnapshotsField
settings={settings}
onChange={changeBtrfsSnapshots}
/>
<EncryptionField
password={settings.encryptionPassword || ""}
method={settings.encryptionMethod}
Expand Down
86 changes: 16 additions & 70 deletions web/src/components/storage/VolumeForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,6 @@ const MountPointFormSelect = ({ value, volumes, onChange, ...selectProps }) => {
);
};

/**
* Btrfs file system type can be offered with two flavors, with and without snapshots.
*/
const BTRFS_WITH_SNAPSHOTS = "BtrfsWithSnapshots";

/**
* Possible file system type options for a volume.
* @function
Expand All @@ -136,40 +131,7 @@ const BTRFS_WITH_SNAPSHOTS = "BtrfsWithSnapshots";
* @returns {string[]}
*/
const fsOptions = (volume) => {
const options = (volume, fsType) => {
if (fsType !== "Btrfs") return fsType;

const { snapshotsConfigurable } = volume.outline;
if (snapshotsConfigurable) return [BTRFS_WITH_SNAPSHOTS, fsType];

return (volume.snapshots ? BTRFS_WITH_SNAPSHOTS : fsType);
};

return volume.outline.fsTypes.flatMap(fsType => options(volume, fsType));
};

/**
* File system option according to the given type and the value of snapshots.
* @function
*
* @param {FsValue} value
* @returns {string}
*/
const fsOption = ({ fsType, snapshots }) => {
if (fsType !== "Btrfs") return fsType;

return (snapshots ? BTRFS_WITH_SNAPSHOTS : fsType);
};

/**
* Label for a file system type option.
* @function
*
* @param {string} fsOption
* @returns {string}
*/
const fsOptionLabel = (fsOption) => {
return (fsOption === BTRFS_WITH_SNAPSHOTS ? sprintf("Btrfs %s", _("with snapshots")) : fsOption);
return volume.outline.fsTypes;
};

/**
Expand All @@ -180,10 +142,7 @@ const fsOptionLabel = (fsOption) => {
* @returns {FsValue}
*/
const fsValue = (fsOption) => {
if (fsOption === BTRFS_WITH_SNAPSHOTS)
return { fsType: "Btrfs", snapshots: true };
else
return { fsType: fsOption, snapshots: false };
return { fsType: fsOption, snapshots: false };
};

/**
Expand All @@ -196,27 +155,9 @@ const fsValue = (fsOption) => {
* @returns {ReactComponent}
*/
const FsSelectOption = ({ fsOption }) => {
const label = () => {
if (fsOption === BTRFS_WITH_SNAPSHOTS) return "Btrfs";
return fsOption;
};

const details = () => {
if (fsOption === BTRFS_WITH_SNAPSHOTS) return _("with snapshots");
return undefined;
};

const description = () => {
if (fsOption === BTRFS_WITH_SNAPSHOTS)
// TRANSLATORS: description of Btrfs snapshots feature.
return _("Allows rolling back any change done to the system and restoring its previous state");

return undefined;
};

return (
<SelectOption value={fsOption} description={description()}>
<strong>{label()}</strong> <If condition={details()} then={details()} />
<SelectOption value={fsOption}>
<strong>{fsOption}</strong>
</SelectOption>
);
};
Expand All @@ -237,7 +178,7 @@ const FsSelect = ({ id, value, volume, onChange }) => {
const [isOpen, setIsOpen] = useState(false);

const options = fsOptions(volume);
const selected = fsOption(value);
const selected = value;

const onToggleClick = () => {
setIsOpen(!isOpen);
Expand All @@ -257,7 +198,7 @@ const FsSelect = ({ id, value, volume, onChange }) => {
isExpanded={isOpen}
className="full-width"
>
{fsOptionLabel(selected)}
{selected}
</MenuToggle>
);
};
Expand Down Expand Up @@ -300,8 +241,13 @@ const FsSelect = ({ id, value, volume, onChange }) => {
*/
const FsField = ({ value, volume, onChange }) => {
const isSingleFs = () => {
const { fsTypes, snapshotsConfigurable } = volume.outline;
return fsTypes.length === 1 && (fsTypes[0] !== "Btrfs" || !snapshotsConfigurable);
// check for btrfs with snapshots
if (volume.fsType === "Btrfs" && volume.snapshots) {
return true;
}

const { fsTypes } = volume.outline;
return fsTypes.length === 1;
};

const Info = () => {
Expand Down Expand Up @@ -330,7 +276,7 @@ const FsField = ({ value, volume, onChange }) => {
condition={isSingleFs()}
then={
<FormGroup label={label}>
<p>{fsOptionLabel(fsOption(value))}</p>
<p>{value}</p>
</FormGroup>
}
else={
Expand Down Expand Up @@ -760,7 +706,7 @@ export default function VolumeForm({ id, volume: currentVolume, templates = [],
if (!Object.keys(errors).length) onSubmit(volume);
};

const { fsType, snapshots } = state.formData;
const { fsType } = state.formData;

const ShowMountPointSelector = () => (
<MountPointFormSelect
Expand Down Expand Up @@ -788,7 +734,7 @@ export default function VolumeForm({ id, volume: currentVolume, templates = [],
}
/>
<FsField
value={{ fsType, snapshots }}
value={fsType}
volume={state.volume}
onChange={updateData}
/>
Expand Down
35 changes: 26 additions & 9 deletions web/src/components/storage/VolumeForm.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ beforeEach(() => {
});

it("renders a control for displaying/selecting the file system type", async () => {
const { user } = plainRender(<VolumeForm {...props} />);
// use home which is not using snapshots
const { user } = plainRender(<VolumeForm volume={{ ...volumes.home }} />);

const fsTypeButton = screen.getByRole("button", { name: "File system type" });
await user.click(fsTypeButton);
screen.getByRole("option", { name: /Btrfs with snapshots/ });
screen.getByRole("option", { name: "Btrfs" });
screen.getByRole("option", { name: "XFS" });
screen.getByRole("option", { name: "Ext4" });
});

Expand All @@ -117,6 +117,24 @@ it("does not render the file system control if there is only one option", async
);
});

it("renders the file system control for root mount point without snapshots", async () => {
const { user } = plainRender(<VolumeForm volume={{ ...volumes.root, snapshots: false }} />);

const fsTypeButton = screen.getByRole("button", { name: "File system type" });
await user.click(fsTypeButton);
screen.getByRole("option", { name: "Btrfs" });
screen.getByRole("option", { name: "Ext4" });
});

it("does not render the file system control for root mount point with btrfs with snapshots", async () => {
plainRender(<VolumeForm {...props} />);

await screen.findByText("Btrfs");
await waitFor(() => (
expect(screen.queryByRole("button", { name: "File system type" })).not.toBeInTheDocument())
);
});

it("renders controls for setting the desired size", () => {
plainRender(<VolumeForm {...props} />);

Expand Down Expand Up @@ -179,17 +197,16 @@ it("calls the onSubmit callback with resulting volume when the form is submitted
await user.type(maxSizeInput, "25");
await user.selectOptions(maxSizeUnitSelector, maxSizeGiBUnit);

const fsTypeButton = screen.getByRole("button", { name: "File system type" });
await user.click(fsTypeButton);
const ext4Button = screen.getByRole("option", { name: "Ext4" });
await user.click(ext4Button);
// root is with btrfs and snapshots, so it is not possible to select it
await screen.findByText("Btrfs");
await waitFor(() => (
expect(screen.queryByRole("button", { name: "File system type" })).not.toBeInTheDocument())
);

await user.click(submitForm);

expect(onSubmitFn).toHaveBeenCalledWith({
...volumes.root,
fsType: "Ext4",
snapshots: false,
minSize: parseToBytes("10 GiB"),
maxSize: parseToBytes("25 GiB")
});
Expand Down

0 comments on commit c2dcead

Please sign in to comment.