284 lines
7.6 KiB
TypeScript
284 lines
7.6 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
|
|
import {
|
|
ActionGroup,
|
|
Button,
|
|
Form,
|
|
FormGroup,
|
|
FormHelperText,
|
|
HelperText,
|
|
HelperTextItem,
|
|
Label,
|
|
LabelGroup,
|
|
MenuToggle,
|
|
Popover,
|
|
Select,
|
|
SelectList,
|
|
SelectOption,
|
|
TextInputGroup,
|
|
TextInputGroupMain,
|
|
TextInputGroupUtilities,
|
|
ValidatedOptions,
|
|
} from '@patternfly/react-core';
|
|
import { MenuToggleElement } from '@patternfly/react-core/dist/esm/components/MenuToggle/MenuToggle';
|
|
import {
|
|
ExclamationCircleIcon,
|
|
HelpIcon,
|
|
TimesIcon,
|
|
} from '@patternfly/react-icons';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
import { AWS_REGIONS } from '../../constants';
|
|
import { useCloneComposeWithNotification as useCloneComposeMutation } from '../../Hooks';
|
|
import {
|
|
ComposeStatus,
|
|
useGetComposeStatusQuery,
|
|
} from '../../store/imageBuilderApi';
|
|
import { resolveRelPath } from '../../Utilities/path';
|
|
|
|
const generateRequests = (
|
|
composeId: string,
|
|
composeStatus: ComposeStatus,
|
|
regions: string[],
|
|
) => {
|
|
return regions.map((region) => {
|
|
const options =
|
|
composeStatus.request.image_requests[0].upload_request.options;
|
|
return {
|
|
composeId: composeId,
|
|
cloneRequest: {
|
|
region: region,
|
|
share_with_sources:
|
|
'share_with_sources' in options
|
|
? options.share_with_sources
|
|
: undefined,
|
|
share_with_accounts:
|
|
'share_with_accounts' in options
|
|
? options.share_with_accounts
|
|
: undefined,
|
|
},
|
|
};
|
|
});
|
|
};
|
|
|
|
type RegionsSelectPropTypes = {
|
|
composeId: string;
|
|
handleClose: () => void;
|
|
};
|
|
|
|
const RegionsSelect = ({ composeId, handleClose }: RegionsSelectPropTypes) => {
|
|
const navigate = useNavigate();
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [validated, setValidated] = useState<ValidatedOptions>(
|
|
ValidatedOptions.default,
|
|
);
|
|
|
|
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, isOpen, initialRegions]);
|
|
|
|
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 { trigger: cloneCompose } = useCloneComposeMutation();
|
|
|
|
const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({
|
|
composeId,
|
|
});
|
|
|
|
if (!isSuccess) {
|
|
return undefined;
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
setIsSaving(true);
|
|
const requests = generateRequests(composeId, composeStatus!, selected);
|
|
await Promise.allSettled(requests.map((request) => cloneCompose(request)));
|
|
navigate(resolveRelPath(''));
|
|
};
|
|
|
|
const handleToggle = () => {
|
|
if (!selected.length) setValidated(ValidatedOptions.error);
|
|
setIsOpen(!isOpen);
|
|
};
|
|
|
|
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
|
|
<MenuToggle
|
|
variant='typeahead'
|
|
onClick={handleToggle}
|
|
ref={toggleRef}
|
|
isExpanded={isOpen}
|
|
>
|
|
<TextInputGroup isPlain>
|
|
<TextInputGroupMain
|
|
value={inputValue}
|
|
onClick={handleToggle}
|
|
onChange={onTextInputChange}
|
|
placeholder='Select region'
|
|
isExpanded={isOpen}
|
|
>
|
|
<LabelGroup aria-label='Selected regions'>
|
|
{selected.map((selection, index) => (
|
|
<Label
|
|
key={index}
|
|
onClose={(ev) => {
|
|
ev.stopPropagation();
|
|
onSelect(selection);
|
|
}}
|
|
>
|
|
{selection}
|
|
</Label>
|
|
))}
|
|
</LabelGroup>
|
|
</TextInputGroupMain>
|
|
<TextInputGroupUtilities>
|
|
{selected.length > 0 && (
|
|
<Button
|
|
icon={<TimesIcon aria-hidden />}
|
|
variant='plain'
|
|
onClick={() => {
|
|
setInputValue('');
|
|
setSelected([]);
|
|
setValidated(ValidatedOptions.error);
|
|
}}
|
|
aria-label='Clear input value'
|
|
/>
|
|
)}
|
|
</TextInputGroupUtilities>
|
|
</TextInputGroup>
|
|
</MenuToggle>
|
|
);
|
|
|
|
return (
|
|
<Form>
|
|
<span id='Clone this image' hidden>
|
|
Select a region
|
|
</span>
|
|
<FormGroup
|
|
label='Select region'
|
|
isRequired
|
|
labelHelp={
|
|
<Popover
|
|
headerContent={<div>Sharing images to other regions</div>}
|
|
bodyContent={
|
|
<div>
|
|
Your image will be built, uploaded to AWS, and shared to the
|
|
regions you select. The shared image will expire within 14 days.
|
|
To permanently access the image, copy the image, which will be
|
|
shared to your account by Red Hat, to your own AWS account.
|
|
</div>
|
|
}
|
|
>
|
|
<Button
|
|
icon={<HelpIcon />}
|
|
variant='plain'
|
|
aria-label='About regions'
|
|
className='pf-v6-u-pl-sm header-button'
|
|
isInline
|
|
/>
|
|
</Popover>
|
|
}
|
|
>
|
|
<Select
|
|
isScrollable
|
|
isOpen={isOpen}
|
|
selected={selected}
|
|
onSelect={(ev, selection) => onSelect(selection as string)}
|
|
onOpenChange={handleToggle}
|
|
toggle={toggle}
|
|
>
|
|
<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>
|
|
<HelperText>
|
|
<HelperTextItem
|
|
icon={<ExclamationCircleIcon />}
|
|
variant={validated}
|
|
>
|
|
Select at least one region to share to.
|
|
</HelperTextItem>
|
|
</HelperText>
|
|
</FormHelperText>
|
|
)}
|
|
</FormGroup>
|
|
<ActionGroup>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
variant='primary'
|
|
key='share'
|
|
isDisabled={selected.length === 0 || isSaving}
|
|
isLoading={isSaving}
|
|
>
|
|
Share
|
|
</Button>
|
|
<Button variant='link' onClick={handleClose} key='cancel'>
|
|
Cancel
|
|
</Button>
|
|
</ActionGroup>
|
|
</Form>
|
|
);
|
|
};
|
|
|
|
export default RegionsSelect;
|