diff --git a/ui/webui/src/components/storage/InstallationMethod.jsx b/ui/webui/src/components/storage/InstallationMethod.jsx index 16b413ab9bc2..f009e4926e1d 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 (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