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

refactor(web): convert some storage components to TypeScript (round 2) #1583

Merged
merged 14 commits into from
Sep 6, 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,13 @@
* find current contact information at www.suse.com.
*/

// @ts-check

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

/**
* @typedef {import("~/components/storage/BootConfigField").BootConfigFieldProps} BootConfigFieldProps
* @typedef {import ("~/client/storage").StorageDevice} StorageDevice
*/
import BootConfigField, { BootConfigFieldProps } from "~/components/storage/BootConfigField";
import { StorageDevice } from "~/types/storage";

/** @type {StorageDevice} */
const sda = {
const sda: StorageDevice = {
sid: 59,
description: "A fake disk for testing",
isDrive: true,
Expand All @@ -54,48 +47,15 @@ const sda = {
udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"],
};

/** @type {BootConfigFieldProps} */
let props;

beforeEach(() => {
props = {
configureBoot: false,
bootDevice: undefined,
defaultBootDevice: undefined,
availableDevices: [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 };
const props: BootConfigFieldProps = {
configureBoot: false,
bootDevice: undefined,
defaultBootDevice: undefined,
availableDevices: [sda],
isLoading: false,
};

imobachgs marked this conversation as resolved.
Show resolved Hide resolved
describe.skip("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} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,51 +29,43 @@ import { sprintf } from "sprintf-js";
import { deviceLabel } from "~/components/storage/utils";
import { Icon } from "~/components/layout";
import { PATHS } from "~/routes/storage";

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

/**
* Internal component for building the link that navigates to selector
*
* @param {object} props
* @param {boolean} [props.isBold=false] - Whether text should be wrapped by <b>.
* @param props
* @param [props.isBold=false] - Whether text should be wrapped by <b>.
*/
const Link = ({ isBold = false }) => {
const Link = ({ isBold = false }: { isBold?: boolean; }) => {
const text = _("Change boot options");

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

export type BootConfig = {
configureBoot: boolean;
bootDevice: StorageDevice;
}

export type BootConfigFieldProps = {
configureBoot: boolean;
bootDevice?: StorageDevice;
defaultBootDevice?: StorageDevice;
availableDevices: StorageDevice[];
isLoading: boolean;
}

/**
* Allows to select the boot config.
* Summarizes how the system will boot.
* @component
*
* @typedef {object} BootConfigFieldProps
* @property {boolean} configureBoot
* @property {StorageDevice|undefined} bootDevice
* @property {StorageDevice|undefined} defaultBootDevice
* @property {StorageDevice[]} availableDevices
* @property {boolean} isLoading
* @property {(boot: BootConfig) => void} onChange
*
* @typedef {object} BootConfig
* @property {boolean} configureBoot
* @property {StorageDevice} bootDevice
*
* @param {BootConfigFieldProps} props
*/
export default function BootConfigField({ configureBoot, bootDevice, isLoading, onChange }) {
const onAccept = ({ configureBoot, bootDevice }) => {
onChange({ configureBoot, bootDevice });
};

export default function BootConfigField({ configureBoot, bootDevice, isLoading }: BootConfigFieldProps) {
if (isLoading && configureBoot === undefined) {
return <Skeleton width="75%" />;
}

let value;
let value: React.ReactNode;

if (!configureBoot) {
value = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,18 @@ import { Icon } from "~/components/layout";
import { _ } from "~/i18n";
import { sprintf } from "sprintf-js";
import { deviceBaseName } from "~/components/storage/utils";

/**
* @typedef {import("../core/ExpandableSelector").ExpandableSelectorColumn} ExpandableSelectorColumn
* @typedef {import("../core/ExpandableSelector").ExpandableSelectorProps} ExpandableSelectorProps
* @typedef {import("~/client/storage").PartitionSlot} PartitionSlot
* @typedef {import ("~/client/storage").StorageDevice} StorageDevice
*/
import { PartitionSlot, StorageDevice } from "~/types/storage";
import { ExpandableSelectorColumn, ExpandableSelectorProps } from "../core/ExpandableSelector";

/**
* @component
*
* @param {object} props
* @param {PartitionSlot|StorageDevice} props.item
*/
const DeviceInfo = ({ item }) => {
const DeviceInfo = ({ item }: { item: PartitionSlot | StorageDevice; }) => {
const device = toStorageDevice(item);
if (!device) return null;

const DeviceType = () => {
let type;
let type: string;

switch (device.type) {
case "multipath": {
Expand All @@ -78,7 +70,7 @@ const DeviceInfo = ({ item }) => {
const technology = device.transport || device.bus;
type = technology
? // TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA"
sprintf(_("%s disk"), technology)
sprintf(_("%s disk"), technology)
: _("Disk");
}
}
Expand Down Expand Up @@ -134,11 +126,8 @@ const DeviceInfo = ({ item }) => {

/**
* @component
*
* @param {object} props
* @param {PartitionSlot|StorageDevice} props.item
*/
const DeviceExtendedDetails = ({ item }) => {
const DeviceExtendedDetails = ({ item }: { item: PartitionSlot | StorageDevice; }) => {
const device = toStorageDevice(item);

if (!device || ["partition", "lvmLv"].includes(device.type)) return <DeviceDetails item={item} />;
Expand Down Expand Up @@ -192,26 +181,23 @@ const DeviceExtendedDetails = ({ item }) => {
);
};

/** @type {ExpandableSelectorColumn[]} */
const columns = [
const columns: ExpandableSelectorColumn[] = [
{ name: _("Device"), value: (item) => <DeviceInfo item={item} /> },
{ name: _("Details"), value: (item) => <DeviceExtendedDetails item={item} /> },
{ name: _("Size"), value: (item) => <DeviceSize item={item} />, classNames: "sizes-column" },
];

type DeviceSelectorTableBaseProps = {
devices: StorageDevice[];
selectedDevices: StorageDevice[];
}
type DeviceSelectorTableProps = DeviceSelectorTableBaseProps & ExpandableSelectorProps;

/**
* Table for selecting the installation device.
* @component
*
* @typedef {object} DeviceSelectorTableBaseProps
* @property {StorageDevice[]} devices
* @property {StorageDevice[]} selectedDevices
*
* @typedef {DeviceSelectorTableBaseProps & ExpandableSelectorProps} DeviceSelectorTableProps
*
* @param {DeviceSelectorTableProps} props
*/
export default function DeviceSelectorTable({ devices, selectedDevices, ...props }) {
export default function DeviceSelectorTable({ devices, selectedDevices, ...props }: DeviceSelectorTableProps) {
return (
<ExpandableSelector
columns={columns}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ import React from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import { EncryptionMethods } from "~/client/storage";
import EncryptionSettingsDialog from "~/components/storage/EncryptionSettingsDialog";
import EncryptionSettingsDialog, { EncryptionSettingsDialogProps } from "~/components/storage/EncryptionSettingsDialog";

/** @type {import("~/components/storage/EncryptionSettingsDialog").EncryptionSettingsDialogProps} */
let props;
let props: EncryptionSettingsDialogProps;
const onCancelFn = jest.fn();
const onAcceptFn = jest.fn();

describe.skip("EncryptionSettingsDialog", () => {
describe("EncryptionSettingsDialog", () => {
beforeEach(() => {
props = {
password: "1234",
Expand All @@ -51,20 +50,20 @@ describe.skip("EncryptionSettingsDialog", () => {

it("allows settings the encryption", async () => {
const { user } = plainRender(<EncryptionSettingsDialog {...props} />);
const switchField = screen.getByRole("switch", { name: "Encrypt the system" });
const checkbox = screen.getByRole("checkbox", { name: "Encrypt the system" });
const passwordInput = screen.getByLabelText("Password");
const confirmationInput = screen.getByLabelText("Password confirmation");
const tpmCheckbox = screen.getByRole("checkbox", { name: /Use.*TPM/ });
const acceptButton = screen.getByRole("button", { name: "Accept" });

expect(switchField).not.toBeChecked();
expect(checkbox).not.toBeChecked();
expect(passwordInput).toBeDisabled();
expect(passwordInput).toBeDisabled();
expect(tpmCheckbox).toBeDisabled();

await user.click(switchField);
await user.click(checkbox);

expect(switchField).toBeChecked();
expect(checkbox).toBeChecked();
expect(passwordInput).toBeEnabled();
expect(passwordInput).toBeEnabled();
expect(tpmCheckbox).toBeEnabled();
Expand Down Expand Up @@ -100,11 +99,11 @@ describe.skip("EncryptionSettingsDialog", () => {

it("allows unsetting the encryption", async () => {
const { user } = plainRender(<EncryptionSettingsDialog {...props} />);
const switchField = screen.getByRole("switch", { name: "Encrypt the system" });
const checkbox = screen.getByRole("checkbox", { name: "Encrypt the system" });
const acceptButton = screen.getByRole("button", { name: "Accept" });
expect(switchField).toBeChecked();
await user.click(switchField);
expect(switchField).not.toBeChecked();
expect(checkbox).toBeChecked();
await user.click(checkbox);
expect(checkbox).not.toBeChecked();
await user.click(acceptButton);
expect(props.onAccept).toHaveBeenCalledWith({ password: "" });
});
Expand Down Expand Up @@ -142,7 +141,7 @@ describe.skip("EncryptionSettingsDialog", () => {

it("does not allow sending not valid settings", async () => {
const { user } = plainRender(<EncryptionSettingsDialog {...props} />);
const switchField = screen.getByRole("switch", { name: "Encrypt the system" });
const checkbox = screen.getByRole("checkbox", { name: "Encrypt the system" });
const passwordInput = screen.getByLabelText("Password");
const confirmationInput = screen.getByLabelText("Password confirmation");
const acceptButton = screen.getByRole("button", { name: "Accept" });
Expand All @@ -151,10 +150,10 @@ describe.skip("EncryptionSettingsDialog", () => {
await user.clear(confirmationInput);
// Now password and passwordConfirmation do not match
expect(acceptButton).toBeDisabled();
await user.click(switchField);
await user.click(checkbox);
// But now the user is trying to unset the encryption
expect(acceptButton).toBeEnabled();
await user.click(switchField);
await user.click(checkbox);
// Back to a not valid settings state
expect(acceptButton).toBeDisabled();
await user.clear(passwordInput);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ import { _ } from "~/i18n";
import { PasswordAndConfirmationInput, Popup } from "~/components/core";
import { EncryptionMethods } from "~/client/storage";

/**
* @typedef {object} EncryptionSetting
* @property {string} password
* @property {string} [method]
*/

const DIALOG_TITLE = _("Encryption");
const DIALOG_DESCRIPTION = _(
"Full Disk Encryption (FDE) allows to protect the information stored \
Expand All @@ -48,20 +42,24 @@ TPM can verify the integrity of the system. TPM sealing requires the new system
directly on its first run.",
);

export type EncryptionSetting = {
password: string;
method?: string;
}

export type EncryptionSettingsDialogProps = {
password: string;
method: string;
methods: string[];
isOpen?: boolean;
isLoading?: boolean;
onCancel: () => void;
onAccept: (settings: EncryptionSetting) => void;
}

/**
* Renders a dialog that allows the user change encryption settings
* @component
*
* @typedef {object} EncryptionSettingsDialogProps
* @property {string} password - Password for encryption.
* @property {string} method - Encryption method.
* @property {string[]} methods - Possible encryption methods.
* @property {boolean} [isOpen=false] - Whether the dialog is visible or not.
* @property {boolean} [isLoading=false] - Whether the data is loading
* @property {() => void} onCancel - Callback to trigger when on cancel action.
* @property {(settings: EncryptionSetting) => void} onAccept - Callback to trigger on accept action.
*
* @param {EncryptionSettingsDialogProps} props
*/
export default function EncryptionSettingsDialog({
password: passwordProp,
Expand All @@ -71,7 +69,7 @@ export default function EncryptionSettingsDialog({
isLoading = false,
onCancel,
onAccept,
}) {
}: EncryptionSettingsDialogProps) {
const [isEnabled, setIsEnabled] = useState(passwordProp?.length > 0);
const [password, setPassword] = useState(passwordProp);
const [method, setMethod] = useState(methodProp);
Expand Down
1 change: 0 additions & 1 deletion web/src/components/storage/PartitionsField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ beforeEach(() => {
bootDevice: undefined,
defaultBootDevice: undefined,
onVolumesChange: jest.fn(),
onBootChange: jest.fn(),
};
});

Expand Down
Loading
Loading