Skip to content

Commit

Permalink
ISPN-15284 Select Multi with Chips
Browse files Browse the repository at this point in the history
  • Loading branch information
karesti committed Nov 1, 2023
1 parent 8c6d8a8 commit 000c610
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 190 deletions.
154 changes: 11 additions & 143 deletions src/app/AccessManagement/CreateRole.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import {
Button,
ButtonVariant,
Chip,
ChipGroup,
Form,
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
MenuToggle,
MenuToggleElement,
Modal,
ModalVariant,
Select,
SelectList,
SelectOption,
SelectOptionProps,
TextInput,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities
TextInput
} from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import formUtils, { IField } from '@services/formUtils';
import { AddCircleOIcon, ExclamationCircleIcon, TimesIcon } from '@patternfly/react-icons';
import { AddCircleOIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
import { useCreateRole, useFetchAvailableRoles } from '@app/services/rolesHook';
import { SelectMultiWithChips } from '@app/Common/SelectMultiWithChips';

const CreateRole = (props: { isModalOpen: boolean; submitModal: () => void; closeModal: () => void }) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -69,12 +60,6 @@ const CreateRole = (props: { isModalOpen: boolean; submitModal: () => void; clos
const [roleDescription, setRoleDescription] = useState<IField>(roleDescriptionInitialState);
const [rolePermissionsField, setRolePermissionsField] = useState<IField>(rolePermissionsInitialState);
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]);
const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
const [isOpenPermissions, setIsOpenPermissions] = useState(false);
const [inputValue, setInputValue] = React.useState<string>('');
const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(initialSelectOptions);
const [activeItem, setActiveItem] = React.useState<string | null>(null);
const textInputRef = React.useRef<HTMLInputElement>();
const { onCreateRole } = useCreateRole(roleName.value, roleDescription.value, selectedPermissions, props.submitModal);

const handleSubmit = () => {
Expand Down Expand Up @@ -122,41 +107,6 @@ const CreateRole = (props: { isModalOpen: boolean; submitModal: () => void; clos
setSelectedPermissions([]);
};

const handlePermissionsToggle = () => {
setIsOpenPermissions(!isOpenPermissions);
};

useEffect(() => {
let newSelectOptions: SelectOptionProps[] = initialSelectOptions;

// Filter menu items based on the text input value when one exists
if (inputValue) {
newSelectOptions = initialSelectOptions.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 (!isOpenPermissions) {
setIsOpenPermissions(true);
}
}

setSelectOptions(newSelectOptions);
setFocusedItemIndex(null);
setActiveItem(null);
}, [inputValue]);

const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
setInputValue(value);
};

const onSelectPermission = (value: string) => {
if (value && value !== 'no results') {
setSelectedPermissions(
Expand All @@ -165,70 +115,8 @@ const CreateRole = (props: { isModalOpen: boolean; submitModal: () => void; clos
: [...selectedPermissions, value]
);
}

textInputRef.current?.focus();
};

const onToggleClick = () => {
setIsOpenPermissions(!isOpenPermissions);
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
variant="typeahead"
onClick={onToggleClick}
innerRef={toggleRef}
isExpanded={isOpenPermissions}
isFullWidth
data-cy="dropdown-button-permissions"
>
<TextInputGroup isPlain>
<TextInputGroupMain
value={inputValue}
onClick={onToggleClick}
onChange={onTextInputChange}
id="multi-typeahead-permissions-input"
autoComplete="off"
innerRef={textInputRef}
placeholder={t('access-management.roles.modal-permissions-list-placeholder')}
{...(activeItem && { 'aria-activedescendant': activeItem })}
role="combobox"
isExpanded={isOpenPermissions}
aria-controls="select-multi-typeahead-permissions"
>
<ChipGroup aria-label="Current permissions">
{selectedPermissions.map((selection, index) => (
<Chip
key={index}
onClick={(ev) => {
ev.stopPropagation();
onSelectPermission(selection);
}}
>
{selection}
</Chip>
))}
</ChipGroup>
</TextInputGroupMain>
<TextInputGroupUtilities>
{selectedPermissions.length > 0 && (
<Button
variant="plain"
onClick={() => {
setInputValue('');
setSelectedPermissions([]);
textInputRef?.current?.focus();
}}
aria-label="Clear input value"
>
<TimesIcon aria-hidden />
</Button>
)}
</TextInputGroupUtilities>
</TextInputGroup>
</MenuToggle>
);

return (
<Modal
position={'top'}
Expand Down Expand Up @@ -301,33 +189,13 @@ const CreateRole = (props: { isModalOpen: boolean; submitModal: () => void; clos
)}
</FormGroup>
<FormGroup fieldId="permissions" isRequired isInline label={t('access-management.roles.modal-permissions')}>
<Select
isScrollable
isOpen={isOpenPermissions}
selected={selectedPermissions}
onSelect={(ev, selection) => onSelectPermission(selection as string)}
onOpenChange={() => setIsOpenPermissions(false)}
onClick={handlePermissionsToggle}
aria-labelledby="role-permissions"
aria-label="role-permissions-select"
toggle={toggle}
>
<SelectList isAriaMultiselectable id="select-multi-typeahead-listbox">
{initialSelectOptions.map((option, index) => (
<SelectOption
key={option.value || option.children}
isFocused={focusedItemIndex === index}
className={option.className}
id={`select-multi-typeahead-${option.value.replace(' ', '-')}`}
{...option}
hasCheckbox
isSelected={selectedPermissions.includes(option.value)}
description={option.description}
ref={null}
/>
))}
</SelectList>
</Select>
<SelectMultiWithChips id="dropdown-button-permissions"
placeholder={t('access-management.roles.modal-permissions-list-placeholder')}
options={initialSelectOptions}
selection={selectedPermissions}
onSelect={onSelectPermission}
onClear={() => setSelectedPermissions([])}
/>
{rolePermissionsField.validated === 'error' && (
<FormHelperText>
<HelperText>
Expand Down
4 changes: 1 addition & 3 deletions src/app/Caches/Create/CacheConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
HelperTextItem, SelectOptionProps,
Text,
TextContent,
TextVariants
Expand Down Expand Up @@ -156,8 +156,6 @@ const CacheConfigEditor = (props: {
<React.Fragment>
<FormGroup fieldId="cache-config-name" label={t('caches.create.templates')}>
<Select
id="template-selector"
data-testid="template-selector"
toggleIcon={<CubeIcon />}
variant={SelectVariant.typeahead}
aria-label={t('caches.create.templates')}
Expand Down
38 changes: 14 additions & 24 deletions src/app/Caches/Create/Features/SecuredCacheConfigurator.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import React, { useEffect, useState } from 'react';
import { FormGroup, FormHelperText, HelperText, HelperTextItem } from '@patternfly/react-core';
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core/deprecated';
import { FormGroup, FormHelperText, HelperText, HelperTextItem, SelectOptionProps } from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { ConsoleServices } from '@services/ConsoleServices';
import { useCreateCache } from '@app/services/createCacheHook';
import { FeatureCard } from '@app/Caches/Create/Features/FeatureCard';
import { CacheFeature } from '@services/infinispanRefData';
import { FeatureAlert } from '@app/Caches/Create/Features/FeatureAlert';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { SelectMultiWithChips } from '@app/Common/SelectMultiWithChips';

const SecuredCacheConfigurator = (props: { isEnabled: boolean }) => {
const { configuration, setConfiguration } = useCreateCache();
const { t } = useTranslation();
const brandname = t('brandname.brandname');

const [roles, setRoles] = useState<string[]>(configuration.feature.securedCache.roles);

const [loading, setLoading] = useState(true);
const [availableRoles, setAvailableRoles] = useState<string[]>([]);
const [error, setError] = useState<'success' | 'error' | 'default'>('default');

const [isOpenRoles, setIsOpenRoles] = useState(false);

useEffect(() => {
if (loading) {
ConsoleServices.security()
Expand Down Expand Up @@ -64,12 +60,13 @@ const SecuredCacheConfigurator = (props: { isEnabled: boolean }) => {
return roles.length > 0;
};

const rolesOptions = () => {
const options = availableRoles.map((role) => <SelectOption id={role} inputId={role} key={role} value={role} />);
return options;
const rolesOptions = () : SelectOptionProps[] => {
const selectOptions: SelectOptionProps[] = [];
availableRoles.forEach((role) => selectOptions.push({value: role, children: role}));
return selectOptions;
};

const onSelectRoles = (event, selection) => {
const onSelectRoles = (selection) => {
if (roles.includes(selection)) setRoles(roles.filter((role) => role !== selection));
else setRoles([...roles, selection]);
};
Expand All @@ -84,20 +81,13 @@ const SecuredCacheConfigurator = (props: { isEnabled: boolean }) => {
description="caches.create.configurations.feature.secured-description"
>
<FormGroup fieldId="select-roles" isRequired label={'Roles'}>
<Select
toggleId="roleSelector"
chipGroupProps={{ numChips: 4, expandedText: 'Hide', collapsedText: 'Show ${remaining}' }}
variant={SelectVariant.typeaheadMulti}
aria-label="Select Roles"
onToggle={() => setIsOpenRoles(!isOpenRoles)}
onSelect={onSelectRoles}
selections={roles}
isOpen={isOpenRoles}
validated={validateForm()}
placeholderText={t('caches.create.configurations.feature.select-roles')}
>
{rolesOptions()}
</Select>
<SelectMultiWithChips id="roleSelector"
placeholder={t('caches.create.configurations.feature.select-roles')}
options={rolesOptions()}
selection={roles}
onSelect={onSelectRoles}
onClear={() => setRoles([])}
/>
{validateForm() === 'error' && (
<FormHelperText>
<HelperText>
Expand Down
34 changes: 15 additions & 19 deletions src/app/Caches/Entries/CreateOrUpdateEntryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
FormHelperText,
HelperText,
HelperTextItem,
Modal,
Modal, SelectOptionProps,
Spinner,
TextArea,
TextInput
Expand All @@ -22,11 +22,12 @@ import { useCacheDetail } from '@app/services/cachesHook';
import { useTranslation } from 'react-i18next';
import { ConsoleServices } from '@services/ConsoleServices';
import { CacheConfigUtils } from '@services/cacheConfigUtils';
import { ContentType, EncodingType, InfinispanFlags } from '@services/infinispanRefData';
import { CacheFeature, ContentType, EncodingType, InfinispanFlags } from '@services/infinispanRefData';
import { ProtobufDataUtils } from '@services/protobufDataUtils';
import { AddCircleOIcon, PencilAltIcon } from '@patternfly/react-icons';
import { PopoverHelp } from '@app/Common/PopoverHelp';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { SelectMultiWithChips } from '@app/Common/SelectMultiWithChips';

const CreateOrUpdateEntryForm = (props: {
cacheName: string;
Expand Down Expand Up @@ -213,8 +214,10 @@ const CreateOrUpdateEntryForm = (props: {
));
};

const flagsOptions = () => {
return Object.keys(InfinispanFlags).map((key) => <SelectOption id={key} key={key} value={InfinispanFlags[key]} />);
const flagsOptions = () : SelectOptionProps[] => {
const selectOptions: SelectOptionProps[] = [];
Object.keys(InfinispanFlags).forEach((key) => selectOptions.push({value: InfinispanFlags[key], children: InfinispanFlags[key]}));
return selectOptions;
};

const setExpanded = (expanded: boolean, stateDispatch: React.Dispatch<React.SetStateAction<ISelectField>>) => {
Expand All @@ -223,7 +226,7 @@ const CreateOrUpdateEntryForm = (props: {
});
};

const onSelectFlags = (event, selection) => {
const onSelectFlags = (selection) => {
let prevSelectedFlags = flags.selected as string[];

if (prevSelectedFlags.includes(selection)) {
Expand Down Expand Up @@ -626,20 +629,13 @@ const CreateOrUpdateEntryForm = (props: {
);
return (
<FormGroup label={t('caches.entries.add-entry-form-flags')} labelIcon={helper} fieldId="flags-helper">
<Select
variant={SelectVariant.typeaheadMulti}
aria-label={t('caches.entries.add-entry-form-flags-label')}
onToggle={(_event, isExpanded) => setExpanded(isExpanded, setFlags)}
onSelect={onSelectFlags}
onClear={() => setFlags(flagsInitialState)}
selections={flags.selected}
isOpen={flags.expanded}
placeholderText={t('caches.entries.add-entry-form-flags-label')}
maxHeight={150}
toggleId="ispnFlags"
>
{flagsOptions()}
</Select>
<SelectMultiWithChips id="ispnFlags"
placeholder={t('caches.entries.add-entry-form-flags-label')}
options={flagsOptions()}
onSelect={onSelectFlags}
onClear={() => setFlags(flagsInitialState)}
selection={flags.selected as string[]}
/>
<FormHelperText>
<HelperText>
<HelperTextItem>{t('caches.entries.add-entry-form-flags-help')}</HelperTextItem>
Expand Down
8 changes: 7 additions & 1 deletion src/app/Common/SelectMultiWithChips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ const SelectMultiWithChips = (props: {id: string, placeholder:string,
};

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle variant="typeahead" onClick={onToggleClick} innerRef={toggleRef} isExpanded={isOpen} isFullWidth>
<MenuToggle variant="typeahead"
data-cy={props.id}
onClick={onToggleClick}
innerRef={toggleRef}
isExpanded={isOpen}
isFullWidth>
<TextInputGroup isPlain>
<TextInputGroupMain
value={inputValue}
Expand Down Expand Up @@ -194,6 +199,7 @@ const SelectMultiWithChips = (props: {id: string, placeholder:string,
onSelect={(ev, selection) => onSelect(selection as string)}
onOpenChange={() => setIsOpen(false)}
toggle={toggle}
isScrollable
>
<SelectList isAriaMultiselectable id={props.id + 'select-multi-typeahead-listbox' }>
{selectOptions.map((option, index) => (
Expand Down

0 comments on commit 000c610

Please sign in to comment.