From 2e1b5665869db7f83063b904f02c8fe72ee48f0f Mon Sep 17 00:00:00 2001 From: Katerina Koukiou Date: Thu, 17 Aug 2023 12:40:46 +0200 Subject: [PATCH] webui: port disk selector to the new Select implementation As noticed the new Select implementation from PF5 is way more configurable than the old one but for code readability purposed and developer experience it is a regression in comparison with the Patternfly 4 component. [1] [1] Opened an issue about this upstream: https://github.com/patternfly/patternfly-react/issues/9511 --- .../components/storage/InstallationMethod.jsx | 272 +++++++++++++----- .../storage/InstallationMethod.scss | 2 +- ui/webui/test/helpers/storage.py | 18 +- 3 files changed, 218 insertions(+), 74 deletions(-) diff --git a/ui/webui/src/components/storage/InstallationMethod.jsx b/ui/webui/src/components/storage/InstallationMethod.jsx index 16b413ab9bc2..73a9712f2583 100644 --- a/ui/webui/src/components/storage/InstallationMethod.jsx +++ b/ui/webui/src/components/storage/InstallationMethod.jsx @@ -21,18 +21,22 @@ import { Alert, AlertActionCloseButton, Button, + Chip, + ChipGroup, Flex, FlexItem, Form, FormGroup, - Title -} from "@patternfly/react-core"; -import { + MenuToggle, Select, + SelectList, SelectOption, - SelectVariant, -} from "@patternfly/react-core/deprecated"; -import { SyncAltIcon, WrenchIcon } from "@patternfly/react-icons"; + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, + Title, +} from "@patternfly/react-core"; +import { SyncAltIcon, TimesIcon, WrenchIcon } from "@patternfly/react-icons"; import { InstallationScenario } from "./InstallationScenario.jsx"; @@ -86,10 +90,196 @@ const containEqualDisks = (disks1, disks2) => { return disks1Str === disks2Str; }; +const LocalDisksSelect = ({ deviceData, diskSelection, idPrefix, setSelectedDisks }) => { + const [isOpen, setIsOpen] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [focusedItemIndex, setFocusedItemIndex] = useState(null); + const textInputRef = useRef(); + + let selectOptions = diskSelection.usableDisks + .map(disk => ({ + description: deviceData[disk]?.description.v, + name: disk, + size: cockpit.format_bytes(deviceData[disk]?.total.v), + value: disk, + })) + .filter(option => + String(option.name) + .toLowerCase() + .includes(inputValue.toLowerCase()) || + String(option.description) + .toLowerCase() + .includes(inputValue.toLowerCase()) + ); + + if (selectOptions.length === 0) { + selectOptions = [ + { children: _("No results found"), value: "no results" } + ]; + } + + const onSelect = (selectedDisk) => { + if (diskSelection.selectedDisks.includes(selectedDisk)) { + setSelectedDisks({ drives: diskSelection.selectedDisks.filter(disk => disk !== selectedDisk) }); + } else { + setSelectedDisks({ drives: [...diskSelection.selectedDisks, selectedDisk] }); + } + textInputRef.current?.focus(); + }; + + const clearSelection = () => { + setSelectedDisks({ drives: [] }); + }; + + const handleMenuArrowKeys = (key) => { + let indexToFocus; + + if (isOpen) { + if (key === "ArrowUp") { + // When no index is set or at the first index, focus to the last, otherwise decrement focus index + if (focusedItemIndex === null || focusedItemIndex === 0) { + indexToFocus = selectOptions.length - 1; + } else { + indexToFocus = focusedItemIndex - 1; + } + } + + if (key === "ArrowDown") { + // When no index is set or at the last index, focus to the first, otherwise increment focus index + if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) { + indexToFocus = 0; + } else { + indexToFocus = focusedItemIndex + 1; + } + } + + setFocusedItemIndex(indexToFocus); + } + }; + + const onInputKeyDown = (event) => { + const enabledMenuItems = selectOptions.filter((menuItem) => !menuItem.isDisabled); + const [firstMenuItem] = enabledMenuItems; + const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem; + + switch (event.key) { + // Select the first available option + case "Enter": + if (!isOpen) { + setIsOpen((prevIsOpen) => !prevIsOpen); + } else if (isOpen && focusedItem.name !== "no results") { + onSelect(focusedItem.name); + } + break; + case "Tab": + case "Escape": + setIsOpen(false); + break; + case "ArrowUp": + case "ArrowDown": + event.preventDefault(); + handleMenuArrowKeys(event.key); + break; + } + }; + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onTextInputChange = (_event, value) => { + setInputValue(value); + }; + + const toggle = (toggleRef) => ( + + + + + {diskSelection.selectedDisks.map((selection, index) => ( + { + ev.stopPropagation(); + onSelect(selection); + }} + > + {selection} + + ))} + + + + {diskSelection.selectedDisks.length > 0 && ( + + )} + + + + ); + + return ( + + ); +}; + const InstallationDestination = ({ deviceData, diskSelection, dispatch, idPrefix, isBootIso, setIsFormValid, onCritFail }) => { const [isRescanningDisks, setIsRescanningDisks] = useState(false); const [equalDisksNotify, setEqualDisksNotify] = useState(false); - const [isOpen, setIsOpen] = useState(false); const refUsableDisks = useRef(); debug("DiskSelector: deviceData: ", JSON.stringify(Object.keys(deviceData)), ", diskSelection: ", JSON.stringify(diskSelection)); @@ -162,63 +352,13 @@ const InstallationDestination = ({ deviceData, diskSelection, dispatch, idPrefix ); - const onSelect = (event, selection) => { - const selectedDisk = selection.name; - - if (diskSelection.selectedDisks.includes(selectedDisk)) { - setSelectedDisks({ drives: diskSelection.selectedDisks.filter(disk => disk !== selectedDisk) }); - } else { - setSelectedDisks({ drives: [...diskSelection.selectedDisks, selectedDisk] }); - } - }; - - const clearSelection = () => { - setSelectedDisks({ drives: [] }); - }; - const localDisksSelect = ( - + ); const equalDisks = refUsableDisks.current && containEqualDisks(refUsableDisks.current, diskSelection.usableDisks); @@ -293,7 +433,11 @@ export const InstallationMethod = ({ }) => { return ( -
+ { e.preventDefault(); return false }} + > {stepNotification && (stepNotification.step === "installation-method") && button") if selected: - self.browser.click(f"#{id_prefix}-disk-selector-option-{disk} button:not(.pf-m-selected)") + self.browser.click(f"#{id_prefix}-disk-selector-option-{disk}:not(.pf-m-selected)") else: - self.browser.click(f"#{id_prefix}-disk-selector-option-{disk} button.pf-m-selected") + self.browser.click(f"#{id_prefix}-disk-selector-option-{disk}.pf-m-selected") if is_single_disk: self.check_single_disk_destination(disk) @@ -63,7 +63,7 @@ def select_disk(self, disk, selected=True, is_single_disk=False): @log_step() def select_none_disks_and_check(self, disks): - self.browser.click(".pf-v5-c-select__toggle-clear") + self.browser.click(f"#{id_prefix}-disk-selector-clear") for disk in disks: self.check_disk_selected(disk, False) @@ -172,16 +172,16 @@ def rescan_disks(self): @log_step(snapshot_before=True) def check_disk_visible(self, disk, visible=True): - if not self.browser.is_present(f"ul[aria-labelledby='{id_prefix}-disk-selector-title']"): - self.browser.click(f"#{id_prefix}-disk-selector-toggle") + if not self.browser.is_present(f".pf-v5-c-menu[aria-labelledby='{id_prefix}-disk-selector-title']"): + self.browser.click(f"#{id_prefix}-disk-selector-toggle > button") if visible: self.browser.wait_visible(f"#{id_prefix}-disk-selector-option-{disk}") else: self.browser.wait_not_present(f"#{id_prefix}-disk-selector-option-{disk}") - self.browser.click(f"#{id_prefix}-disk-selector-toggle") - self.browser.wait_not_present(f"ul[aria-labelledby='{id_prefix}-disk-selector-title']") + self.browser.click(f"#{id_prefix}-disk-selector-toggle > button") + self.browser.wait_not_present(f".pf-v5-c-menu[aria-labelledby='{id_prefix}-disk-selector-title']") def _partitioning_selector(self, scenario): return f"#{id_prefix}-scenario-" + scenario