ShareImageModal: update the typeahead selector to pf5
This dropdown needed to be update as part of the overall pf5 upgrade. Each of the components in the select now need to be declared and handled. Also, some of the Select and SelectOption props come from the Menu props since Select inherits Menu.
This commit is contained in:
parent
a5b1b1f775
commit
cf1d477544
3 changed files with 164 additions and 103 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
ActionGroup,
|
||||
|
|
@ -9,14 +9,23 @@ import {
|
|||
HelperTextItem,
|
||||
FormHelperText,
|
||||
Popover,
|
||||
ValidatedOptions,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import { ExclamationCircleIcon, HelpIcon } from '@patternfly/react-icons';
|
||||
SelectList,
|
||||
ValidatedOptions,
|
||||
MenuToggle,
|
||||
MenuToggleElement,
|
||||
TextInputGroup,
|
||||
TextInputGroupMain,
|
||||
TextInputGroupUtilities,
|
||||
ChipGroup,
|
||||
Chip,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
ExclamationCircleIcon,
|
||||
HelpIcon,
|
||||
TimesIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AWS_REGIONS } from '../../constants';
|
||||
|
|
@ -55,28 +64,72 @@ const generateRequests = (
|
|||
type RegionsSelectPropTypes = {
|
||||
composeId: string;
|
||||
handleClose: any;
|
||||
handleToggle: any;
|
||||
isOpen: boolean;
|
||||
setIsOpen: any;
|
||||
};
|
||||
|
||||
const RegionsSelect = ({
|
||||
composeId,
|
||||
handleClose,
|
||||
handleToggle,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: RegionsSelectPropTypes) => {
|
||||
const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const titleId = 'Clone this image';
|
||||
const [validated, setValidated] = useState<ValidatedOptions>(
|
||||
ValidatedOptions.default
|
||||
);
|
||||
const [helperTextInvalid] = useState(
|
||||
'Select at least one region to share to.'
|
||||
);
|
||||
|
||||
const initialRegions = AWS_REGIONS;
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [selectOptions, setSelectOptions] = useState(initialRegions);
|
||||
|
||||
// Filter dropdown items when there is a typed input
|
||||
useEffect(() => {
|
||||
let newSelectOptions = initialRegions;
|
||||
|
||||
if (inputValue) {
|
||||
newSelectOptions = initialRegions.filter((region) =>
|
||||
region.value.toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
|
||||
// When no options are found after filtering, display 'No results found'
|
||||
if (!newSelectOptions.length) {
|
||||
newSelectOptions = [
|
||||
{
|
||||
disableRegion: false,
|
||||
description: `No results found for "${inputValue}"`,
|
||||
value: 'empty',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Open the menu when the input value changes and the new value is not empty
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectOptions(newSelectOptions);
|
||||
}, [inputValue]);
|
||||
|
||||
const onTextInputChange = (
|
||||
_event: React.FormEvent<HTMLInputElement>,
|
||||
value: string
|
||||
) => {
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
const onSelect = (value: string) => {
|
||||
if (value && value !== 'no results') {
|
||||
setSelected(
|
||||
selected.includes(value)
|
||||
? selected.filter((selection) => selection !== value)
|
||||
: [...selected, value]
|
||||
);
|
||||
setValidated(ValidatedOptions.success);
|
||||
} else {
|
||||
setValidated(ValidatedOptions.error);
|
||||
}
|
||||
};
|
||||
|
||||
const [cloneCompose] = useCloneComposeMutation();
|
||||
|
||||
const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({
|
||||
|
|
@ -87,33 +140,6 @@ const RegionsSelect = ({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const options = AWS_REGIONS;
|
||||
|
||||
const handleSelect = (
|
||||
event: React.MouseEvent<Element, MouseEvent> | React.ChangeEvent<Element>,
|
||||
selection: string
|
||||
): void => {
|
||||
let nextSelected;
|
||||
if (selected.includes(selection)) {
|
||||
nextSelected = selected.filter((region) => region !== selection);
|
||||
setSelected(nextSelected);
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
nextSelected = [...selected, selection];
|
||||
setSelected(nextSelected);
|
||||
setIsOpen(false);
|
||||
}
|
||||
nextSelected.length === 0
|
||||
? setValidated(ValidatedOptions.error)
|
||||
: setValidated(ValidatedOptions.default);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setSelected([]);
|
||||
setIsOpen(false);
|
||||
setValidated(ValidatedOptions.error);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSaving(true);
|
||||
const requests = generateRequests(composeId, composeStatus, selected);
|
||||
|
|
@ -121,9 +147,63 @@ const RegionsSelect = ({
|
|||
navigate(resolveRelPath(''));
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!selected.length) setValidated(ValidatedOptions.error);
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
|
||||
<MenuToggle
|
||||
variant="typeahead"
|
||||
onClick={handleToggle}
|
||||
innerRef={toggleRef}
|
||||
isExpanded={isOpen}
|
||||
isFullWidth
|
||||
>
|
||||
<TextInputGroup isPlain>
|
||||
<TextInputGroupMain
|
||||
value={inputValue}
|
||||
onClick={handleToggle}
|
||||
onChange={onTextInputChange}
|
||||
placeholder="Select region"
|
||||
isExpanded={isOpen}
|
||||
>
|
||||
<ChipGroup aria-label="Selected regions">
|
||||
{selected.map((selection, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onSelect(selection);
|
||||
}}
|
||||
>
|
||||
{selection}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</TextInputGroupMain>
|
||||
<TextInputGroupUtilities>
|
||||
{selected.length > 0 && (
|
||||
<Button
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
setInputValue('');
|
||||
setSelected([]);
|
||||
setValidated(ValidatedOptions.error);
|
||||
}}
|
||||
aria-label="Clear input value"
|
||||
>
|
||||
<TimesIcon aria-hidden />
|
||||
</Button>
|
||||
)}
|
||||
</TextInputGroupUtilities>
|
||||
</TextInputGroup>
|
||||
</MenuToggle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<span id={titleId} hidden>
|
||||
<span id="Clone this image" hidden>
|
||||
Select a region
|
||||
</span>
|
||||
<FormGroup
|
||||
|
|
@ -141,40 +221,37 @@ const RegionsSelect = ({
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="More info for name field"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
aria-describedby="simple-form-name-01"
|
||||
className="pf-c-form__group-label-help"
|
||||
<Button
|
||||
variant="plain"
|
||||
aria-label="About regions"
|
||||
className="pf-u-pl-sm header-button"
|
||||
isInline
|
||||
>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
</Button>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
typeAheadAriaLabel="Select a region"
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
onClear={handleClear}
|
||||
selections={selected}
|
||||
isScrollable
|
||||
isOpen={isOpen}
|
||||
aria-labelledby={titleId}
|
||||
placeholderText="Select a region"
|
||||
menuAppendTo="parent"
|
||||
validated={validated}
|
||||
maxHeight="25rem"
|
||||
selected={selected}
|
||||
onSelect={(ev, selection) => onSelect(selection as string)}
|
||||
onOpenChange={handleToggle}
|
||||
toggle={toggle}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<SelectOption
|
||||
isDisabled={option.disableRegion}
|
||||
key={index}
|
||||
value={option.value}
|
||||
{...(option.description && { description: option.description })}
|
||||
/>
|
||||
))}
|
||||
<SelectList isAriaMultiselectable>
|
||||
{selectOptions.map((option) => (
|
||||
<SelectOption
|
||||
isDisabled={option.disableRegion}
|
||||
key={option.value}
|
||||
description={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.description}
|
||||
</SelectOption>
|
||||
))}
|
||||
</SelectList>
|
||||
</Select>
|
||||
{validated !== 'success' && (
|
||||
<FormHelperText>
|
||||
|
|
@ -183,7 +260,7 @@ const RegionsSelect = ({
|
|||
icon={<ExclamationCircleIcon />}
|
||||
variant={validated}
|
||||
>
|
||||
{helperTextInvalid}
|
||||
Select at least one region to share to.
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Modal } from '@patternfly/react-core';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
|
@ -11,20 +11,9 @@ import { resolveRelPath } from '../../Utilities/path';
|
|||
const ShareToRegionsModal = () => {
|
||||
const navigate = useNavigate();
|
||||
const handleClose = () => navigate(resolveRelPath(''));
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { composeId } = useParams();
|
||||
|
||||
const handleToggle = (isOpen: boolean) => setIsOpen(isOpen);
|
||||
|
||||
const handleEscapePress = () => {
|
||||
if (isOpen) {
|
||||
handleToggle(isOpen);
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
const appendTo = useMemo(() => {
|
||||
const modalAnchor = document.querySelector(MODAL_ANCHOR);
|
||||
return modalAnchor === null ? undefined : (modalAnchor as HTMLElement);
|
||||
|
|
@ -44,16 +33,9 @@ const ShareToRegionsModal = () => {
|
|||
title="Share to new region"
|
||||
description="Configure new regions for this image that will run on your AWS. All the
|
||||
regions will launch with the same configuration."
|
||||
onEscapePress={handleEscapePress}
|
||||
appendTo={appendTo}
|
||||
>
|
||||
<RegionsSelect
|
||||
composeId={composeId}
|
||||
handleClose={handleClose}
|
||||
handleToggle={handleToggle}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
/>
|
||||
<RegionsSelect composeId={composeId} handleClose={handleClose} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,18 +40,20 @@ describe('Create Share To Regions Modal', () => {
|
|||
const shareButton = await screen.findByRole('button', { name: /share/i });
|
||||
expect(shareButton).toBeDisabled();
|
||||
|
||||
const selectToggle = screen.getByRole('button', { name: /options menu/i });
|
||||
const selectToggle = screen.getByRole('button', { name: /menu toggle/i });
|
||||
// eslint-disable-next-line testing-library/no-unnecessary-act
|
||||
user.click(selectToggle);
|
||||
|
||||
const usEast2 = await screen.findByRole('option', {
|
||||
name: /us-east-2 us east \(ohio\)/i,
|
||||
name: /us east \(ohio\) us\-east\-2/i,
|
||||
});
|
||||
expect(usEast2).not.toHaveClass('pf-m-disabled');
|
||||
user.click(usEast2);
|
||||
await waitFor(() => expect(shareButton).toBeEnabled());
|
||||
|
||||
const clearAllButton = screen.getByRole('button', { name: /clear all/i });
|
||||
const clearAllButton = screen.getByRole('button', {
|
||||
name: /clear input value/i,
|
||||
});
|
||||
user.click(clearAllButton);
|
||||
await waitFor(() => expect(shareButton).toBeDisabled());
|
||||
|
||||
|
|
@ -97,16 +99,16 @@ describe('Create Share To Regions Modal', () => {
|
|||
renderCustomRoutesWithReduxRouter(`share/${composeId}`, {}, routes);
|
||||
|
||||
const selectToggle = await screen.findByRole('button', {
|
||||
name: /options menu/i,
|
||||
name: /menu toggle/i,
|
||||
});
|
||||
// eslint-disable-next-line testing-library/no-unnecessary-act
|
||||
user.click(selectToggle);
|
||||
|
||||
// parent region disabled
|
||||
const usEast1 = await screen.findByRole('option', {
|
||||
name: /us-east-1 us east \(n. virginia\)/i,
|
||||
name: /us east \(n. virginia\) us-east-1/i,
|
||||
});
|
||||
expect(usEast1).toHaveClass('pf-m-disabled');
|
||||
expect(usEast1).toBeDisabled();
|
||||
|
||||
// close the select again to avoid state update
|
||||
// eslint-disable-next-line testing-library/no-unnecessary-act
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue