Skip to content

Commit

Permalink
[RHOAIENG-2404] Replace remaining PF deprecated components
Browse files Browse the repository at this point in the history
  • Loading branch information
jeff-phillips-18 committed Aug 15, 2024
1 parent ac0a765 commit 253a4ee
Show file tree
Hide file tree
Showing 20 changed files with 925 additions and 389 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class ClusterStorageModal extends Modal {
findWorkbenchConnectionSelect() {
return this.find()
.findByTestId('connect-existing-workbench-group')
.findByRole('button', { name: 'Options menu' });
.findByRole('button', { name: 'Typeahead menu toggle' });
}

findMountField() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class DataConnectionModal extends Modal {
findWorkbenchConnectionSelect() {
return cy
.findByTestId('connect-existing-workbench-group')
.findByRole('button', { name: 'Options menu' });
.findByRole('button', { name: 'Notebook select' });
}

findNotebookRestartAlert() {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/__tests__/cypress/cypress/pages/modelRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class ModelRegistry {
}

shouldModelRegistrySelectorExist() {
cy.get('#model-registry-selector-dropdown').should('exist');
cy.findByTestId('model-registry-selector-dropdown').should('exist');
}

shouldtableToolbarExist() {
Expand Down Expand Up @@ -145,7 +145,7 @@ class ModelRegistry {
}

findModelRegistry() {
return cy.get('#model-registry-selector-dropdown');
return cy.findByTestId('model-registry-selector-dropdown');
}

findModelVersionsTableHeaderButton(name: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class PermissionTable extends Contextual<HTMLElement> {
}

findNameSelect() {
return this.find().get(`[aria-label="Name selection"]`);
return this.find().get(`[aria-label="Type to filter"]`);
}

getTableRow(name: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class GroupSettingSection extends Contextual<HTMLElement> {
}

findMultiGroupSelectButton() {
return this.find().findByRole('button', { name: 'Options menu' });
return this.find().findByTestId('group-setting-select');
}

selectMultiGroup(name: string) {
Expand Down
15 changes: 7 additions & 8 deletions frontend/src/__tests__/cypress/cypress/pages/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ class StorageModal extends Modal {
}

selectExistingPersistentStorage(name: string) {
this.find()
.findByTestId('persistent-storage-group')
.findByRole('button', { name: 'Options menu' })
.findSelectOption(name)
cy.findByTestId('persistent-storage-group')
.findByRole('button', { name: 'Typeahead menu toggle' })
.click();
cy.findByTestId('persistent-storage-group').findByRole('option', { name }).click();
}

findSubmitButton() {
Expand Down Expand Up @@ -190,9 +189,9 @@ class CreateSpawnerPage {

selectExistingPersistentStorage(name: string) {
cy.findByTestId('persistent-storage-group')
.findByRole('button', { name: 'Options menu' })
.findSelectOption(name)
.findByRole('button', { name: 'Typeahead menu toggle' })
.click();
cy.get('[id="dashboard-page-main"]').findByRole('option', { name }).click();
}

selectPVSize(name: string) {
Expand Down Expand Up @@ -271,9 +270,9 @@ class CreateSpawnerPage {

selectExistingDataConnection(name: string) {
cy.findByTestId('data-connection-group')
.findByRole('button', { name: 'Options menu' })
.findSelectOption(name)
.findByRole('button', { name: 'Typeahead menu toggle' })
.click();
cy.get('[id="dashboard-page-main"]').findByRole('option', { name }).click();
}

findAwsNameInput() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('User Management', () => {
userGroupSection.findChipItem('system:authenticated').should('exist');
userGroupSection.clearMultiChipItem();
userGroupSection.findErrorText().should('exist');
userGroupSection.selectMultiGroup('odh-admins');
userGroupSection.selectMultiGroup('odh-admins', false);
userGroupSection.findChipItem(/^odh-admins$/).should('exist');
userGroupSection.findMultiGroupSelectButton().click();
userManagement.findSubmitButton().should('be.enabled');
Expand Down
171 changes: 138 additions & 33 deletions frontend/src/components/MultiSelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
Button,
HelperText,
HelperTextItem,
SelectGroup,
Divider,
} from '@patternfly/react-core';
import { TimesIcon } from '@patternfly/react-icons/dist/esm/icons/times-icon';

Expand All @@ -22,28 +24,94 @@ export type SelectionOptions = {
selected?: boolean;
};

export type GroupSelectionOptions = {
id: number | string;
name: string;
values: SelectionOptions[];
};

type MultiSelectionProps = {
value: SelectionOptions[];
id?: string;
value?: SelectionOptions[];
groupedValues?: GroupSelectionOptions[];
setValue: (itemSelection: SelectionOptions[]) => void;
toggleId?: string;
ariaLabel: string;
placeholder?: string;
isDisabled?: boolean;
selectionRequired?: boolean;
noSelectedOptionsMessage?: string;
toggleTestId?: string;
};

export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue }) => {
export const MultiSelection: React.FC<MultiSelectionProps> = ({
value = [],
groupedValues = [],
setValue,
placeholder,
isDisabled,
ariaLabel = 'Options menu',
id,
toggleId,
toggleTestId,
selectionRequired,
noSelectedOptionsMessage = 'One or more options must be selected',
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState<string>('');
const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | null>(null);
const [activeItem, setActiveItem] = React.useState<string | null>(null);
const textInputRef = React.useRef<HTMLInputElement>();
const selected = React.useMemo(() => value.filter((v) => v.selected), [value]);

const selectGroups = React.useMemo(() => {
let counter = 0;
return groupedValues
.map((g) => {
const values = g.values.filter(
(v) => !inputValue || v.name.toLowerCase().includes(inputValue.toLowerCase()),
);
return {
...g,
values: values.map((v) => ({ ...v, index: counter++ })),
};
})
.filter((g) => g.values.length);
}, [inputValue, groupedValues]);

const setOpen = (open: boolean) => {
setIsOpen(open);
if (!open) {
setInputValue('');
}
};
const groupOptions = selectGroups.reduce<SelectionOptions[]>((acc, g) => {
acc.push(...g.values);
return acc;
}, []);

const selectOptions = React.useMemo(
() =>
value.filter((v) => !inputValue || v.name.toLowerCase().includes(inputValue.toLowerCase())),
[inputValue, value],
value
.filter((v) => !inputValue || v.name.toLowerCase().includes(inputValue.toLowerCase()))
.map((v, index) => ({ ...v, index: groupOptions.length + index })),
[groupOptions, inputValue, value],
);

const allOptions = React.useMemo(() => {
const options = [];
groupedValues.forEach((group) => options.push(...group.values));
options.push(...value);

return options;
}, [groupedValues, value]);

const visibleOptions = [...groupOptions, ...selectOptions];

const selected = React.useMemo(() => allOptions.filter((v) => v.selected), [allOptions]);

React.useEffect(() => {
if (inputValue) {
setIsOpen(true);
setOpen(true);
}
setFocusedItemIndex(null);
setActiveItem(null);
Expand All @@ -52,20 +120,23 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue
const handleMenuArrowKeys = (key: string) => {
let indexToFocus;
if (!isOpen) {
setIsOpen(true);
setOpen(true);
setFocusedItemIndex(0);
return;
}

const optionsLength = visibleOptions.length;

if (key === 'ArrowUp') {
if (focusedItemIndex === null || focusedItemIndex === 0) {
indexToFocus = selectOptions.length - 1;
indexToFocus = optionsLength - 1;
} else {
indexToFocus = focusedItemIndex - 1;
}
}

if (key === 'ArrowDown') {
if (focusedItemIndex === null || focusedItemIndex === selectOptions.length - 1) {
if (focusedItemIndex === null || focusedItemIndex === optionsLength - 1) {
indexToFocus = 0;
} else {
indexToFocus = focusedItemIndex + 1;
Expand All @@ -74,13 +145,13 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue

if (indexToFocus != null) {
setFocusedItemIndex(indexToFocus);
const focusedItem = selectOptions[indexToFocus];
const focusedItem = visibleOptions[indexToFocus];
setActiveItem(`select-multi-typeahead-${focusedItem.name.replace(' ', '-')}`);
}
};

const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
const focusedItem = focusedItemIndex !== null ? selectOptions[focusedItemIndex] : null;
const focusedItem = focusedItemIndex !== null ? visibleOptions[focusedItemIndex] : null;
switch (event.key) {
case 'Enter':
if (isOpen && focusedItem) {
Expand All @@ -92,7 +163,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue
break;
case 'Tab':
case 'Escape':
setIsOpen(false);
setOpen(false);
setActiveItem(null);
break;
case 'ArrowUp':
Expand All @@ -104,7 +175,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue
};

const onToggleClick = () => {
setIsOpen(!isOpen);
setOpen(!isOpen);
setTimeout(() => textInputRef.current?.focus(), 100);
};
const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, valueOfInput: string) => {
Expand All @@ -113,22 +184,26 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue
const onSelect = (menuItem?: SelectionOptions) => {
if (menuItem) {
setValue(
selected.includes(menuItem)
? value.map((option) => (option === menuItem ? { ...option, selected: false } : option))
: value.map((option) => (option === menuItem ? { ...option, selected: true } : option)),
allOptions.map((option) =>
option.id === menuItem.id ? { ...option, selected: !option.selected } : option,
),
);
}
textInputRef.current?.focus();
};

const noSelectedItems = value.filter((option) => option.selected).length === 0;
const noSelectedItems = allOptions.filter((option) => option.selected).length === 0;

const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
id={toggleId}
data-testid={toggleTestId}
variant="typeahead"
aria-label="Options menu"
status={selectionRequired && noSelectedItems ? 'danger' : undefined}
aria-label={ariaLabel}
onClick={onToggleClick}
innerRef={toggleRef}
isDisabled={isDisabled}
isExpanded={isOpen}
isFullWidth
>
Expand All @@ -144,6 +219,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue
role="combobox"
isExpanded={isOpen}
aria-controls="select-multi-typeahead-listbox"
placeholder={placeholder}
>
<ChipGroup aria-label="Current selections">
{selected.map((selection, index) => (
Expand All @@ -165,7 +241,7 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue
variant="plain"
onClick={() => {
setInputValue('');
setValue(value.map((option) => ({ ...option, selected: false })));
setValue(allOptions.map((option) => ({ ...option, selected: false })));
textInputRef.current?.focus();
}}
aria-label="Clear input value"
Expand All @@ -181,34 +257,63 @@ export const MultiSelection: React.FC<MultiSelectionProps> = ({ value, setValue
return (
<>
<Select
id={id}
isOpen={isOpen}
selected={selected}
onSelect={(ev, selection) => onSelect(value.find((option) => option.name === selection))}
onOpenChange={() => setIsOpen(false)}
onSelect={(ev, selection) => {
const selectedOption = allOptions.find((option) => option.id === selection);
onSelect(selectedOption);
}}
onOpenChange={() => setOpen(false)}
toggle={toggle}
>
<SelectList isAriaMultiselectable>
{selectOptions.length === 0 && inputValue ? (
{visibleOptions.length === 0 && inputValue ? (
<SelectList isAriaMultiselectable>
<SelectOption isDisabled>No results found</SelectOption>
) : (
selectOptions.map((option, index) => (
</SelectList>
) : null}
{selectGroups.map((g, index) => (
<>
<SelectGroup label={g.name} key={g.id}>
<SelectList isAriaMultiselectable>
{g.values.map((option) => (
<SelectOption
key={option.name}
isFocused={focusedItemIndex === option.index}
id={`select-multi-typeahead-${option.name.replace(' ', '-')}`}
value={option.id}
ref={null}
isSelected={option.selected}
>
{option.name}
</SelectOption>
))}
</SelectList>
</SelectGroup>
{index < selectGroups.length - 1 || selectOptions.length ? <Divider /> : null}
</>
))}
{selectOptions.length ? (
<SelectList isAriaMultiselectable>
{selectOptions.map((option) => (
<SelectOption
key={option.name}
isFocused={focusedItemIndex === index}
isFocused={focusedItemIndex === option.index}
id={`select-multi-typeahead-${option.name.replace(' ', '-')}`}
value={option.name}
value={option.id}
ref={null}
isSelected={option.selected}
>
{option.name}
</SelectOption>
))
)}
</SelectList>
))}
</SelectList>
) : null}
</Select>
{noSelectedItems && (
<HelperText>
{noSelectedItems && selectionRequired && (
<HelperText isLiveRegion>
<HelperTextItem variant="error" hasIcon data-testid="group-selection-error-text">
One or more group must be selected
{noSelectedOptionsMessage}
</HelperTextItem>
</HelperText>
)}
Expand Down
Loading

0 comments on commit 253a4ee

Please sign in to comment.