Skip to content

Commit

Permalink
web: start adapting booting selection
Browse files Browse the repository at this point in the history
It has to improve a lot
  • Loading branch information
dgdavid committed May 22, 2024
1 parent 286c825 commit d09b57a
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 226 deletions.
2 changes: 1 addition & 1 deletion web/src/components/core/Page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const NextActions = ({ children }) => (
);

const MainContent = ({ children }) => (
<PageSection isFilled>{children}</PageSection>
<PageSection variant="light" isFilled>{children}</PageSection>
);

const Navigation = ({ routes }) => {
Expand Down
40 changes: 7 additions & 33 deletions web/src/components/storage/BootConfigField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,31 @@

// @ts-check

import React, { useState } from "react";
import React from "react";
import { Link as RouterLink } from "react-router-dom";
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
* 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 {() => void} props.onClick - Callback to trigger when user clicks.
*/
const Button = ({ isBold = false, onClick }) => {
const Link = ({ isBold = false }) => {
const text = _("Change boot options");

return (
<button onClick={onClick} className="inline-flex-button">
<RouterLink to="booting-partitions">
{isBold ? <b>{text}</b> : text}
</button>
</RouterLink>
);
};

Expand All @@ -73,19 +70,10 @@ const Button = ({ isBold = false, onClick }) => {
export default function BootConfigField({
configureBoot,
bootDevice,
defaultBootDevice,
availableDevices,
isLoading,
onChange
}) {
const [isDialogOpen, setIsDialogOpen] = useState(false);

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

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

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

Expand All @@ -106,21 +94,7 @@ export default function BootConfigField({

return (
<div>
{value} <Button onClick={openDialog} isBold={!configureBoot} />
<If
condition={isDialogOpen}
then={
<BootSelectionDialog
isOpen
configureBoot={configureBoot}
bootDevice={bootDevice}
defaultBootDevice={defaultBootDevice}
availableDevices={availableDevices}
onAccept={onAccept}
onCancel={closeDialog}
/>
}
/>
{value} <Link isBold={!configureBoot} />
</div>
);
}
208 changes: 208 additions & 0 deletions web/src/components/storage/BootSelection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* 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, { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Form, FormGroup, Radio } from "@patternfly/react-core";
import { _ } from "~/i18n";
import { DevicesFormSelect } from "~/components/storage";
import { Page } from "~/components/core";
import { Loading } from "~/components/layout";
import { deviceLabel } from "~/components/storage/utils";
import { sprintf } from "sprintf-js";
import { useCancellablePromise } from "~/utils";
import { useInstallerClient } from "~/context/installer";

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

const BOOT_AUTO_ID = "boot-auto";
const BOOT_MANUAL_ID = "boot-manual";
const BOOT_DISABLED_ID = "boot-disabled";
const OPTIONS_NAME = "boot-mode";

/**
* Allows the user to select the boot configuration.
*/
export default function BootSelectionDialog() {
const { cancellablePromise } = useCancellablePromise();
const { storage: client } = useInstallerClient();
const [state, setState] = useState({});
const navigate = useNavigate();

// FIXME: Repeated code, see DeviceSelection. Use a context/hook or whatever
// approach to avoid duplication
const loadProposalResult = useCallback(async () => {
return await cancellablePromise(client.proposal.getResult());
}, [client, cancellablePromise]);

const loadAvailableDevices = useCallback(async () => {
return await cancellablePromise(client.proposal.getAvailableDevices());
}, [client, cancellablePromise]);

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

const load = async () => {
let selectedOption;
const { settings } = await loadProposalResult();
const availableDevices = await loadAvailableDevices();
const { bootDevice, configureBoot, defaultBootDevice } = settings;

console.log(settings);

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

setState({
load: true,
bootDevice: availableDevices.find(d => d.name === bootDevice),
configureBoot,
defaultBootDevice,
availableDevices,
selectedOption
});
};

load().catch(console.error);
}, [state, loadAvailableDevices, loadProposalResult]);

if (!state.load) return <Loading />;

const onSubmit = async (e) => {
e.preventDefault();
// FIXME: try to use formData here too?
// const formData = new FormData(e.target);
// const mode = formData.get("bootMode");
// const device = formData.get("bootDevice");
const { settings } = await loadProposalResult();
const newSettings = {
configureBoot: state.selectedOption !== BOOT_DISABLED_ID,
bootDevice: state.selectedOption === BOOT_MANUAL_ID ? state.bootDevice.name : undefined,
};

console.log("newSettings", newSettings);

await client.proposal.calculate({ ...settings, ...newSettings });
navigate("..");
};

const isAcceptDisabled = () => {
return state.selectedOption === BOOT_MANUAL_ID && state.bootDevice === undefined;
};

const description = _(
"To ensure the new system is able to boot, the installer may need to create or configure some \
partitions in the appropriate disk."
);

const automaticText = () => {
if (!state.defaultBootDevice) {
return _("Partitions to boot will be allocated at the installation disk.");
}

return sprintf(
// TRANSLATORS: %s is replaced by a device name and size (e.g., "/dev/sda, 500GiB")
_("Partitions to boot will be allocated at the installation disk (%s)."),
deviceLabel(state.defaultBootDevice)
);
};

const updateSelectedOption = (e) => {
setState({ ...state, selectedOption: e.target.value });
};

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

return (
<>
<Page.MainContent>
<Form id="bootSelectionForm" onSubmit={onSubmit}>
{description}
<FormGroup isStack>
<Radio
name="bootMode"
id={BOOT_AUTO_ID}
value={BOOT_AUTO_ID}
defaultChecked={state.selectedOption === BOOT_AUTO_ID}
onChange={updateSelectedOption}
label={_("Automatic")}
body={automaticText()}
/>
<Radio
name="bootMode"
id={BOOT_MANUAL_ID}
value={BOOT_MANUAL_ID}
defaultChecked={state.selectedOption === BOOT_MANUAL_ID}
onChange={updateSelectedOption}
label={_("Select a disk")}
body={
<div className="stack">
<div>
{_("Partitions to boot will be allocated at the following device.")}
</div>
<DevicesFormSelect
aria-label={_("Choose a disk for placing the boot loader")}
name="bootDevice"
devices={state?.availableDevices || []}
selectedDevice={state.bootDevice}
onChange={setBootDevice}
isDisabled={state.selectedOption !== BOOT_MANUAL_ID}
/>
</div>
}
/>
<Radio
name="bootMode"
id={BOOT_DISABLED_ID}
value={BOOT_DISABLED_ID}
defaultChecked={state.selectedOption === BOOT_DISABLED_ID}
onChange={updateSelectedOption}
label={_("Do not configure")}
body={
<div>
{_("No partitions will be automatically configured for booting. Use with caution.")}
</div>
}
/>
</FormGroup>
</Form>
</Page.MainContent>

<Page.NextActions>
<Page.CancelAction />
<Page.Action isDisabled={isAcceptDisabled()} type="submit" form="bootSelectionForm">
{_("Accept")}
</Page.Action>
</Page.NextActions>
</>
);
}
Loading

0 comments on commit d09b57a

Please sign in to comment.