mirror of https://github.com/portainer/portainer
feat(docker/containers): limit items on volume selector [EE-7077] (#11845)
parent
d7b412eccc
commit
50fd7c6286
|
@ -96,7 +96,7 @@
|
|||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.input-group .portainer-selector-root .portainer-selector__control:not(:last-child) {
|
||||
.input-group .portainer-selector-root .portainer-selector__control:not(:last-child, .portainer-selector__control--menu-is-open) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import ReactSelectCreatable, {
|
||||
CreatableProps as ReactSelectCreatableProps,
|
||||
} from 'react-select/creatable';
|
||||
import ReactSelectAsync, {
|
||||
AsyncProps as ReactSelectAsyncProps,
|
||||
} from 'react-select/async';
|
||||
import ReactSelect, {
|
||||
GroupBase,
|
||||
OptionsOrGroups,
|
||||
Props as ReactSelectProps,
|
||||
} from 'react-select';
|
||||
import clsx from 'clsx';
|
||||
import { RefAttributes } from 'react';
|
||||
import { RefAttributes, useMemo } from 'react';
|
||||
import ReactSelectType from 'react-select/dist/declarations/src/Select';
|
||||
|
||||
import './ReactSelect.css';
|
||||
|
@ -56,9 +60,24 @@ export function Select<
|
|||
className,
|
||||
isCreatable = false,
|
||||
size = 'md',
|
||||
|
||||
...props
|
||||
}: Props<Option, IsMulti, Group> & AutomationTestingProps) {
|
||||
}: Props<Option, IsMulti, Group> &
|
||||
AutomationTestingProps & {
|
||||
isItemVisible?: (item: Option, search: string) => boolean;
|
||||
}) {
|
||||
const Component = isCreatable ? ReactSelectCreatable : ReactSelect;
|
||||
const { options } = props;
|
||||
|
||||
if ((options?.length || 0) > 1000) {
|
||||
return (
|
||||
<TooManyResultsSelector
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
|
@ -84,3 +103,86 @@ export function Creatable<
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Async<
|
||||
Option = DefaultOption,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>,
|
||||
>({
|
||||
className,
|
||||
size,
|
||||
...props
|
||||
}: ReactSelectAsyncProps<Option, IsMulti, Group> & { size?: 'sm' | 'md' }) {
|
||||
return (
|
||||
<ReactSelectAsync
|
||||
className={clsx(className, 'portainer-selector-root', size)}
|
||||
classNamePrefix="portainer-selector"
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TooManyResultsSelector<
|
||||
Option = DefaultOption,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>,
|
||||
>({
|
||||
options,
|
||||
isLoading,
|
||||
getOptionValue,
|
||||
isItemVisible = (item, search) =>
|
||||
!!getOptionValue?.(item).toLowerCase().includes(search.toLowerCase()),
|
||||
...props
|
||||
}: RegularProps<Option, IsMulti, Group> & {
|
||||
isItemVisible?: (item: Option, search: string) => boolean;
|
||||
}) {
|
||||
const defaultOptions = useMemo(() => options?.slice(0, 100), [options]);
|
||||
|
||||
return (
|
||||
<Async
|
||||
isLoading={isLoading}
|
||||
getOptionValue={getOptionValue}
|
||||
loadOptions={(search: string) =>
|
||||
filterOptions<Option, Group>(options, isItemVisible, search)
|
||||
}
|
||||
defaultOptions={defaultOptions}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function filterOptions<
|
||||
Option = DefaultOption,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>,
|
||||
>(
|
||||
options: OptionsOrGroups<Option, Group> | undefined,
|
||||
isItemVisible: (item: Option, search: string) => boolean,
|
||||
search: string
|
||||
): Promise<OptionsOrGroups<Option, Group> | undefined> {
|
||||
return Promise.resolve<OptionsOrGroups<Option, Group> | undefined>(
|
||||
options
|
||||
?.filter((item) =>
|
||||
isGroup(item)
|
||||
? item.options.some((ni) => isItemVisible(ni, search))
|
||||
: isItemVisible(item, search)
|
||||
)
|
||||
.slice(0, 100)
|
||||
);
|
||||
}
|
||||
|
||||
function isGroup<
|
||||
Option = DefaultOption,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>,
|
||||
>(option: Option | Group): option is Group {
|
||||
if (!option) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof option !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'options' in option;
|
||||
}
|
||||
|
|
|
@ -22,16 +22,13 @@ export function VolumeSelector({
|
|||
},
|
||||
});
|
||||
|
||||
if (!volumesQuery.data) {
|
||||
return null;
|
||||
}
|
||||
const initialVolumes = volumesQuery.data || [];
|
||||
|
||||
const volumes = allowAuto
|
||||
? [...volumesQuery.data, { Name: 'auto', Driver: '' }]
|
||||
: volumesQuery.data;
|
||||
? [...initialVolumes, { Name: 'auto', Driver: '' }]
|
||||
: initialVolumes;
|
||||
|
||||
const selectedValue = volumes.find((vol) => vol.Name === value);
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder="Select a volume"
|
||||
|
@ -47,6 +44,7 @@ export function VolumeSelector({
|
|||
onChange={(vol) => onChange(vol?.Name)}
|
||||
inputId={inputId}
|
||||
data-cy="docker-containers-volume-selector"
|
||||
size="sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue