Skip to content

Commit

Permalink
feat(web): allow changing boot options
Browse files Browse the repository at this point in the history
  • Loading branch information
joseivanlopez committed Dec 19, 2024
1 parent d807efe commit cbf0d39
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 116 deletions.
2 changes: 1 addition & 1 deletion web/src/components/storage/BootConfigField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { STORAGE as PATHS } from "~/routes/paths";
const Link = ({ isBold = false }: { isBold?: boolean }) => {
const text = _("Change boot options");

return <RouterLink to={PATHS.bootingPartition}>{isBold ? <b>{text}</b> : text}</RouterLink>;
return <RouterLink to={PATHS.bootDevice}>{isBold ? <b>{text}</b> : text}</RouterLink>;
};

export type BootConfig = {
Expand Down
149 changes: 66 additions & 83 deletions web/src/components/storage/BootSelection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@

import React from "react";
import { screen, within } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { mockNavigateFn, plainRender } from "~/test-utils";
import BootSelection from "./BootSelection";
import { StorageDevice } from "~/types/storage";
import { BootHook } from "~/queries/storage/config-model";

const sda: StorageDevice = {
sid: 59,
Expand Down Expand Up @@ -94,50 +95,62 @@ const sdc: StorageDevice = {
udevPaths: ["pci-0000:00-19"],
};

let props;

describe.skip("BootSelection", () => {
beforeEach(() => {
props = {
isOpen: true,
configureBoot: false,
availableDevices: [sda, sdb, sdc],
bootDevice: undefined,
defaultBootDevice: undefined,
onCancel: jest.fn(),
onAccept: jest.fn(),
};
});
const mockAvailableDevices = [sda, sdb, sdc];

const mockBoot: BootHook = {
configure: false,
isDefault: false,
deviceName: undefined,
setDevice: jest.fn(),
setDefault: jest.fn(),
disable: jest.fn(),
};

jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockNavigateFn,
}));

jest.mock("~/queries/storage", () => ({
...jest.requireActual("~/queries/storage"),
useAvailableDevices: () => mockAvailableDevices,
}));

jest.mock("~/queries/storage/config-model", () => ({
...jest.requireActual("~/queries/storage/config-model"),
useBoot: () => mockBoot,
}));

describe("BootSelection", () => {
const automaticOption = () => screen.queryByRole("radio", { name: "Automatic" });
const selectDiskOption = () => screen.queryByRole("radio", { name: "Select a disk" });
const notConfigureOption = () => screen.queryByRole("radio", { name: "Do not configure" });
const diskSelector = () => screen.queryByRole("combobox", { name: /choose a disk/i });

it("offers an option to configure boot in the installation disk", () => {
plainRender(<BootSelection {...props} />);
plainRender(<BootSelection />);
expect(automaticOption()).toBeInTheDocument();
});

it("offers an option to configure boot in a selected disk", () => {
plainRender(<BootSelection {...props} />);
plainRender(<BootSelection />);
expect(selectDiskOption()).toBeInTheDocument();
expect(diskSelector()).toBeInTheDocument();
});

it("offers an option to not configure boot", () => {
plainRender(<BootSelection {...props} />);
plainRender(<BootSelection />);
expect(notConfigureOption()).toBeInTheDocument();
});

describe("if the current value is set to boot from the installation disk", () => {
beforeEach(() => {
props.configureBoot = true;
props.bootDevice = undefined;
mockBoot.configure = true;
mockBoot.isDefault = true;
});

it("selects 'Automatic' option by default", () => {
plainRender(<BootSelection {...props} />);
plainRender(<BootSelection />);
expect(automaticOption()).toBeChecked();
expect(selectDiskOption()).not.toBeChecked();
expect(diskSelector()).toBeDisabled();
Expand All @@ -147,12 +160,13 @@ describe.skip("BootSelection", () => {

describe("if the current value is set to boot from a selected disk", () => {
beforeEach(() => {
props.configureBoot = true;
props.bootDevice = sdb;
mockBoot.configure = true;
mockBoot.isDefault = false;
mockBoot.deviceName = sda.name;
});

it("selects 'Select a disk' option by default", () => {
plainRender(<BootSelection {...props} />);
plainRender(<BootSelection />);
expect(automaticOption()).not.toBeChecked();
expect(selectDiskOption()).toBeChecked();
expect(diskSelector()).toBeEnabled();
Expand All @@ -162,91 +176,60 @@ describe.skip("BootSelection", () => {

describe("if the current value is set to not configure boot", () => {
beforeEach(() => {
props.configureBoot = false;
props.bootDevice = sdb;
mockBoot.configure = false;
});

it("selects 'Do not configure' option by default", () => {
plainRender(<BootSelection {...props} />);
plainRender(<BootSelection />);
expect(automaticOption()).not.toBeChecked();
expect(selectDiskOption()).not.toBeChecked();
expect(diskSelector()).toBeDisabled();
expect(notConfigureOption()).toBeChecked();
});
});

it("does not call onAccept on cancel", async () => {
const { user } = plainRender(<BootSelection {...props} />);
it("does not change the boot options on cancel", async () => {
const { user } = plainRender(<BootSelection />);
const cancel = screen.getByRole("button", { name: "Cancel" });

await user.click(cancel);

expect(props.onAccept).not.toHaveBeenCalled();
expect(mockBoot.setDevice).not.toHaveBeenCalled();
expect(mockBoot.setDefault).not.toHaveBeenCalled();
expect(mockBoot.disable).not.toHaveBeenCalled();
});

describe("if the 'Automatic' option is selected", () => {
beforeEach(() => {
props.configureBoot = false;
props.bootDevice = undefined;
});

it("calls onAccept with the selected options on accept", async () => {
const { user } = plainRender(<BootSelection {...props} />);

await user.click(automaticOption());
it("applies the expected boot options when 'Automatic' is selected", async () => {
const { user } = plainRender(<BootSelection />);
await user.click(automaticOption());

const accept = screen.getByRole("button", { name: "Confirm" });
await user.click(accept);
const accept = screen.getByRole("button", { name: "Accept" });
await user.click(accept);

expect(props.onAccept).toHaveBeenCalledWith({
configureBoot: true,
bootDevice: undefined,
});
});
expect(mockBoot.setDefault).toHaveBeenCalled();
});

describe("if the 'Select a disk' option is selected", () => {
beforeEach(() => {
props.configureBoot = false;
props.bootDevice = undefined;
});
it("applies the expected boot options when a disk is selected", async () => {
const { user } = plainRender(<BootSelection />);

it("calls onAccept with the selected options on accept", async () => {
const { user } = plainRender(<BootSelection {...props} />);
await user.click(selectDiskOption());
const selector = diskSelector();
const sdbOption = within(selector).getByRole("option", { name: /sdb/ });
await user.selectOptions(selector, sdbOption);

await user.click(selectDiskOption());
const selector = diskSelector();
const sdbOption = within(selector).getByRole("option", { name: /sdb/ });
await user.selectOptions(selector, sdbOption);
const accept = screen.getByRole("button", { name: "Accept" });
await user.click(accept);

const accept = screen.getByRole("button", { name: "Confirm" });
await user.click(accept);

expect(props.onAccept).toHaveBeenCalledWith({
configureBoot: true,
bootDevice: sdb,
});
});
expect(mockBoot.setDevice).toHaveBeenCalledWith(sdb.name);
});

describe("if the 'Do not configure' option is selected", () => {
beforeEach(() => {
props.configureBoot = true;
props.bootDevice = sda;
});

it("calls onAccept with the selected options on accept", async () => {
const { user } = plainRender(<BootSelection {...props} />);
it("applies the expected boot options when 'No configure' is selected", async () => {
const { user } = plainRender(<BootSelection />);
await user.click(notConfigureOption());

await user.click(notConfigureOption());
const accept = screen.getByRole("button", { name: "Accept" });
await user.click(accept);

const accept = screen.getByRole("button", { name: "Confirm" });
await user.click(accept);

expect(props.onAccept).toHaveBeenCalledWith({
configureBoot: false,
bootDevice: undefined,
});
});
expect(mockBoot.disable).toHaveBeenCalled();
});
});
64 changes: 35 additions & 29 deletions web/src/components/storage/BootSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ import { DevicesFormSelect } from "~/components/storage";
import { Page } from "~/components/core";
import { deviceLabel } from "~/components/storage/utils";
import { StorageDevice } from "~/types/storage";
import { useAvailableDevices, useProposalMutation, useProposalResult } from "~/queries/storage";
import { useAvailableDevices } from "~/queries/storage";
import textStyles from "@patternfly/react-styles/css/utilities/Text/text";
import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
import { useBoot } from "~/queries/storage/config-model";

// FIXME: improve classNames
// FIXME: improve and rename to BootSelectionDialog
Expand All @@ -39,50 +40,49 @@ const BOOT_AUTO_ID = "boot-auto";
const BOOT_MANUAL_ID = "boot-manual";
const BOOT_DISABLED_ID = "boot-disabled";

type BootSelectionState = {
load: boolean;
selectedOption?: string;
configureBoot?: boolean;
bootDevice?: StorageDevice;
defaultBootDevice?: StorageDevice;
availableDevices?: StorageDevice[];
};

/**
* Allows the user to select the boot configuration.
*/
export default function BootSelectionDialog() {
type BootSelectionState = {
load: boolean;
selectedOption?: string;
configureBoot?: boolean;
bootDevice?: StorageDevice;
defaultBootDevice?: StorageDevice;
availableDevices?: StorageDevice[];
};

const [state, setState] = useState<BootSelectionState>({ load: false });
const { settings } = useProposalResult();
const availableDevices = useAvailableDevices();
const updateProposal = useProposalMutation();
const navigate = useNavigate();
const boot = useBoot();

useEffect(() => {
if (state.load) return;

let selectedOption: string;
const { bootDevice, configureBoot, defaultBootDevice } = settings;

if (!configureBoot) {
if (!boot.configure) {
selectedOption = BOOT_DISABLED_ID;
} else if (configureBoot && bootDevice === "") {
} else if (boot.isDefault) {
selectedOption = BOOT_AUTO_ID;
} else {
selectedOption = BOOT_MANUAL_ID;
}

const findDevice = (name: string) => availableDevices.find((d) => d.name === name);
const bootDevice = availableDevices.find((d) => d.name === boot.deviceName);
const defaultBootDevice = boot.isDefault ? bootDevice : undefined;

setState({
load: true,
bootDevice: findDevice(bootDevice) || findDevice(defaultBootDevice) || availableDevices[0],
configureBoot,
defaultBootDevice: findDevice(defaultBootDevice),
bootDevice: bootDevice || availableDevices[0],
configureBoot: boot.configure,
defaultBootDevice,
availableDevices,
selectedOption,
});
}, [availableDevices, settings, state.load]);
}, [availableDevices, boot, state.load]);

if (!state.load) return;

Expand All @@ -92,12 +92,18 @@ export default function BootSelectionDialog() {
// const formData = new FormData(e.target);
// const mode = formData.get("bootMode");
// const device = formData.get("bootDevice");
const newSettings = {
configureBoot: state.selectedOption !== BOOT_DISABLED_ID,
bootDevice: state.selectedOption === BOOT_MANUAL_ID ? state.bootDevice.name : undefined,
};

await updateProposal.mutateAsync({ ...settings, ...newSettings });
switch (state.selectedOption) {
case BOOT_DISABLED_ID:
boot.disable();
break;
case BOOT_AUTO_ID:
boot.setDefault();
break;
default:
boot.setDevice(state.bootDevice?.name);
}

navigate("..");
};

Expand Down Expand Up @@ -126,20 +132,20 @@ partitions in the appropriate disk.",
setState({ ...state, selectedOption: e.target.value });
};

const setBootDevice = (v) => {
const changeBootDevice = (v) => {
setState({ ...state, bootDevice: v });
};

return (
<Page>
<Page.Header>
<h2>{_("Select booting partition")}</h2>
<h2>{_("Boot options")}</h2>
<p className={textStyles.color_400}>{description}</p>
</Page.Header>

<Page.Content>
<Form id="bootSelectionForm" onSubmit={onSubmit}>
<Page.Section>
<Page.Section aria-label={_("Select a boot option")}>
<FormGroup isStack>
<Radio
name="bootMode"
Expand Down Expand Up @@ -183,7 +189,7 @@ partitions in the appropriate disk.",
name="bootDevice"
devices={state?.availableDevices || []}
selectedDevice={state.bootDevice}
onChange={setBootDevice}
onChange={changeBootDevice}
isDisabled={state.selectedOption !== BOOT_MANUAL_ID}
/>
</Stack>
Expand Down
Loading

0 comments on commit cbf0d39

Please sign in to comment.