mirror of https://github.com/portainer/portainer
refactor(ui): replace ng selectors with react-select [EE-3608] (#7203)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>pull/7592/head^2
parent
1e21961e6a
commit
ceaee4e175
@ -0,0 +1,11 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export function trimSHA(imageName: string) {
|
||||||
|
if (!imageName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (imageName.indexOf('sha256:') === 0) {
|
||||||
|
return imageName.substring(7, 19);
|
||||||
|
}
|
||||||
|
return _.split(imageName, '@sha256')[0];
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
<!-- on-select/on-remove are called with model because ui-select uses 2-way-binding -->
|
|
||||||
<ui-select
|
|
||||||
multiple
|
|
||||||
ng-model="$ctrl.model"
|
|
||||||
close-on-select="false"
|
|
||||||
on-select="$ctrl.onChange($ctrl.model)"
|
|
||||||
on-remove="$ctrl.onChange($ctrl.model)"
|
|
||||||
data-cy="edgeGroupCreate-edgeGroupsSelector"
|
|
||||||
>
|
|
||||||
<ui-select-match placeholder="Select one or multiple group(s)">
|
|
||||||
<span>
|
|
||||||
{{ $item.Name }}
|
|
||||||
</span>
|
|
||||||
</ui-select-match>
|
|
||||||
<ui-select-choices repeat="item.Id as item in $ctrl.items | filter: { Name: $select.search }">
|
|
||||||
<span>
|
|
||||||
{{ item.Name }}
|
|
||||||
</span>
|
|
||||||
</ui-select-choices>
|
|
||||||
</ui-select>
|
|
@ -1,10 +0,0 @@
|
|||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('edgeGroupsSelector', {
|
|
||||||
templateUrl: './edgeGroupsSelector.html',
|
|
||||||
bindings: {
|
|
||||||
model: '<',
|
|
||||||
items: '<',
|
|
||||||
onChange: '<',
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,6 +1,11 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
export const componentsModule = angular.module(
|
import { EdgeGroupsSelector } from '@/react/edge/components/EdgeGroupsSelector';
|
||||||
'portainer.edge.react.components',
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
[]
|
|
||||||
).name;
|
export const componentsModule = angular
|
||||||
|
.module('portainer.edge.react.components', [])
|
||||||
|
.component(
|
||||||
|
'edgeGroupsSelector',
|
||||||
|
r2a(EdgeGroupsSelector, ['items', 'onChange', 'value'])
|
||||||
|
).name;
|
||||||
|
@ -1,6 +1,53 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
export const componentsModule = angular.module(
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
'portainer.kubernetes.react.components',
|
import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessView/NamespacesSelector';
|
||||||
[]
|
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/StorageAccessModeSelector';
|
||||||
).name;
|
import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/AccessView/NamespaceAccessUsersSelector';
|
||||||
|
import { CreateNamespaceRegistriesSelector } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector';
|
||||||
|
|
||||||
|
export const componentsModule = angular
|
||||||
|
.module('portainer.kubernetes.react.components', [])
|
||||||
|
.component(
|
||||||
|
'namespacesSelector',
|
||||||
|
r2a(NamespacesSelector, [
|
||||||
|
'dataCy',
|
||||||
|
'inputId',
|
||||||
|
'name',
|
||||||
|
'namespaces',
|
||||||
|
'onChange',
|
||||||
|
'placeholder',
|
||||||
|
'value',
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'storageAccessModeSelector',
|
||||||
|
r2a(StorageAccessModeSelector, [
|
||||||
|
'inputId',
|
||||||
|
'onChange',
|
||||||
|
'options',
|
||||||
|
'value',
|
||||||
|
'storageClassName',
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'namespaceAccessUsersSelector',
|
||||||
|
r2a(NamespaceAccessUsersSelector, [
|
||||||
|
'inputId',
|
||||||
|
'onChange',
|
||||||
|
'options',
|
||||||
|
'value',
|
||||||
|
'dataCy',
|
||||||
|
'placeholder',
|
||||||
|
'name',
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'createNamespaceRegistriesSelector',
|
||||||
|
r2a(CreateNamespaceRegistriesSelector, [
|
||||||
|
'inputId',
|
||||||
|
'onChange',
|
||||||
|
'options',
|
||||||
|
'value',
|
||||||
|
])
|
||||||
|
).name;
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { porAccessManagement } from './por-access-management';
|
import { porAccessManagement } from './por-access-management';
|
||||||
import { porAccessManagementUsersSelector } from './por-access-management-users-selector';
|
|
||||||
|
|
||||||
export default angular
|
export default angular.module('portainer.app.component.access-management', []).component('porAccessManagement', porAccessManagement).name;
|
||||||
.module('portainer.app.component.access-management', [])
|
|
||||||
.component('porAccessManagement', porAccessManagement)
|
|
||||||
.component('porAccessManagementUsersSelector', porAccessManagementUsersSelector).name;
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
export const porAccessManagementUsersSelector = {
|
|
||||||
templateUrl: './por-access-management-users-selector.html',
|
|
||||||
bindings: {
|
|
||||||
options: '<',
|
|
||||||
value: '=',
|
|
||||||
},
|
|
||||||
};
|
|
@ -1,20 +0,0 @@
|
|||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-3 col-lg-2 control-label text-left vertical-center"> Select user(s) and/or team(s) </label>
|
|
||||||
<div class="col-sm-9 col-lg-4 vertical-center">
|
|
||||||
<span class="small text-muted" ng-if="$ctrl.options.length === 0"> No users or teams available. </span>
|
|
||||||
<span
|
|
||||||
isteven-multi-select
|
|
||||||
ng-if="$ctrl.options.length > 0"
|
|
||||||
input-model="$ctrl.options"
|
|
||||||
output-model="$ctrl.value"
|
|
||||||
button-label="icon Name"
|
|
||||||
item-label="icon Name"
|
|
||||||
tick-property="ticked"
|
|
||||||
helper-elements="filter"
|
|
||||||
search-property="Name"
|
|
||||||
translation="{nothingSelected: 'Select one or more users and/or teams', search: 'Search...'}"
|
|
||||||
data-cy="component-selectUser"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||||||
angular.module('portainer.app').component('endpointSelector', {
|
|
||||||
templateUrl: './endpointSelector.html',
|
|
||||||
controller: 'EndpointSelectorController',
|
|
||||||
bindings: {
|
|
||||||
model: '=',
|
|
||||||
endpoints: '<',
|
|
||||||
groups: '<',
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,8 +0,0 @@
|
|||||||
<ui-select ng-model="$ctrl.model">
|
|
||||||
<ui-select-match placeholder="Select an environment">
|
|
||||||
<span>{{ $select.selected.Name }}</span>
|
|
||||||
</ui-select-match>
|
|
||||||
<ui-select-choices group-by="$ctrl.groupEndpoints" group-filter="$ctrl.sortGroups" repeat="endpoint in ($ctrl.endpoints | filter: $select.search) track by endpoint.Id">
|
|
||||||
<span>{{ endpoint.Name }}</span>
|
|
||||||
</ui-select-choices>
|
|
||||||
</ui-select>
|
|
@ -1,35 +0,0 @@
|
|||||||
import _ from 'lodash-es';
|
|
||||||
|
|
||||||
angular.module('portainer.app').controller('EndpointSelectorController', function () {
|
|
||||||
var ctrl = this;
|
|
||||||
|
|
||||||
this.sortGroups = function (groups) {
|
|
||||||
return _.sortBy(groups, ['name']);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.groupEndpoints = function (endpoint) {
|
|
||||||
for (var i = 0; i < ctrl.availableGroups.length; i++) {
|
|
||||||
var group = ctrl.availableGroups[i];
|
|
||||||
|
|
||||||
if (endpoint.GroupId === group.Id) {
|
|
||||||
return group.Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$onInit = function () {
|
|
||||||
this.availableGroups = filterEmptyGroups(this.groups, this.endpoints);
|
|
||||||
};
|
|
||||||
|
|
||||||
function filterEmptyGroups(groups, endpoints) {
|
|
||||||
return groups.filter(function f(group) {
|
|
||||||
for (var i = 0; i < endpoints.length; i++) {
|
|
||||||
var endpoint = endpoints[i];
|
|
||||||
if (endpoint.GroupId === group.Id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -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;
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
@ -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…
Reference in new issue