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

Webui pre blivet dialogs #5113

Merged
merged 9 commits into from
Sep 6, 2023
3 changes: 2 additions & 1 deletion ui/webui/src/components/Error.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,11 @@ const addExceptionDataToReportURL = (url, exception) => {
};

const exceptionInfo = (exception, idPrefix) => {
const exceptionNamePrefix = exception.name ? exception.name + ": " : "";
return (
<TextContent id={idPrefix + "-bz-report-modal-details"}>
<Text component={TextVariants.p}>
{exception.name + ": " + exception.message}
{exceptionNamePrefix + exception.message}
</Text>
</TextContent>
);
Expand Down
188 changes: 164 additions & 24 deletions ui/webui/src/components/storage/InstallationMethod.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,20 @@ import {
Form,
FormGroup,
MenuToggle,
Modal,
Select,
SelectList,
SelectOption,
Spinner,
Text,
TextContent,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
TextVariants,
Title,
} from "@patternfly/react-core";
import { SyncAltIcon, TimesIcon, WrenchIcon } from "@patternfly/react-icons";
import { SyncAltIcon, TimesIcon, WrenchIcon, ExternalLinkAltIcon } from "@patternfly/react-icons";

import { InstallationScenario } from "./InstallationScenario.jsx";

Expand Down Expand Up @@ -288,9 +292,38 @@ const LocalDisksSelect = ({ deviceData, diskSelection, idPrefix, isRescanningDis
);
};

const InstallationDestination = ({ deviceData, diskSelection, dispatch, idPrefix, isBootIso, setIsFormValid, onCritFail }) => {
const rescanDisks = (setIsRescanningDisks, refUsableDisks, dispatch, errorHandler) => {
setIsRescanningDisks(true);
refUsableDisks.current = undefined;
scanDevicesWithTask()
.then(res => {
return runStorageTask({
task: res[0],
onSuccess: () => resetPartitioning()
.then(() => Promise.all([
dispatch(getDevicesAction()),
dispatch(getDiskSelectionAction())
]))
.catch(errorHandler),
onFail: errorHandler
});
})
.finally(() => setIsRescanningDisks(false));
};

const InstallationDestination = ({
deviceData,
diskSelection,
dispatch,
idPrefix,
isBootIso,
setIsFormValid,
onRescanDisks,
onCritFail
}) => {
const [isRescanningDisks, setIsRescanningDisks] = useState(false);
const [equalDisksNotify, setEqualDisksNotify] = useState(false);
const [openedDialog, setOpenedDialog] = useState("");
const refUsableDisks = useRef();

debug("DiskSelector: deviceData: ", JSON.stringify(Object.keys(deviceData)), ", diskSelection: ", JSON.stringify(diskSelection));
Expand Down Expand Up @@ -327,7 +360,7 @@ const InstallationDestination = ({ deviceData, diskSelection, dispatch, idPrefix

const loading = !deviceData || diskSelection.usableDisks.some(disk => !deviceData[disk]);

const errorHandler = onCritFail({
const rescanErrorHandler = onCritFail({
context: N_("Rescanning of the disks failed.")
});

Expand All @@ -340,24 +373,12 @@ const InstallationDestination = ({ deviceData, diskSelection, dispatch, idPrefix
variant="link"
isLoading={isRescanningDisks}
icon={<SyncAltIcon />}
onClick={() => {
setIsRescanningDisks(true);
refUsableDisks.current = undefined;
scanDevicesWithTask()
.then(res => {
return runStorageTask({
task: res[0],
onSuccess: () => resetPartitioning()
.then(() => Promise.all([
dispatch(getDevicesAction()),
dispatch(getDiskSelectionAction())
]))
.catch(errorHandler),
onFail: errorHandler
});
})
.finally(() => setIsRescanningDisks(false));
}}
onClick={() => rescanDisks(
setIsRescanningDisks,
refUsableDisks,
dispatch,
rescanErrorHandler
)}
>
{_("Rescan")}
</Button>
Expand Down Expand Up @@ -410,25 +431,144 @@ const InstallationDestination = ({ deviceData, diskSelection, dispatch, idPrefix
: _("No usable disks detected")
)}
{rescanDisksButton}
<ModifyStorageButton isBootIso={isBootIso} />
<ModifyStorageButton idPrefix={idPrefix} isBootIso={isBootIso} onModifyStorage={() => setOpenedDialog("modify")} />
</Flex>
</FormGroup>
{openedDialog === "modify" &&
<ModifyStorageModal
onClose={() => setOpenedDialog("")}
onToolStarted={() => setOpenedDialog("rescan")}
errorHandler={onCritFail({ context: N_("Modifying the storage failed.") })}
/>}
{openedDialog === "rescan" &&
<StorageModifiedModal
onClose={() => setOpenedDialog("")}
onRescan={() => rescanDisks(
setIsRescanningDisks,
refUsableDisks,
dispatch,
rescanErrorHandler
)}
/>}
</>
);
};

const ModifyStorageButton = ({ isBootIso }) => {
const ModifyStorageButton = ({ idPrefix, isBootIso, onModifyStorage }) => {
if (isBootIso) {
return null;
}

return (
<Button variant="link" icon={<WrenchIcon />} onClick={() => cockpit.spawn(["blivet-gui"])}>
<Button
id={idPrefix + "-modify-storage"}
variant="link"
icon={<WrenchIcon />}
onClick={() => onModifyStorage()}>
{_("Modify storage")}
</Button>
);
};

const startBlivetGUI = (onStart, onStarted, errorHandler) => {
console.log("Spawning blivet-gui.");
// We don't have an event informing that blivet-gui started so just wait a bit.
const timeoutId = window.setTimeout(onStarted, 3000);
cockpit.spawn(["blivet-gui"], { err: "message" })
.then(() => {
console.log("blivet-gui exited.");
// If the blivet-gui exits earlier cancel the delay
window.clearTimeout(timeoutId);
return onStarted();
})
.catch((error) => { window.clearTimeout(timeoutId); errorHandler(error) });
onStart();
};

const StorageModifiedModal = ({ onClose, onRescan }) => {
return (
<Modal
id="storage-modified-modal"
title={_("Modified storage")}
isOpen
variant="small"
showClose={false}
footer={
<>
<Button
onClick={() => { onClose(); onRescan() }}
variant="primary"
id="storage-modified-modal-rescan-btn"
key="rescan"
>
{_("Rescan storage")}
</Button>
<Button
variant="secondary"
onClick={() => onClose()}
id="storage-modified-modal-ignore-btn"
key="ignore"
>
{_("Ignore")}
</Button>
</>
}>
{_("If you have made changes on partitions or disks, please rescan storage.")}
</Modal>
);
};

const ModifyStorageModal = ({ onClose, onToolStarted, errorHandler }) => {
const [toolIsStarting, setToolIsStarting] = useState(false);
const onStart = () => setToolIsStarting(true);
const onStarted = () => { setToolIsStarting(false); onToolStarted() };
return (
<Modal
id="modify-storage-modal"
title={_("Modify storage")}
isOpen
variant="small"
titleIconVariant="warning"
showClose={false}
footer={
<>
<Button
onClick={() => startBlivetGUI(
onStart,
onStarted,
errorHandler
)}
id="modify-storage-modal-modify-btn"
icon={toolIsStarting ? null : <ExternalLinkAltIcon />}
isLoading={toolIsStarting}
isDisabled={toolIsStarting}
variant="primary"
>
{_("Launch Blivet-gui storage editor")}
</Button>
<Button
variant="link"
onClick={() => onClose()}
id="modify-storage-modal-cancel-btn"
key="cancel"
isDisabled={toolIsStarting}
>
{_("Cancel")}
</Button>
</>
}>
<TextContent>
<Text component={TextVariants.p}>
{_("Blivet-gui is and advanced storage editor that lets you resize, delete, and create partitions. It can set up LVM and much more.")}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: "an advanced storage editor", not "and advanced storage editor"

</Text>
<Text component={TextVariants.p}>
{_("Changes made in Blivet-gui will directly affect your storage.")}
</Text>
</TextContent>
</Modal>
);
};

export const InstallationMethod = ({
deviceData,
diskSelection,
Expand Down
4 changes: 2 additions & 2 deletions ui/webui/test/check-basic
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ from language import Language
from review import Review
from storage import Storage
from testlib import nondestructive, test_main, wait # pylint: disable=import-error
from utils import pretend_live_iso


@nondestructive
Expand Down Expand Up @@ -83,8 +84,7 @@ class TestBasic(anacondalib.VirtInstallMachineCase):
i = Installer(b, m)
r = Review(b)

self.restore_file('/run/anaconda/anaconda.conf')
m.execute("sed -i 's/type = BOOT_ISO/type = LIVE_OS/g' /run/anaconda/anaconda.conf")
pretend_live_iso(self)

# For live media the first screen is the installation-method
i.open(step="installation-method")
Expand Down
50 changes: 50 additions & 0 deletions ui/webui/test/check-storage
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ from storage import Storage
from review import Review
from testlib import nondestructive, test_main # pylint: disable=import-error
from storagelib import StorageHelpers # pylint: disable=import-error
from utils import pretend_live_iso


@nondestructive
Expand Down Expand Up @@ -78,6 +79,55 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
s.select_disk("vda", True)
s.select_none_disks_and_check([dev, "vda"])

def testModifyStorage(self):
b = self.browser
m = self.machine
i = Installer(b, self.machine)
s = Storage(b, self.machine)

self.addCleanup(m.execute, "killall blivet-gui")

pretend_live_iso(self)

# For live media the first screen is the installation-method
i.open(step="installation-method")

disk="vda"

# Check the auto-selected disk's details
s.check_single_disk_destination(disk, "16.1 GB")

# Pixel test the storage step
b.assert_pixels(
"#app",
"storage-step-basic-live",
ignore=["#betanag-icon"],
wait_animations=False,
)

s.modify_storage()
b.click("#modify-storage-modal-cancel-btn")

s.modify_storage()
# Run the tool
b.click("#modify-storage-modal-modify-btn")
b.wait_visible(f"#modify-storage-modal-modify-btn:not([aria-disabled={True}]")
b.wait_visible(f"#storage-modified-modal-rescan-btn")
b.click("#storage-modified-modal-ignore-btn")
# The disk is still selected
s.check_single_disk_destination(disk, "16.1 GB")

#s.modify_storage()
#b.click("#modify-storage-modal-cancel-btn")

s.modify_storage()
b.click("#modify-storage-modal-modify-btn")
b.wait_visible(f"#modify-storage-modal-modify-btn:not([aria-disabled={True}]")
b.wait_visible(f"#storage-modified-modal-rescan-btn")
b.click("#storage-modified-modal-rescan-btn")
# The disk is still selected
s.check_single_disk_destination(disk, "16.1 GB")

# Test moving back and forth between screens.
# Disk initialization mode is applied to the backend in the test.
# Partitioning is not applied to the backend in the test.
Expand Down
3 changes: 3 additions & 0 deletions ui/webui/test/helpers/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ def rescan_disks(self):
self.browser.click(f"#{self._step}-rescan-disks")
self.browser.wait_not_present(f"#{self._step}-rescan-disks.pf-m-disabled")

def modify_storage(self):
self.browser.click(f"#{self._step}-modify-storage")

@log_step(snapshot_before=True)
def check_disk_visible(self, disk, visible=True):
if not self.browser.is_present(f".pf-v5-c-menu[aria-labelledby='{id_prefix}-disk-selector-title']"):
Expand Down
4 changes: 4 additions & 0 deletions ui/webui/test/helpers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ def add_public_key(machine):
authorized_keys = os.path.join(sysroot_ssh, 'authorized_keys')
machine.execute(f"chmod 700 {sysroot_ssh}")
machine.write(authorized_keys, public_key, perm="0600")

def pretend_live_iso(test):
test.restore_file('/run/anaconda/anaconda.conf')
test.machine.execute("sed -i 's/type = BOOT_ISO/type = LIVE_OS/g' /run/anaconda/anaconda.conf")