1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +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:
Chaim Lev-Ari 2022-09-21 10:10:58 +03:00 committed by GitHub
parent 1e21961e6a
commit ceaee4e175
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1188 additions and 625 deletions

View 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;
}

View 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);
}

View file

@ -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);
}

View file

@ -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}
/>