mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
refactor(ui): replace ng selectors with react-select [EE-3608] (#7203)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
This commit is contained in:
parent
1e21961e6a
commit
ceaee4e175
66 changed files with 1188 additions and 625 deletions
|
@ -13,14 +13,14 @@ export default meta;
|
|||
export { Example };
|
||||
|
||||
function Example() {
|
||||
const [selectedTeams, setSelectedTeams] = useState([1]);
|
||||
const [selectedTeams, setSelectedTeams] = useState<readonly number[]>([1]);
|
||||
|
||||
const teams = [createMockTeam(1, 'team1'), createMockTeam(2, 'team2')];
|
||||
|
||||
return (
|
||||
<TeamsSelector
|
||||
value={selectedTeams}
|
||||
onChange={setSelectedTeams}
|
||||
onChange={(value) => setSelectedTeams(value)}
|
||||
teams={teams}
|
||||
placeholder="Select one or more teams"
|
||||
/>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Team, TeamId } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
import { PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
value: TeamId[];
|
||||
onChange(value: TeamId[]): void;
|
||||
value: TeamId[] | readonly TeamId[];
|
||||
onChange(value: readonly TeamId[]): void;
|
||||
teams: Team[];
|
||||
dataCy?: string;
|
||||
inputId?: string;
|
||||
|
@ -21,18 +21,15 @@ export function TeamsSelector({
|
|||
inputId,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
const options = teams.map((team) => ({ label: team.Name, value: team.Id }));
|
||||
|
||||
return (
|
||||
<Select
|
||||
<PortainerSelect<number>
|
||||
name={name}
|
||||
isMulti
|
||||
getOptionLabel={(team) => team.Name}
|
||||
getOptionValue={(team) => String(team.Id)}
|
||||
options={teams}
|
||||
value={teams.filter((team) => value.includes(team.Id))}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={(selectedTeams) =>
|
||||
onChange(selectedTeams.map((team) => team.Id))
|
||||
}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value)}
|
||||
data-cy={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
|
|
|
@ -10,9 +10,17 @@
|
|||
|
||||
.sort-button {
|
||||
background-color: var(--bg-sortbutton-color);
|
||||
color: var(--text-ui-select-color);
|
||||
color: var(--grey-6);
|
||||
border: 1px solid var(--border-sortbutton);
|
||||
display: inline-block;
|
||||
padding: 8px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
:global(:root[theme='dark']) .sort-button {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
:global(:root[theme='highcontrast']) .sort-button {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
|
149
app/react/components/form-components/PortainerSelect.tsx
Normal file
149
app/react/components/form-components/PortainerSelect.tsx
Normal file
|
@ -0,0 +1,149 @@
|
|||
import { OptionsOrGroups } from 'react-select';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
import { Select as ReactSelect } from '@@/form-components/ReactSelect';
|
||||
|
||||
interface Option<TValue> {
|
||||
value: TValue;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type Group<TValue> = { label: string; options: Option<TValue>[] };
|
||||
|
||||
type Options<TValue> = OptionsOrGroups<Option<TValue>, Group<TValue>>;
|
||||
|
||||
interface SharedProps extends AutomationTestingProps {
|
||||
name?: string;
|
||||
inputId?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
isClearable?: boolean;
|
||||
bindToBody?: boolean;
|
||||
}
|
||||
|
||||
interface MultiProps<TValue> extends SharedProps {
|
||||
value: readonly TValue[];
|
||||
onChange(value: readonly TValue[]): void;
|
||||
options: Options<TValue>;
|
||||
isMulti: true;
|
||||
}
|
||||
|
||||
interface SingleProps<TValue> extends SharedProps {
|
||||
value: TValue;
|
||||
onChange(value: TValue | null): void;
|
||||
options: Options<TValue>;
|
||||
isMulti?: never;
|
||||
}
|
||||
|
||||
type Props<TValue> = MultiProps<TValue> | SingleProps<TValue>;
|
||||
|
||||
export function PortainerSelect<TValue = string>(props: Props<TValue>) {
|
||||
return isMultiProps(props) ? (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<MultiSelect {...props} />
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<SingleSelect {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function isMultiProps<TValue>(
|
||||
props: Props<TValue>
|
||||
): props is MultiProps<TValue> {
|
||||
return 'isMulti' in props && !!props.isMulti;
|
||||
}
|
||||
|
||||
export function SingleSelect<TValue = string>({
|
||||
name,
|
||||
options,
|
||||
onChange,
|
||||
value,
|
||||
'data-cy': dataCy,
|
||||
disabled,
|
||||
inputId,
|
||||
placeholder,
|
||||
isClearable,
|
||||
bindToBody,
|
||||
}: SingleProps<TValue>) {
|
||||
const selectedValue = value
|
||||
? _.first(findSelectedOptions<TValue>(options, value))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<ReactSelect<Option<TValue>>
|
||||
name={name}
|
||||
isClearable={isClearable}
|
||||
getOptionLabel={(option) => option.label}
|
||||
getOptionValue={(option) => String(option.value)}
|
||||
options={options}
|
||||
value={selectedValue}
|
||||
onChange={(option) => onChange(option ? option.value : null)}
|
||||
data-cy={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
isDisabled={disabled}
|
||||
menuPortalTarget={bindToBody ? document.body : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function findSelectedOptions<TValue>(
|
||||
options: Options<TValue>,
|
||||
value: TValue | readonly TValue[]
|
||||
) {
|
||||
const valueArr = Array.isArray(value) ? value : [value];
|
||||
return _.compact(
|
||||
options.flatMap((option) => {
|
||||
if (isGroup(option)) {
|
||||
return option.options.find((option) => valueArr.includes(option.value));
|
||||
}
|
||||
|
||||
if (valueArr.includes(option.value)) {
|
||||
return option;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function MultiSelect<TValue = string>({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
'data-cy': dataCy,
|
||||
inputId,
|
||||
placeholder,
|
||||
disabled,
|
||||
isClearable,
|
||||
bindToBody,
|
||||
}: Omit<MultiProps<TValue>, 'isMulti'>) {
|
||||
const selectedOptions = findSelectedOptions(options, value);
|
||||
return (
|
||||
<ReactSelect
|
||||
name={name}
|
||||
isMulti
|
||||
isClearable={isClearable}
|
||||
getOptionLabel={(option) => option.label}
|
||||
getOptionValue={(option) => String(option.value)}
|
||||
options={options}
|
||||
value={selectedOptions}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={(newValue) => onChange(newValue.map((option) => option.value))}
|
||||
data-cy={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
isDisabled={disabled}
|
||||
menuPortalTarget={bindToBody ? document.body : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isGroup<TValue>(
|
||||
option: Option<TValue> | Group<TValue>
|
||||
): option is Group<TValue> {
|
||||
return 'options' in option;
|
||||
}
|
100
app/react/components/form-components/ReactSelect.css
Normal file
100
app/react/components/form-components/ReactSelect.css
Normal file
|
@ -0,0 +1,100 @@
|
|||
.portainer-selector-root {
|
||||
--multi-value-tag-bg: var(--grey-51);
|
||||
--single-value-option-text-color: currentColor;
|
||||
}
|
||||
|
||||
:root[theme='dark'] .portainer-selector-root {
|
||||
--multi-value-tag-bg: var(--grey-3);
|
||||
--single-value-option-text-color: var(--white-color);
|
||||
}
|
||||
|
||||
:root[theme='highcontrast'] .portainer-selector-root {
|
||||
--multi-value-tag-bg: var(--grey-3);
|
||||
--single-value-option-text-color: var(--white-color);
|
||||
}
|
||||
|
||||
/* input style */
|
||||
.portainer-selector-root .portainer-selector__control {
|
||||
border-color: var(--border-form-control-color);
|
||||
background-color: var(--bg-inputbox);
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.portainer-selector-root .portainer-selector__multi-value {
|
||||
background-color: var(--multi-value-tag-bg);
|
||||
}
|
||||
|
||||
.portainer-selector-root .portainer-selector__input-container {
|
||||
color: var(--text-form-control-color);
|
||||
}
|
||||
|
||||
.portainer-selector-root .portainer-selector__dropdown-indicator {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.portainer-selector-root .portainer-selector__multi-value__label {
|
||||
@apply text-black;
|
||||
@apply th-dark:text-white;
|
||||
@apply th-highcontrast:text-white;
|
||||
}
|
||||
|
||||
.portainer-selector-root .portainer-selector__single-value {
|
||||
color: var(--single-value-option-text-color);
|
||||
}
|
||||
|
||||
/* Menu colors */
|
||||
.portainer-selector__menu {
|
||||
--bg-multiselect-color: var(--white-color);
|
||||
--border-multiselect: var(--grey-48);
|
||||
|
||||
--focused-option-bg: var(--ui-gray-3);
|
||||
--focused-option-color: currentColor;
|
||||
|
||||
--selected-option-text-color: var(--grey-7);
|
||||
}
|
||||
|
||||
:root[theme='dark'] .portainer-selector__menu {
|
||||
--bg-multiselect-color: var(--grey-1);
|
||||
--border-multiselect: var(--grey-3);
|
||||
|
||||
--focused-option-bg: var(--blue-2);
|
||||
--focused-option-color: var(--white-color);
|
||||
|
||||
--selected-option-text-color: var(--white);
|
||||
}
|
||||
:root[theme='highcontrast'] .portainer-selector__menu {
|
||||
--bg-multiselect-color: var(--black-color);
|
||||
--border-multiselect: var(--grey-3);
|
||||
|
||||
--focused-option-bg: var(--blue-2);
|
||||
--focused-option-color: var(--white-color);
|
||||
|
||||
--selected-option-text-color: var(--white);
|
||||
}
|
||||
|
||||
.portainer-selector__menu-portal .portainer-selector__menu,
|
||||
.portainer-selector-root .portainer-selector__menu {
|
||||
background-color: var(--bg-multiselect-color);
|
||||
border: 1px solid var(--border-multiselect);
|
||||
padding: 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.portainer-selector__menu-portal .portainer-selector__menu .portainer-selector__option,
|
||||
.portainer-selector-root .portainer-selector__menu .portainer-selector__option {
|
||||
background-color: var(--bg-multiselect-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.portainer-selector__menu-portal .portainer-selector__menu .portainer-selector__option:active,
|
||||
.portainer-selector__menu-portal .portainer-selector__menu .portainer-selector__option--is-focused,
|
||||
.portainer-selector-root .portainer-selector__menu .portainer-selector__option:active,
|
||||
.portainer-selector-root .portainer-selector__menu .portainer-selector__option--is-focused {
|
||||
background-color: var(--focused-option-bg);
|
||||
color: var(--focused-option-color);
|
||||
}
|
||||
|
||||
.portainer-selector__menu-portal .portainer-selector__menu .portainer-selector__option--is-selected,
|
||||
.portainer-selector-root .portainer-selector__menu .portainer-selector__option--is-selected {
|
||||
color: var(--selected-option-text-color);
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
.root :global .selector__control {
|
||||
border: 1px solid var(--border-multiselect);
|
||||
background-color: var(--bg-multiselect-color);
|
||||
}
|
||||
|
||||
.root :global .selector__multi-value {
|
||||
background-color: var(--grey-51);
|
||||
}
|
||||
|
||||
:global :root[theme='dark'] :local .root :global .selector__multi-value {
|
||||
background-color: var(--grey-3);
|
||||
}
|
||||
|
||||
:global :root[theme='highcontrast'] :local .root :global .selector__multi-value {
|
||||
background-color: var(--grey-3);
|
||||
}
|
||||
|
||||
.root :global .selector__multi-value__label {
|
||||
color: var(--black-color);
|
||||
}
|
||||
|
||||
:global :root[theme='dark'] :local .root :global .selector__multi-value__label {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
:global :root[theme='highcontrast'] :local .root :global .selector__multi-value__label {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
.root :global .selector__menu {
|
||||
background-color: var(--bg-multiselect-color);
|
||||
border: 1px solid var(--border-multiselect);
|
||||
padding: 5px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.root :global .selector__option {
|
||||
background-color: var(--bg-multiselect-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.root :global .selector__option:active,
|
||||
.root :global .selector__option--is-focused {
|
||||
background-color: var(--ui-gray-3);
|
||||
}
|
||||
|
||||
:global :root[theme='dark'] :local .root :global .selector__option:active,
|
||||
:global :root[theme='dark'] :local .root :global .selector__option--is-focused {
|
||||
background-color: var(--blue-2);
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
.root :global .selector__option--is-selected {
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
:global :root[theme='highcontrast'] :local .root :global .selector__single-value,
|
||||
:global :root[theme='dark'] :local .root :global .selector__single-value {
|
||||
color: var(--white-color);
|
||||
}
|
||||
|
||||
:global :root[theme='highcontrast'] :local .root :global .selector__input-container,
|
||||
:global :root[theme='dark'] :local .root :global .selector__input-container {
|
||||
color: var(--white-color);
|
||||
}
|
|
@ -1,24 +1,53 @@
|
|||
import ReactSelectCreatable, { CreatableProps } from 'react-select/creatable';
|
||||
import ReactSelect, { GroupBase, Props as SelectProps } from 'react-select';
|
||||
import ReactSelectCreatable, {
|
||||
CreatableProps as ReactSelectCreatableProps,
|
||||
} from 'react-select/creatable';
|
||||
import ReactSelect, {
|
||||
GroupBase,
|
||||
Props as ReactSelectProps,
|
||||
} from 'react-select';
|
||||
import clsx from 'clsx';
|
||||
import { RefAttributes } from 'react';
|
||||
import ReactSelectType from 'react-select/dist/declarations/src/Select';
|
||||
|
||||
import styles from './ReactSelect.module.css';
|
||||
import './ReactSelect.css';
|
||||
|
||||
export function Select<
|
||||
Option = unknown,
|
||||
interface DefaultOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
type RegularProps<
|
||||
Option = DefaultOption,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
className,
|
||||
...props
|
||||
}: SelectProps<Option, IsMulti, Group> &
|
||||
RefAttributes<ReactSelectType<Option, IsMulti, Group>>) {
|
||||
> = { isCreatable?: false } & ReactSelectProps<Option, IsMulti, Group> &
|
||||
RefAttributes<ReactSelectType<Option, IsMulti, Group>>;
|
||||
|
||||
type CreatableProps<
|
||||
Option = DefaultOption,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
> = { isCreatable: true } & ReactSelectCreatableProps<Option, IsMulti, Group>;
|
||||
|
||||
type Props<
|
||||
Option = DefaultOption,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
> =
|
||||
| CreatableProps<Option, IsMulti, Group>
|
||||
| RegularProps<Option, IsMulti, Group>;
|
||||
|
||||
export function Select<
|
||||
Option = DefaultOption,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({ className, isCreatable = false, ...props }: Props<Option, IsMulti, Group>) {
|
||||
const Component = isCreatable ? ReactSelectCreatable : ReactSelect;
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
className={clsx(styles.root, className)}
|
||||
classNamePrefix="selector"
|
||||
<Component
|
||||
className={clsx(className, 'portainer-selector-root')}
|
||||
classNamePrefix="portainer-selector"
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
|
@ -26,14 +55,14 @@ export function Select<
|
|||
}
|
||||
|
||||
export function Creatable<
|
||||
Option = unknown,
|
||||
Option = DefaultOption,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({ className, ...props }: CreatableProps<Option, IsMulti, Group>) {
|
||||
>({ className, ...props }: ReactSelectCreatableProps<Option, IsMulti, Group>) {
|
||||
return (
|
||||
<ReactSelectCreatable
|
||||
className={clsx(styles.root, className)}
|
||||
classNamePrefix="selector"
|
||||
className={clsx(className, 'portainer-selector-root')}
|
||||
classNamePrefix="portainer-selector"
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -9,10 +9,16 @@
|
|||
}
|
||||
|
||||
.sort-button {
|
||||
--text-color: var(--grey-6);
|
||||
background-color: var(--bg-sortbutton-color);
|
||||
color: var(--text-ui-select-color);
|
||||
color: var(--text-color);
|
||||
border: 1px solid var(--border-sortbutton);
|
||||
display: inline-block;
|
||||
padding: 8px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
:global([theme='dark']) .sort-button,
|
||||
:global([theme='highcontrast']) .sort-button {
|
||||
--text-color: var(--white-color);
|
||||
}
|
||||
|
|
34
app/react/edge/components/EdgeGroupsSelector.tsx
Normal file
34
app/react/edge/components/EdgeGroupsSelector.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
import { EdgeGroup } from '../edge-groups/types';
|
||||
|
||||
type SingleValue = EdgeGroup['Id'];
|
||||
|
||||
interface Props {
|
||||
items: EdgeGroup[];
|
||||
value: SingleValue[];
|
||||
onChange: (value: SingleValue[]) => void;
|
||||
}
|
||||
|
||||
export function EdgeGroupsSelector({ items, value, onChange }: Props) {
|
||||
const valueGroups = _.compact(
|
||||
value.map((id) => items.find((item) => item.Id === id))
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={items}
|
||||
isMulti
|
||||
getOptionLabel={(item) => item.Name}
|
||||
getOptionValue={(item) => String(item.Id)}
|
||||
value={valueGroups}
|
||||
onChange={(value) => {
|
||||
onChange(value.map((item) => item.Id));
|
||||
}}
|
||||
placeholder="Select one or multiple group(s)"
|
||||
closeMenuOnSelect={false}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { components, MultiValueGenericProps } from 'react-select';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
interface Option {
|
||||
Name: string;
|
||||
Description: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: Option[];
|
||||
onChange(storageClassName: string, value: readonly Option[]): void;
|
||||
options: Option[];
|
||||
inputId?: string;
|
||||
storageClassName: string;
|
||||
}
|
||||
|
||||
export function StorageAccessModeSelector({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
inputId,
|
||||
storageClassName,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
isMulti
|
||||
getOptionLabel={(option) => option.Description}
|
||||
getOptionValue={(option) => option.Name}
|
||||
components={{ MultiValueLabel }}
|
||||
options={options}
|
||||
value={value}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={(value) => onChange(storageClassName, value)}
|
||||
inputId={inputId}
|
||||
placeholder="Select one or more teams"
|
||||
data-cy={`kubeSetup-storageAccessSelect${storageClassName}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiValueLabel({
|
||||
data,
|
||||
innerProps,
|
||||
selectProps,
|
||||
}: MultiValueGenericProps<Option>) {
|
||||
if (!data || !data.Name) {
|
||||
throw new Error('missing option name');
|
||||
}
|
||||
|
||||
return (
|
||||
<components.MultiValueLabel
|
||||
data={data}
|
||||
innerProps={innerProps}
|
||||
selectProps={selectProps}
|
||||
>
|
||||
{data.Name}
|
||||
</components.MultiValueLabel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
interface Namespace {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
value: string[];
|
||||
onChange(value: string[]): void;
|
||||
namespaces: Namespace[];
|
||||
dataCy?: string;
|
||||
inputId?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function NamespacesSelector({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
namespaces,
|
||||
dataCy,
|
||||
inputId,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
isMulti
|
||||
getOptionLabel={(namespace) => namespace.name}
|
||||
getOptionValue={(namespace) => String(namespace.id)}
|
||||
options={namespaces}
|
||||
value={_.compact(
|
||||
value.map((namespaceName) =>
|
||||
namespaces.find((namespace) => namespace.name === namespaceName)
|
||||
)
|
||||
)}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={(selectedTeams) =>
|
||||
onChange(selectedTeams.map((namespace) => namespace.name))
|
||||
}
|
||||
data-cy={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { User as UserIcon, Users as TeamIcon } from 'react-feather';
|
||||
import { OptionProps, components, MultiValueGenericProps } from 'react-select';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
type Role = { Name: string };
|
||||
type Option = { Type: 'user' | 'team'; Id: number; Name: string; Role: Role };
|
||||
|
||||
interface Props {
|
||||
name?: string;
|
||||
value: Option[];
|
||||
onChange(value: readonly Option[]): void;
|
||||
options: Option[];
|
||||
dataCy?: string;
|
||||
inputId?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function NamespaceAccessUsersSelector({
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
dataCy,
|
||||
inputId,
|
||||
name,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
isMulti
|
||||
name={name}
|
||||
getOptionLabel={(option) => option.Name}
|
||||
getOptionValue={(option) => `${option.Id}-${option.Type}`}
|
||||
options={options}
|
||||
value={value}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={onChange}
|
||||
data-cy={dataCy}
|
||||
inputId={inputId}
|
||||
placeholder={placeholder}
|
||||
components={{ MultiValueLabel, Option: OptionComponent }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isOption(option: unknown): option is Option {
|
||||
return !!option && typeof option === 'object' && 'Type' in option;
|
||||
}
|
||||
|
||||
function OptionComponent({ data, ...props }: OptionProps<Option, true>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<components.Option data={data} {...props}>
|
||||
{isOption(data) && <Label option={data} />}
|
||||
</components.Option>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiValueLabel({
|
||||
data,
|
||||
...props
|
||||
}: MultiValueGenericProps<Option, true>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<components.MultiValueLabel data={data} {...props}>
|
||||
{isOption(data) && <Label option={data} />}
|
||||
</components.MultiValueLabel>
|
||||
);
|
||||
}
|
||||
|
||||
function Label({ option }: { option: Option }) {
|
||||
const Icon = option.Type === 'user' ? UserIcon : TeamIcon;
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
<Icon />
|
||||
<span>{option.Name}</span>
|
||||
<span>|</span>
|
||||
<span>{option.Role.Name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { Registry } from '@/portainer/environments/environment.service/registries';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
interface Props {
|
||||
value: Registry[];
|
||||
onChange(value: readonly Registry[]): void;
|
||||
options: Registry[];
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
export function CreateNamespaceRegistriesSelector({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
inputId,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
isMulti
|
||||
getOptionLabel={(option) => option.Name}
|
||||
getOptionValue={(option) => String(option.Id)}
|
||||
options={options}
|
||||
value={value}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={onChange}
|
||||
inputId={inputId}
|
||||
data-cy="namespaceCreate-registrySelect"
|
||||
placeholder="Select one or more registry"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { User as UserIcon, Users as TeamIcon } from 'react-feather';
|
||||
import { OptionProps, components, MultiValueGenericProps } from 'react-select';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
type Option = { Type: 'user' | 'team'; Id: number; Name: string };
|
||||
|
||||
interface Props {
|
||||
value: Option[];
|
||||
onChange(value: readonly Option[]): void;
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
export function PorAccessManagementUsersSelector({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="form-group">
|
||||
<label
|
||||
className="col-sm-3 col-lg-2 control-label text-left"
|
||||
htmlFor="users-selector"
|
||||
>
|
||||
Select user(s) and/or team(s)
|
||||
</label>
|
||||
<div className="col-sm-9 col-lg-4">
|
||||
{options.length === 0 ? (
|
||||
<span className="small text-muted">No users or teams available.</span>
|
||||
) : (
|
||||
<Select
|
||||
isMulti
|
||||
getOptionLabel={(option) => option.Name}
|
||||
getOptionValue={(option) => `${option.Id}-${option.Type}`}
|
||||
options={options}
|
||||
value={value}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={onChange}
|
||||
data-cy="component-selectUser"
|
||||
inputId="users-selector"
|
||||
placeholder="Select one or more users and/or teams"
|
||||
components={{ MultiValueLabel, Option: OptionComponent }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isOption(option: unknown): option is Option {
|
||||
return !!option && typeof option === 'object' && 'Type' in option;
|
||||
}
|
||||
|
||||
function OptionComponent({ data, ...props }: OptionProps<Option, true>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<components.Option data={data} {...props}>
|
||||
{isOption(data) && <Label option={data} />}
|
||||
</components.Option>
|
||||
);
|
||||
}
|
||||
|
||||
function MultiValueLabel({
|
||||
data,
|
||||
...props
|
||||
}: MultiValueGenericProps<Option, true>) {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<components.MultiValueLabel data={data} {...props}>
|
||||
{isOption(data) && <Label option={data} />}
|
||||
</components.MultiValueLabel>
|
||||
);
|
||||
}
|
||||
|
||||
function Label({ option }: { option: Option }) {
|
||||
const Icon = option.Type === 'user' ? UserIcon : TeamIcon;
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
<Icon />
|
||||
<span>{option.Name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { Team } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
interface Props {
|
||||
value: Team[];
|
||||
onChange(value: readonly Team[]): void;
|
||||
options: Team[];
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
// to be removed with the angularjs app/portainer/components/accessControlForm
|
||||
export function PorAccessControlFormTeamSelector({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
inputId,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
isMulti
|
||||
getOptionLabel={(option) => option.Name}
|
||||
getOptionValue={(option) => String(option.Id)}
|
||||
options={options}
|
||||
value={value}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={onChange}
|
||||
data-cy="portainer-selectTeamAccess"
|
||||
inputId={inputId}
|
||||
placeholder="Select one or more teams"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { User } from '@/portainer/users/types';
|
||||
|
||||
import { Select } from '@@/form-components/ReactSelect';
|
||||
|
||||
interface Props {
|
||||
value: User[];
|
||||
onChange(value: readonly User[]): void;
|
||||
options: User[];
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
// to be removed with the angularjs app/portainer/components/accessControlForm
|
||||
export function PorAccessControlFormUserSelector({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
inputId,
|
||||
}: Props) {
|
||||
return (
|
||||
<Select
|
||||
isMulti
|
||||
getOptionLabel={(option) => option.Username}
|
||||
getOptionValue={(option) => String(option.Id)}
|
||||
options={options}
|
||||
value={value}
|
||||
closeMenuOnSelect={false}
|
||||
onChange={onChange}
|
||||
data-cy="portainer-selectUserAccess"
|
||||
inputId={inputId}
|
||||
placeholder="Select one or more teams"
|
||||
/>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue