Skip to content

Commit

Permalink
web: Move storage/BootConfigField to its own file
Browse files Browse the repository at this point in the history
And adapts it to looks a bit different according the changes proposed in
the context of https://trello.com/c/czpTfm3y (internal link).

It enables type checking in the storage/BootSelectionDialog.test.jsx
too.
  • Loading branch information
dgdavid committed Apr 10, 2024
1 parent 7111693 commit 37d70b9
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 76 deletions.
8 changes: 8 additions & 0 deletions web/src/assets/styles/utilities.scss
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@
padding: 0;
}

.inline-flex-button{
@extend .plain-button;
display: inline-flex;
align-items: center;
gap: 0.7ch;
text-decoration: underline;
}

.p-0 {
padding: 0;
}
Expand Down
4 changes: 4 additions & 0 deletions web/src/components/layout/Icon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import EditSquare from "@icons/edit_square.svg?component";
import Error from "@icons/error.svg?component";
import ExpandAll from "@icons/expand_all.svg?component";
import ExpandMore from "@icons/expand_more.svg?component";
import Feedback from "@icons/feedback.svg?component";
import Folder from "@icons/folder.svg?component";
import FolderOff from "@icons/folder_off.svg?component";
import Globe from "@icons/globe.svg?component";
Expand All @@ -61,6 +62,7 @@ import Schedule from "@icons/schedule.svg?component";
import SettingsApplications from "@icons/settings_applications.svg?component";
import SettingsEthernet from "@icons/settings_ethernet.svg?component";
import SettingsFill from "@icons/settings-fill.svg?component";
import Shadow from "@icons/shadow.svg?component";
import SignalCellularAlt from "@icons/signal_cellular_alt.svg?component";
import Storage from "@icons/storage.svg?component";
import Sync from "@icons/sync.svg?component";
Expand Down Expand Up @@ -104,6 +106,7 @@ const icons = {
error: Error,
expand_all: ExpandAll,
expand_more: ExpandMore,
feedback: Feedback,
folder: Folder,
folder_off: FolderOff,
globe: Globe,
Expand All @@ -128,6 +131,7 @@ const icons = {
settings: SettingsFill,
settings_applications: SettingsApplications,
settings_ethernet: SettingsEthernet,
shadow: Shadow,
signal_cellular_alt: SignalCellularAlt,
storage: Storage,
sync: Sync,
Expand Down
124 changes: 124 additions & 0 deletions web/src/components/storage/BootConfigField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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.
*/

// @ts-check

import React, { useState } from "react";
import { Skeleton } from "@patternfly/react-core";

import { _ } from "~/i18n";
import { sprintf } from "sprintf-js";
import { deviceLabel } from "~/components/storage/utils";
import { If } from "~/components/core";
import { Icon } from "~/components/layout";
import BootSelectionDialog from "~/components/storage/BootSelectionDialog";

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

/**
* Internal component for building the button that opens the dialog
*
* @param {object} props
* @param {boolean} [props.isBold=false] - Whether text should be wrapped by <b>.
* @param {() => void} props.onClick - Callback to trigger when user clicks.
*/
const Button = ({ isBold = false, onClick }) => {
const text = _("Change boot options");

return (
<button onClick={onClick} className="inline-flex-button">
{isBold ? <b>{text}</b> : text} <Icon name="shadow" size="xxs" />
</button>
);
};

/**
* Allows to select the boot config.
* @component
*
* @param {object} props
* @param {boolean} props.configureBoot
* @param {StorageDevice|undefined} props.bootDevice
* @param {StorageDevice|undefined} props.defaultBootDevice
* @param {StorageDevice[]} props.devices
* @param {boolean} props.isLoading
* @param {(boot: Boot) => void} props.onChange
*
* @typedef {object} Boot
* @property {boolean} configureBoot
* @property {StorageDevice} bootDevice
*/
export default function BootConfigField ({
configureBoot,
bootDevice,
defaultBootDevice,
devices,
isLoading,
onChange
}) {
const [isDialogOpen, setIsDialogOpen] = useState(false);

const openDialog = () => setIsDialogOpen(true);

const closeDialog = () => setIsDialogOpen(false);

const onAccept = ({ configureBoot, bootDevice }) => {
closeDialog();
onChange({ configureBoot, bootDevice });
};

if (isLoading) {
return <Skeleton screenreaderText={_("Waiting for information about boot config")} width="75%" />;
}

let value;

if (!configureBoot) {
value = <><Icon name="feedback" size="xs" /> {_("Installation will not create boot partitions.")}</>;
} else if (!bootDevice) {
value = _("Installation might create boot partitions at the installation device.");
} else {
// TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB)
value = sprintf(_("Installation might create boot partitions at %s."), deviceLabel(bootDevice));
}

return (
<div>
{ value } <Button onClick={openDialog} isBold={!configureBoot} />
<If
condition={isDialogOpen}
then={
<BootSelectionDialog
isOpen
configureBoot={configureBoot}
bootDevice={bootDevice}
defaultBootDevice={defaultBootDevice}
devices={devices}
onAccept={onAccept}
onCancel={closeDialog}
/>
}
/>
</div>
);
}
112 changes: 112 additions & 0 deletions web/src/components/storage/BootConfigField.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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.
*/

// @ts-check

import React from "react";
import { screen, within } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import BootConfigField from "~/components/storage/BootConfigField";

const sda = {
sid: 59,
description: "A fake disk for testing",
isDrive: true,
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"],
};

let props;

beforeEach(() => {
props = {
configureBoot: false,
bootDevice: undefined,
defaultBootDevice: undefined,
devices: [sda],
isLoading: false,
onChange: jest.fn()
};
});

/**
* Helper function that implicitly test that field provides a button for
* opening the dialog
*/
const openBootConfigDialog = async () => {
const { user } = plainRender(<BootConfigField {...props} />);
const button = screen.getByRole("button");
await user.click(button);
const dialog = screen.getByRole("dialog", { name: "Partitions for booting" });

return { user, dialog };
};

describe("BootConfigField", () => {
it("triggers onChange callback when user confirms the dialog", async () => {
const { user, dialog } = await openBootConfigDialog();
const button = within(dialog).getByRole("button", { name: "Confirm" });
await user.click(button);
expect(props.onChange).toHaveBeenCalled();
});

it("does not trigger onChange callback when user cancels the dialog", async () => {
const { user, dialog } = await openBootConfigDialog();
const button = within(dialog).getByRole("button", { name: "Cancel" });
await user.click(button);
expect(props.onChange).not.toHaveBeenCalled();
});

describe("when installation is set for not configuring boot", () => {
it("renders a text warning about it", () => {
plainRender(<BootConfigField {...props} />);
screen.getByText(/will not create boot partitions/);
});
});

describe("when installation is set for automatically configuring boot", () => {
it("renders a text reporting about it", () => {
plainRender(<BootConfigField {...props} configureBoot />);
screen.getByText(/create boot partitions at the installation device/);
});
});

describe("when installation is set for configuring boot at specific device", () => {
it("renders a text reporting about it", () => {
plainRender(<BootConfigField {...props} configureBoot bootDevice={sda} />);
screen.getByText(/boot partitions at \/dev\/sda/);
});
});
});
2 changes: 2 additions & 0 deletions web/src/components/storage/BootSelectionDialog.test.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 { screen, within } from "@testing-library/react";
import { plainRender } from "~/test-utils";
Expand Down
80 changes: 4 additions & 76 deletions web/src/components/storage/ProposalSettingsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@
// @ts-check

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

import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
import { BootSelectionDialog, ProposalVolumes } from "~/components/storage";
import { ProposalVolumes } from "~/components/storage";
import SpacePolicyField from "~/components/storage/SpacePolicyField";
import BootConfigField from "~/components/storage/BootConfigField";
import { If, PasswordAndConfirmationInput, Section, Popup } from "~/components/core";
import { Icon } from "~/components/layout";
import { noop } from "~/utils";
import { hasFS, deviceLabel, SPACE_POLICIES } from "~/components/storage/utils";
import { hasFS, SPACE_POLICIES } from "~/components/storage/utils";

/**
* @typedef {import ("~/client/storage").ProposalSettings} ProposalSettings
Expand Down Expand Up @@ -266,78 +266,6 @@ const EncryptionField = ({
);
};

/**
* Allows to select the boot config.
* @component
*
* @param {object} props
* @param {boolean} props.configureBoot
* @param {StorageDevice|undefined} props.bootDevice
* @param {StorageDevice|undefined} props.defaultBootDevice
* @param {StorageDevice[]} props.devices
* @param {boolean} props.isLoading
* @param {(boot: Boot) => void} props.onChange
*
* @typedef {object} Boot
* @property {boolean} configureBoot
* @property {StorageDevice} bootDevice
*/
const BootConfigField = ({
configureBoot,
bootDevice,
defaultBootDevice,
devices,
isLoading,
onChange
}) => {
const [isDialogOpen, setIsDialogOpen] = useState(false);

const openDialog = () => setIsDialogOpen(true);

const closeDialog = () => setIsDialogOpen(false);

const onAccept = ({ configureBoot, bootDevice }) => {
closeDialog();
onChange({ configureBoot, bootDevice });
};

const label = _("Automatically configure any additional partition to boot the system");

const value = () => {
if (!configureBoot) return _("nowhere (manual boot setup)");

if (!bootDevice) return _("at the installation device");

// TRANSLATORS: %s is the disk used to configure the boot-related partitions (eg. "/dev/sda, 80 GiB)
return sprintf(_("at %s"), deviceLabel(bootDevice));
};

if (isLoading) {
return <Skeleton screenreaderText={_("Waiting for information about boot config")} width="25%" />;
}

return (
<div className="split">
<span>{label}</span>
<Button variant="link" isInline onClick={openDialog}>{value()}</Button>
<If
condition={isDialogOpen}
then={
<BootSelectionDialog
isOpen
configureBoot={configureBoot}
bootDevice={bootDevice}
defaultBootDevice={defaultBootDevice}
devices={devices}
onAccept={onAccept}
onCancel={closeDialog}
/>
}
/>
</div>
);
};

/**
* Section for editing the proposal settings
* @component
Expand Down

0 comments on commit 37d70b9

Please sign in to comment.