From 8c6d8a87bb85dc888f31ae859180ac8e6812ee53 Mon Sep 17 00:00:00 2001 From: Katia Aresti Date: Tue, 31 Oct 2023 16:12:04 +0000 Subject: [PATCH] ISPN-15284 Refactor Select: FeaturesSelector --- src/app/Caches/Create/FeaturesSelector.tsx | 41 ++-- src/app/Common/SelectMultiWithChips.tsx | 214 +++++++++++++++++++++ src/app/assets/languages/en.json | 1 - 3 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 src/app/Common/SelectMultiWithChips.tsx diff --git a/src/app/Caches/Create/FeaturesSelector.tsx b/src/app/Caches/Create/FeaturesSelector.tsx index e72f7e50..46373604 100644 --- a/src/app/Caches/Create/FeaturesSelector.tsx +++ b/src/app/Caches/Create/FeaturesSelector.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Alert, Form, FormAlert, FormGroup, FormSection } from '@patternfly/react-core'; -import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated'; +import { Alert, Form, FormAlert, FormGroup, FormSection, SelectOptionProps } from '@patternfly/react-core'; import { CacheFeature, CacheMode } from '@services/infinispanRefData'; import { useTranslation } from 'react-i18next'; import { ConsoleServices } from '@services/ConsoleServices'; @@ -15,6 +14,7 @@ import { useConnectedUser } from '@app/services/userManagementHook'; import { validFeatures } from '@app/utils/featuresValidation'; import { useFetchProtobufTypes } from '@app/services/protobufHook'; import { ConsoleACL } from '@services/securityService'; +import { SelectMultiWithChips } from '@app/Common/SelectMultiWithChips'; const FeaturesSelector = () => { const { t } = useTranslation(); @@ -26,7 +26,6 @@ const FeaturesSelector = () => { const [loadingBackups, setLoadingBackups] = useState(true); const [isBackups, setIsBackups] = useState(false); - const [isOpenCacheFeature, setIsOpenCacheFeature] = useState(false); useEffect(() => { if (loadingBackups) { @@ -42,13 +41,12 @@ const FeaturesSelector = () => { } }, [loadingBackups]); - const onSelectFeature = (event, selection) => { + const onSelectFeature = (selection) => { if (configuration.feature.cacheFeatureSelected.includes(selection)) { removeFeature(selection); } else { addFeature(selection); } - setIsOpenCacheFeature(false); }; const onClearFeatureSelection = () => { @@ -61,11 +59,6 @@ const FeaturesSelector = () => { } }; }); - setIsOpenCacheFeature(false); - }; - - const cacheFeatureOptions = () => { - return Object.keys(CacheFeature).map((key) => ); }; const displayAlert = () => { @@ -89,6 +82,12 @@ const FeaturesSelector = () => { return !notSecured && ConsoleServices.security().hasConsoleACL(ConsoleACL.ADMIN, connectedUser); }; + const featuresOptions = () : SelectOptionProps[] => { + const selectOptions: SelectOptionProps[] = []; + Object.keys(CacheFeature).forEach((key) => selectOptions.push({value: CacheFeature[key], children: CacheFeature[key]})); + return selectOptions; + } + return (
{ > - + {displayAlert()} diff --git a/src/app/Common/SelectMultiWithChips.tsx b/src/app/Common/SelectMultiWithChips.tsx new file mode 100644 index 00000000..cff37f42 --- /dev/null +++ b/src/app/Common/SelectMultiWithChips.tsx @@ -0,0 +1,214 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Chip, + ChipGroup, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + SelectOptionProps, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities +} from '@patternfly/react-core'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; + +const SelectMultiWithChips = (props: {id: string, placeholder:string, + options: SelectOptionProps[], + onSelect: (selection) => void, + onClear: () => void, + selection: string[] +} +) => { + const [isOpen, setIsOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [selected, setSelected] = useState(props.selection); + const [selectOptions, setSelectOptions] = useState(props.options); + const [focusedItemIndex, setFocusedItemIndex] = useState(null); + const [activeItem, setActiveItem] = useState(null); + const textInputRef = React.useRef(); + + useEffect(() => { + setSelected(props.selection); + }, [props.selection]) + + useEffect(() => { + let newSelectOptions: SelectOptionProps[] = props.options; + + // Filter menu items based on the text input value when one exists + if (inputValue) { + newSelectOptions = props.options.filter((menuItem) => + String(menuItem.children).toLowerCase().includes(inputValue.toLowerCase()) + ); + + // When no options are found after filtering, display 'No results found' + if (!newSelectOptions.length) { + newSelectOptions = [ + { isDisabled: false, children: `No results found for "${inputValue}"`, value: 'no results' } + ]; + } + + // Open the menu when the input value changes and the new value is not empty + if (!isOpen) { + setIsOpen(true); + } + } + + setSelectOptions(newSelectOptions); + setFocusedItemIndex(null); + setActiveItem(null); + }, [inputValue]); + + const handleMenuArrowKeys = (key: string) => { + 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 focusedItem = selectOptions.filter((option) => !option.isDisabled)[indexToFocus]; + setActiveItem(`select-multi-typeahead-${focusedItem.value.replace(' ', '-')}`); + } + }; + + const onInputKeyDown = (event: React.KeyboardEvent) => { + 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.value !== 'no results') { + onSelect(focusedItem.value as string); + } + break; + case 'Tab': + case 'Escape': + setIsOpen(false); + setActiveItem(null); + break; + case 'ArrowUp': + case 'ArrowDown': + event.preventDefault(); + handleMenuArrowKeys(event.key); + break; + } + }; + + const onToggleClick = () => { + setIsOpen(!isOpen); + }; + + const onTextInputChange = (_event: React.FormEvent, value: string) => { + setInputValue(value); + }; + + const onSelect = (value: string) => { + if (value && value !== 'no results') { + setSelected( + selected.includes(value) ? selected.filter((selection) => selection !== value) : [...selected, value] + ); + } + + textInputRef.current?.focus(); + props.onSelect(value); + }; + + const toggle = (toggleRef: React.Ref) => ( + + + + + {selected.map((selection, index) => ( + { + ev.stopPropagation(); + onSelect(selection); + }} + > + {selection} + + ))} + + + + {selected.length > 0 && ( + + )} + + + + ); + + return ( + + ); +}; + +export { SelectMultiWithChips } diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json index 9aa090e6..6005f1de 100644 --- a/src/app/assets/languages/en.json +++ b/src/app/assets/languages/en.json @@ -303,7 +303,6 @@ "cache-feature-list": "Add {{brandname}} capabilities", "cache-feature-list-tooltip": "Add capabilities to your cache.", "cache-feature-list-placeholder": "Select capabilities", - "cache-feature-list-typeahead": "Select capabilities", "bounded": "Bounded", "bounded-tooltip": "To restrict the size of the cache, configure {{brandname}} to evict entries. You can set an eviction threshold based on the maximum amount of memory or based on the total number of entries that a cache can hold.", "radio-max-size": "Maximum amount of memory",