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
|
@ -692,60 +692,6 @@ input[type='checkbox'] {
|
|||
|
||||
/*!bootbox override*/
|
||||
|
||||
/*angular-multi-select override*/
|
||||
.multiSelect > button {
|
||||
min-height: 30px !important;
|
||||
background-image: var(--bg-image-multiselect-button);
|
||||
border-color: var(--border-multiselect);
|
||||
color: var(--text-multiselect);
|
||||
background-color: var(--bg-multiselect-color);
|
||||
}
|
||||
|
||||
.multiSelect > button:hover {
|
||||
background-image: var(--bg-image-multiselect-hover);
|
||||
}
|
||||
|
||||
.multiSelect .checkboxLayer {
|
||||
border-color: var(--border-multiselect-checkboxlayer);
|
||||
}
|
||||
|
||||
.multiSelect .checkBoxContainer {
|
||||
background-color: var(--bg-multiselect-checkboxcontainer);
|
||||
}
|
||||
|
||||
.multiSelect .multiSelectItem {
|
||||
color: var(--text-multiselect-item);
|
||||
}
|
||||
|
||||
.multiSelect .helperContainer {
|
||||
background-color: var(--bg-multiselect-helpercontainer);
|
||||
}
|
||||
|
||||
.multiSelect .multiSelectFocus {
|
||||
background-image: var(--bg-image-multiselect);
|
||||
}
|
||||
|
||||
.multiSelect .multiSelectItem:not(.multiSelectGroup).selected {
|
||||
background-image: var(--bg-image-multiselect);
|
||||
color: var(--white-color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.multiSelect .multiSelectItem:hover,
|
||||
.multiSelect .multiSelectGroup:hover {
|
||||
border-color: var(--grey-3);
|
||||
background-image: var(--bg-image-multiselect) !important;
|
||||
color: var(--white-color) !important;
|
||||
}
|
||||
|
||||
.multiSelect .tickMark,
|
||||
.widget .widget-body table tbody .multiSelect .tickMark {
|
||||
top: 2px;
|
||||
right: 12px;
|
||||
font-size: 20px !important;
|
||||
}
|
||||
/*!angular-multi-select override*/
|
||||
|
||||
/*toaster override*/
|
||||
#toast-container > div {
|
||||
opacity: 0.9;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'ui-select/dist/select.css';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import '@fortawesome/fontawesome-free/css/brands.css';
|
||||
import '@fortawesome/fontawesome-free/css/solid.css';
|
||||
|
@ -11,7 +10,6 @@ import 'codemirror/addon/lint/lint.css';
|
|||
import 'angular-json-tree/dist/angular-json-tree.css';
|
||||
import 'angular-loading-bar/build/loading-bar.css';
|
||||
import 'angular-moment-picker/dist/angular-moment-picker.min.css';
|
||||
import 'angular-multiselect/isteven-multi-select.css';
|
||||
import 'spinkit/spinkit.min.css';
|
||||
import '@reach/menu-button/styles.css';
|
||||
|
||||
|
|
|
@ -125,7 +125,6 @@
|
|||
--border-pagination-color: var(--ui-white);
|
||||
--bg-pagination-span-color: var(--white-color);
|
||||
--bg-pagination-hover-color: var(--ui-blue-3);
|
||||
--bg-ui-select-hover-color: var(--grey-14);
|
||||
--bg-motd-body-color: var(--grey-20);
|
||||
--bg-item-highlighted-color: var(--grey-21);
|
||||
--bg-item-highlighted-null-color: var(--grey-14);
|
||||
|
@ -135,7 +134,6 @@
|
|||
--bg-input-sm-color: var(--white-color);
|
||||
--bg-app-datatable-thead: var(--grey-23);
|
||||
--bg-app-datatable-tbody: var(--grey-24);
|
||||
--bg-multiselect-color: var(--white-color);
|
||||
--bg-daterangepicker-color: var(--white-color);
|
||||
--bg-calendar-color: var(--white-color);
|
||||
--bg-calendar-table-color: var(--white-color);
|
||||
|
@ -194,8 +192,6 @@
|
|||
--text-pagination-color: var(--grey-26);
|
||||
--text-pagination-span-color: var(--grey-3);
|
||||
--text-pagination-span-hover-color: var(--grey-3);
|
||||
--text-ui-select-color: var(--grey-6);
|
||||
--text-ui-select-hover-color: var(--grey-28);
|
||||
--text-summary-color: var(--black-color);
|
||||
--text-tooltip-color: var(--white-color);
|
||||
--text-rzslider-color: var(--grey-36);
|
||||
|
@ -253,16 +249,6 @@
|
|||
--button-opacity: 0.2;
|
||||
--button-opacity-hover: 0.5;
|
||||
|
||||
--bg-image-multiselect: linear-gradient(var(--blue-2), var(--blue-2));
|
||||
--bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17));
|
||||
--bg-image-multiselect-hover: linear-gradient(var(--white-color), var(--grey-43));
|
||||
--border-multiselect: var(--grey-48);
|
||||
--border-multiselect-checkboxlayer: var(--grey-51);
|
||||
--text-multiselect: var(--grey-29);
|
||||
--text-multiselect-selectitem: var(--white-color);
|
||||
--bg-multiselect-checkboxcontainer: var(--white-color);
|
||||
--text-multiselect-item: var(--grey-30);
|
||||
--bg-multiselect-helpercontainer: var(--white-color);
|
||||
--text-input-textarea: var(--white-color);
|
||||
|
||||
--user-menu-icon-color: var(--ui-gray-4);
|
||||
|
@ -315,7 +301,6 @@
|
|||
--bg-pagination-color: var(--grey-3);
|
||||
--bg-pagination-span-color: var(--grey-1);
|
||||
--bg-pagination-hover-color: var(--grey-3);
|
||||
--bg-ui-select-hover-color: var(--grey-3);
|
||||
--bg-motd-body-color: var(--grey-1);
|
||||
--bg-item-highlighted-color: var(--grey-2);
|
||||
--bg-item-highlighted-null-color: var(--grey-2);
|
||||
|
@ -328,7 +313,6 @@
|
|||
--bg-app-datatable-thead: var(--grey-1);
|
||||
--bg-service-datatable-tbody: var(--grey-1);
|
||||
--bg-app-datatable-tbody: var(--grey-1);
|
||||
--bg-multiselect-color: var(--grey-1);
|
||||
--bg-daterangepicker-color: var(--grey-3);
|
||||
--bg-calendar-color: var(--grey-3);
|
||||
--bg-calendar-table-color: var(--grey-3);
|
||||
|
@ -386,8 +370,6 @@
|
|||
--text-pagination-color: var(--white-color);
|
||||
--text-pagination-span-color: var(--ui-white);
|
||||
--text-pagination-span-hover-color: var(--ui-white);
|
||||
--text-ui-select-color: var(--white-color);
|
||||
--text-ui-select-hover-color: var(--white-color);
|
||||
--text-summary-color: var(--white-color);
|
||||
--text-boxselector-wrapper-color: var(--white-color);
|
||||
--text-tooltip-color: var(--white-color);
|
||||
|
@ -447,15 +429,6 @@
|
|||
--shadow-box-color: none;
|
||||
--shadow-boxselector-color: none;
|
||||
|
||||
--bg-image-multiselect: linear-gradient(var(--grey-38), var(--grey-38));
|
||||
--bg-image-multiselect-button: linear-gradient(var(--grey-1), var(--grey-1));
|
||||
--bg-image-multiselect-hover: linear-gradient(var(--grey-3), var(--grey-3));
|
||||
--border-multiselect: var(--grey-3);
|
||||
--border-multiselect-checkboxlayer: var(--grey-3);
|
||||
--text-multiselect: var(--white-color);
|
||||
--bg-multiselect-checkboxcontainer: var(--grey-3);
|
||||
--text-multiselect-item: var(--white-color);
|
||||
--bg-multiselect-helpercontainer: var(--grey-1);
|
||||
--text-input-textarea: var(--grey-1);
|
||||
|
||||
--user-menu-icon-color: var(--grey-3);
|
||||
|
@ -508,7 +481,6 @@
|
|||
--bg-app-datatable-tbody: var(--black-color);
|
||||
--bg-pagination-color: var(--grey-3);
|
||||
--bg-pagination-span-color: var(--ui-black);
|
||||
--bg-multiselect-color: var(--grey-1);
|
||||
--bg-daterangepicker-color: var(--black-color);
|
||||
--bg-calendar-color: var(--black-color);
|
||||
--bg-calendar-table-color: var(--black-color);
|
||||
|
@ -568,7 +540,6 @@
|
|||
--text-daterangepicker-end-date: var(--ui-white);
|
||||
--text-daterangepicker-in-range: var(--white-color);
|
||||
--text-daterangepicker-active: var(--white-color);
|
||||
--text-ui-select-color: var(--white-color);
|
||||
--text-json-tree-color: var(--white-color);
|
||||
--text-json-tree-leaf-color: var(--white-color);
|
||||
--text-json-tree-branch-preview-color: var(--white-color);
|
||||
|
@ -619,15 +590,6 @@
|
|||
--shadow-box-color: none;
|
||||
--shadow-boxselector-color: none;
|
||||
|
||||
--bg-image-multiselect: linear-gradient(var(--black-color), var(--black-color));
|
||||
--bg-image-multiselect-button: linear-gradient(var(--grey-1), var(--grey-1));
|
||||
--bg-image-multiselect-hover: linear-gradient(var(--grey-3), var(--grey-3));
|
||||
--border-multiselect: var(--black-color);
|
||||
--border-multiselect-checkboxlayer: var(--grey-3);
|
||||
--text-multiselect: var(--white-color);
|
||||
--bg-multiselect-checkboxcontainer: var(--grey-3);
|
||||
--text-multiselect-item: var(--white-color);
|
||||
--bg-multiselect-helpercontainer: var(--grey-1);
|
||||
--text-input-textarea: var(--black-color);
|
||||
--bg-item-highlighted-null-color: var(--grey-2);
|
||||
--text-cm-default-color: var(--blue-9);
|
||||
|
|
|
@ -245,37 +245,6 @@ json-tree .branch-preview {
|
|||
background-color: var(--bg-progress-color);
|
||||
}
|
||||
|
||||
.ui-select-search,
|
||||
.ui-select-toggle {
|
||||
height: 30px;
|
||||
min-width: 260px;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.ui-select-toggle {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.ui-select-match-text {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ui-select-match-text > a {
|
||||
verical-align: middle;
|
||||
}
|
||||
|
||||
.ui-select-bootstrap .ui-select-choices-row > span {
|
||||
color: var(--text-ui-select-color);
|
||||
}
|
||||
|
||||
.ui-select-bootstrap .ui-select-choices-row > span:hover,
|
||||
.ui-select-bootstrap .ui-select-choices-row > span:focus {
|
||||
background-color: var(--bg-ui-select-hover-color);
|
||||
color: var(--text-ui-select-hover-color);
|
||||
}
|
||||
|
||||
.motd-body {
|
||||
background-color: var(--bg-motd-body-color) !important;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
import { trimSHA } from './utils';
|
||||
|
||||
function includeString(text, values) {
|
||||
return values.some(function (val) {
|
||||
|
@ -253,15 +254,7 @@ angular
|
|||
})
|
||||
.filter('trimshasum', function () {
|
||||
'use strict';
|
||||
return function (imageName) {
|
||||
if (!imageName) {
|
||||
return;
|
||||
}
|
||||
if (imageName.indexOf('sha256:') === 0) {
|
||||
return imageName.substring(7, 19);
|
||||
}
|
||||
return _.split(imageName, '@sha256')[0];
|
||||
};
|
||||
return trimSHA;
|
||||
})
|
||||
.filter('trimversiontag', function () {
|
||||
'use strict';
|
||||
|
|
|
@ -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: '<',
|
||||
},
|
||||
});
|
|
@ -2,7 +2,7 @@
|
|||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<edge-groups-selector model="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
||||
<edge-groups-selector value="$ctrl.model.EdgeGroups" items="$ctrl.edgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@ import { PortainerEndpointTypes } from '@/portainer/models/endpoint/models';
|
|||
|
||||
export class EditEdgeStackFormController {
|
||||
/* @ngInject */
|
||||
constructor() {
|
||||
constructor($scope) {
|
||||
this.$scope = $scope;
|
||||
this.state = {
|
||||
endpointTypes: [],
|
||||
};
|
||||
|
@ -32,9 +33,11 @@ export class EditEdgeStackFormController {
|
|||
}
|
||||
|
||||
onChangeGroups(groups) {
|
||||
this.model.EdgeGroups = groups;
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.model.EdgeGroups = groups;
|
||||
|
||||
this.checkEndpointTypes(groups);
|
||||
this.checkEndpointTypes(groups);
|
||||
});
|
||||
}
|
||||
|
||||
isFormValid() {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import angular from 'angular';
|
||||
|
||||
export const componentsModule = angular.module(
|
||||
'portainer.edge.react.components',
|
||||
[]
|
||||
).name;
|
||||
import { EdgeGroupsSelector } from '@/react/edge/components/EdgeGroupsSelector';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.edge.react.components', [])
|
||||
.component(
|
||||
'edgeGroupsSelector',
|
||||
r2a(EdgeGroupsSelector, ['items', 'onChange', 'value'])
|
||||
).name;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default class CreateEdgeStackViewController {
|
||||
/* @ngInject */
|
||||
constructor($state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async) {
|
||||
Object.assign(this, { $state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async });
|
||||
constructor($state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async, $scope) {
|
||||
Object.assign(this, { $state, $window, ModalService, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async, $scope });
|
||||
|
||||
this.formValues = {
|
||||
Name: '',
|
||||
|
@ -119,9 +119,11 @@ export default class CreateEdgeStackViewController {
|
|||
}
|
||||
|
||||
onChangeGroups(groups) {
|
||||
this.formValues.Groups = groups;
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.Groups = groups;
|
||||
|
||||
this.checkIfEndpointTypes(groups);
|
||||
this.checkIfEndpointTypes(groups);
|
||||
});
|
||||
}
|
||||
|
||||
checkIfEndpointTypes(groups) {
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<div class="col-sm-12 form-section-title"> Edge Groups </div>
|
||||
<div class="form-group" ng-if="$ctrl.edgeGroups">
|
||||
<div class="col-sm-12">
|
||||
<edge-groups-selector ng-if="!$ctrl.noGroups" model="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||
<edge-groups-selector ng-if="!$ctrl.noGroups" value="$ctrl.formValues.Groups" on-change="($ctrl.onChangeGroups)" items="$ctrl.edgeGroups"></edge-groups-selector>
|
||||
</div>
|
||||
<div ng-if="$ctrl.noGroups" class="col-sm-12 small text-muted">
|
||||
No Edge groups are available. Head over to the <a ui-sref="edge.groups">Edge groups view</a> to create one.
|
||||
|
|
|
@ -28,8 +28,6 @@ angular
|
|||
'ui.bootstrap',
|
||||
'ui.router',
|
||||
UI_ROUTER_REACT_HYBRID,
|
||||
'ui.select',
|
||||
'isteven-multi-select',
|
||||
'ngSanitize',
|
||||
'ngFileUpload',
|
||||
'ngMessages',
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
export default class HelmTemplatesListController {
|
||||
/* @ngInject */
|
||||
constructor($async, DatatableService, HelmService, Notifications) {
|
||||
constructor($async, $scope, DatatableService, HelmService, Notifications) {
|
||||
this.$async = $async;
|
||||
this.$scope = $scope;
|
||||
this.DatatableService = DatatableService;
|
||||
this.HelmService = HelmService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.state = {
|
||||
textFilter: '',
|
||||
selectedCategory: '',
|
||||
categories: [],
|
||||
};
|
||||
|
||||
this.updateCategories = this.updateCategories.bind(this);
|
||||
this.onCategoryChange = this.onCategoryChange.bind(this);
|
||||
}
|
||||
|
||||
async updateCategories() {
|
||||
|
@ -16,18 +24,20 @@ export default class HelmTemplatesListController {
|
|||
.filter((a) => a) // filter out undefined/nulls
|
||||
.map((c) => c.category); // get annotation category
|
||||
const availableCategories = [...new Set(annotationCategories)].sort(); // unique and sort
|
||||
this.state.categories = availableCategories;
|
||||
this.state.categories = availableCategories.map((cat) => ({ label: cat, value: cat }));
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve helm charts categories');
|
||||
}
|
||||
}
|
||||
|
||||
onTextFilterChange() {
|
||||
this.DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||
onCategoryChange(value) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.state.selectedCategory = value || '';
|
||||
});
|
||||
}
|
||||
|
||||
clearCategory() {
|
||||
this.state.selectedCategory = '';
|
||||
onTextFilterChange() {
|
||||
this.DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||
}
|
||||
|
||||
$onChanges() {
|
||||
|
@ -38,12 +48,6 @@ export default class HelmTemplatesListController {
|
|||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.state = {
|
||||
textFilter: '',
|
||||
selectedCategory: '',
|
||||
categories: [],
|
||||
};
|
||||
|
||||
const textFilter = this.DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
if (textFilter !== null) {
|
||||
this.state.textFilter = textFilter;
|
||||
|
|
|
@ -22,24 +22,21 @@
|
|||
ng-model-options="{ debounce: 300 }"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ui-select ng-model="$ctrl.state.selectedCategory" theme="bootstrap" append-to-body="true">
|
||||
<ui-select-match placeholder="Select a category">
|
||||
<a class="btn btn-xs btn-link pull-right vertical-center" ng-click="$ctrl.clearCategory()">
|
||||
<pr-icon icon="'x'" feather="true"></pr-icon>
|
||||
</a>
|
||||
<span class="vertical-center">{{ $select.selected }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="category in ($ctrl.state.categories | filter: $select.search)">
|
||||
<span>{{ category }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
<div class="w-1/5">
|
||||
<por-select
|
||||
placeholder="'Select a category'"
|
||||
value="$ctrl.state.selectedCategory"
|
||||
options="$ctrl.state.categories"
|
||||
on-change="($ctrl.onCategoryChange)"
|
||||
is-clearable="true"
|
||||
bind-to-body="true"
|
||||
></por-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blocklist">
|
||||
<helm-templates-list-item
|
||||
ng-repeat="chart in $ctrl.charts | filter:$ctrl.state.textFilter | filter: $ctrl.state.selectedCategory "
|
||||
ng-repeat="chart in $ctrl.charts | filter:$ctrl.state.textFilter | filter: $ctrl.state.selectedCategory"
|
||||
model="chart"
|
||||
type-label="helm"
|
||||
on-select="($ctrl.selectAction)"
|
||||
|
|
|
@ -1,6 +1,53 @@
|
|||
import angular from 'angular';
|
||||
|
||||
export const componentsModule = angular.module(
|
||||
'portainer.kubernetes.react.components',
|
||||
[]
|
||||
).name;
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { NamespacesSelector } from '@/react/kubernetes/cluster/RegistryAccessView/NamespacesSelector';
|
||||
import { StorageAccessModeSelector } from '@/react/kubernetes/cluster/ConfigureView/StorageAccessModeSelector';
|
||||
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;
|
||||
|
|
|
@ -2,8 +2,9 @@ import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
|||
|
||||
export default class KubernetesRegistryAccessController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, ModalService, EndpointService, Notifications, RegistryService, KubernetesResourcePoolService) {
|
||||
constructor($async, $scope, $state, ModalService, EndpointService, Notifications, RegistryService, KubernetesResourcePoolService) {
|
||||
this.$async = $async;
|
||||
this.$scope = $scope;
|
||||
this.$state = $state;
|
||||
this.ModalService = ModalService;
|
||||
this.Notifications = Notifications;
|
||||
|
@ -20,10 +21,11 @@ export default class KubernetesRegistryAccessController {
|
|||
this.savedResourcePools = [];
|
||||
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
this.onChangeResourcePools = this.onChangeResourcePools.bind(this);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
return this.updateNamespaces([...this.savedResourcePools.map(({ value }) => value), ...this.selectedResourcePools.map((pool) => pool.name)]);
|
||||
return this.updateNamespaces([...this.savedResourcePools.map(({ value }) => value), ...this.selectedResourcePools]);
|
||||
}
|
||||
|
||||
handleRemove(namespaces) {
|
||||
|
@ -52,6 +54,12 @@ export default class KubernetesRegistryAccessController {
|
|||
});
|
||||
}
|
||||
|
||||
onChangeResourcePools(resourcePools) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.selectedResourcePools = resourcePools;
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
try {
|
||||
|
|
|
@ -12,19 +12,14 @@
|
|||
<label class="col-sm-3 col-lg-2 control-label text-left" style="padding-top: 0"> Select namespaces </label>
|
||||
<div class="col-sm-9 col-lg-4" style="margin-bottom: 15px">
|
||||
<span class="small text-muted" ng-if="!$ctrl.resourcePools.length"> No namespaces available. </span>
|
||||
<span
|
||||
isteven-multi-select
|
||||
|
||||
<namespaces-selector
|
||||
ng-if="$ctrl.resourcePools.length"
|
||||
input-model="$ctrl.resourcePools"
|
||||
output-model="$ctrl.selectedResourcePools"
|
||||
button-label="name"
|
||||
item-label="name"
|
||||
tick-property="ticked"
|
||||
helper-elements="filter"
|
||||
search-property="name"
|
||||
translation="{nothingSelected: 'Select one or more namespaces', search: 'Search...'}"
|
||||
>
|
||||
</span>
|
||||
value="$ctrl.selectedResourcePools"
|
||||
namespaces="$ctrl.resourcePools"
|
||||
placeholder="'Select one or more namespaces'"
|
||||
on-change="($ctrl.onChangeResourcePools)"
|
||||
></namespaces-selector>
|
||||
</div>
|
||||
<div class="col-sm-12 small text-muted vertical-center">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon>
|
||||
|
|
|
@ -299,19 +299,12 @@
|
|||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
isteven-multi-select
|
||||
input-model="class.availableAccessModes"
|
||||
output-model="class.AccessModes"
|
||||
button-label="Name"
|
||||
item-label="Description"
|
||||
tick-property="selected"
|
||||
directive-id="{{ class.Name }}"
|
||||
helper-elements=""
|
||||
translation="{nothingSelected: 'Not configured'}"
|
||||
data-cy="kubeSetup-storageAccessSelect{{ class.Name }}"
|
||||
>
|
||||
</span>
|
||||
<storage-access-mode-selector
|
||||
options="ctrl.availableAccessModes"
|
||||
value="class.AccessModes"
|
||||
on-change="(ctrl.onChangeStorageClassAccessMode)"
|
||||
storage-class-name="class.Name"
|
||||
></storage-access-mode-selector>
|
||||
</td>
|
||||
<td>
|
||||
<div style="margin: 5px">
|
||||
|
|
|
@ -45,6 +45,7 @@ class KubernetesConfigureController {
|
|||
this.limitedFeatureAutoWindow = FeatureId.HIDE_AUTO_UPDATE_WINDOW;
|
||||
this.onToggleAutoUpdate = this.onToggleAutoUpdate.bind(this);
|
||||
this.onChangeEnableResourceOverCommit = this.onChangeEnableResourceOverCommit.bind(this);
|
||||
this.onChangeStorageClassAccessMode = this.onChangeStorageClassAccessMode.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
|
@ -263,6 +264,18 @@ class KubernetesConfigureController {
|
|||
});
|
||||
}
|
||||
|
||||
onChangeStorageClassAccessMode(storageClassName, accessModes) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
const storageClass = this.StorageClasses.find((item) => item.Name === storageClassName);
|
||||
|
||||
if (!storageClass) {
|
||||
throw new Error('Storage class not found');
|
||||
}
|
||||
|
||||
storageClass.AccessModes = accessModes;
|
||||
});
|
||||
}
|
||||
|
||||
/* #region ON INIT */
|
||||
async onInit() {
|
||||
this.state = {
|
||||
|
@ -288,18 +301,14 @@ class KubernetesConfigureController {
|
|||
};
|
||||
|
||||
try {
|
||||
this.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||
|
||||
[this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(this.state.endpointId), this.EndpointService.endpoint(this.state.endpointId)]);
|
||||
_.forEach(this.StorageClasses, (item) => {
|
||||
item.availableAccessModes = new KubernetesStorageClassAccessPolicies();
|
||||
const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name);
|
||||
if (storage) {
|
||||
item.selected = true;
|
||||
_.forEach(storage.AccessModes, (access) => {
|
||||
const mode = _.find(item.availableAccessModes, { Name: access });
|
||||
if (mode) {
|
||||
mode.selected = true;
|
||||
}
|
||||
});
|
||||
item.AccessModes = storage.AccessModes.map((name) => this.availableAccessModes.find((accessMode) => accessMode.Name === name));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -52,25 +52,19 @@
|
|||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left"> Select user(s) and/or team(s) </label>
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left" for="users-selector"> Select user(s) and/or team(s) </label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
<span class="small text-muted" ng-if="ctrl.availableUsersAndTeams.length === 0">
|
||||
No user nor team access has been set on the environment. Head over to the
|
||||
<a ui-sref="portainer.endpoints">Environments view</a> to manage them.
|
||||
</span>
|
||||
<span
|
||||
isteven-multi-select
|
||||
<namespace-access-users-selector
|
||||
ng-if="ctrl.availableUsersAndTeams.length > 0"
|
||||
input-model="ctrl.availableUsersAndTeams"
|
||||
output-model="ctrl.formValues.multiselectOutput"
|
||||
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...'}"
|
||||
>
|
||||
</span>
|
||||
input-id="users-selector"
|
||||
value="ctrl.formValues.multiselectOutput"
|
||||
options="ctrl.availableUsersAndTeams"
|
||||
on-change="(ctrl.onUsersAndTeamsChange)"
|
||||
></namespace-access-users-selector>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -80,7 +74,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm !ml-0 vertical-center"
|
||||
ng-disabled="(ctrl.availableUsersAndTeams | filter:{ticked:true}).length === 0 || ctrl.actionInProgress"
|
||||
ng-disabled="ctrl.formValues.multiselectOutput.length === 0 || ctrl.actionInProgress"
|
||||
ng-click="ctrl.authorizeAccess()"
|
||||
button-spinner="ctrl.actionInProgress"
|
||||
>
|
||||
|
|
|
@ -6,9 +6,10 @@ import KubernetesConfigMapHelper from 'Kubernetes/helpers/configMapHelper';
|
|||
|
||||
class KubernetesResourcePoolAccessController {
|
||||
/* @ngInject */
|
||||
constructor($async, $state, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, GroupService, AccessService) {
|
||||
constructor($async, $state, $scope, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, GroupService, AccessService) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.$scope = $scope;
|
||||
this.Notifications = Notifications;
|
||||
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||
this.KubernetesConfigMapService = KubernetesConfigMapService;
|
||||
|
@ -19,7 +20,7 @@ class KubernetesResourcePoolAccessController {
|
|||
this.onInit = this.onInit.bind(this);
|
||||
this.authorizeAccessAsync = this.authorizeAccessAsync.bind(this);
|
||||
this.unauthorizeAccessAsync = this.unauthorizeAccessAsync.bind(this);
|
||||
|
||||
this.onUsersAndTeamsChange = this.onUsersAndTeamsChange.bind(this);
|
||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
||||
}
|
||||
|
||||
|
@ -72,6 +73,7 @@ class KubernetesResourcePoolAccessController {
|
|||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
this.availableUsersAndTeams = _.without(endpointAccesses.authorizedUsersAndTeams, ...this.authorizedUsersAndTeams);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve namespace information');
|
||||
|
@ -100,6 +102,12 @@ class KubernetesResourcePoolAccessController {
|
|||
}
|
||||
}
|
||||
|
||||
onUsersAndTeamsChange(value) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.multiselectOutput = value;
|
||||
});
|
||||
}
|
||||
|
||||
authorizeAccess() {
|
||||
return this.$async(this.authorizeAccessAsync);
|
||||
}
|
||||
|
|
|
@ -403,7 +403,7 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left !pt-0"> Select registries </label>
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left !pt-0" for="registries-selector"> Select registries </label>
|
||||
<div class="col-sm-8 col-lg-9">
|
||||
<span class="small text-muted" ng-if="!$ctrl.registries.length && $ctrl.state.isAdmin">
|
||||
No registries available. Head over to the <a ui-sref="portainer.registries">registry view</a> to define a container registry.
|
||||
|
@ -411,20 +411,13 @@
|
|||
<span class="small text-muted" ng-if="!$ctrl.registries.length && !$ctrl.state.isAdmin">
|
||||
No registries available. Contact your administrator to create a container registry.
|
||||
</span>
|
||||
<span
|
||||
isteven-multi-select
|
||||
ng-if="$ctrl.registries.length"
|
||||
input-model="$ctrl.registries"
|
||||
output-model="$ctrl.formValues.Registries"
|
||||
button-label="Name"
|
||||
item-label="Name"
|
||||
tick-property="Checked"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
translation="{nothingSelected: 'Select one or more registries', search: 'Search...'}"
|
||||
data-cy="namespaceCreate-registrySelect"
|
||||
<create-namespace-registries-selector
|
||||
input-id="'registries-selector'"
|
||||
value="$ctrl.formValues.Registries"
|
||||
on-change="($ctrl.onRegistriesChange)"
|
||||
options="$ctrl.registries"
|
||||
>
|
||||
</span>
|
||||
</create-namespace-registries-selector>
|
||||
</div>
|
||||
</div>
|
||||
<!-- #endregion -->
|
||||
|
|
|
@ -39,6 +39,7 @@ class KubernetesCreateResourcePoolController {
|
|||
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||
this.onToggleLoadBalancerQuota = this.onToggleLoadBalancerQuota.bind(this);
|
||||
this.onToggleResourceQuota = this.onToggleResourceQuota.bind(this);
|
||||
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
|
@ -92,6 +93,12 @@ class KubernetesCreateResourcePoolController {
|
|||
});
|
||||
}
|
||||
|
||||
onRegistriesChange(registries) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.Registries = registries;
|
||||
});
|
||||
}
|
||||
|
||||
addHostname(ingressClass) {
|
||||
ingressClass.Hosts.push(new KubernetesResourcePoolIngressClassHostFormValue());
|
||||
}
|
||||
|
|
|
@ -364,27 +364,21 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left !pt-0"> Select registries </label>
|
||||
<div class="col-sm-8 col-lg-9">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left !pt-0" for="registries-selector"> Select registries </label>
|
||||
<div class="col-sm-9 col-lg-4">
|
||||
<span class="small text-muted" ng-if="!ctrl.registries.length && ctrl.isAdmin">
|
||||
No registries available. Head over to the <a ui-sref="portainer.registries">registry view</a> to define a container registry.
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="!ctrl.registries.length && !ctrl.isAdmin">
|
||||
No registries available. Contact your administrator to create a container registry.
|
||||
</span>
|
||||
<span
|
||||
isteven-multi-select
|
||||
ng-if="ctrl.registries.length"
|
||||
input-model="ctrl.registries"
|
||||
output-model="ctrl.formValues.Registries"
|
||||
button-label="Name"
|
||||
item-label="Name"
|
||||
tick-property="Checked"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
translation="{nothingSelected: 'Select one or more registries', search: 'Search...'}"
|
||||
<create-namespace-registries-selector
|
||||
input-id="'registries-selector'"
|
||||
value="ctrl.formValues.Registries"
|
||||
on-change="(ctrl.onRegistriesChange)"
|
||||
options="ctrl.registries"
|
||||
>
|
||||
</span>
|
||||
</create-namespace-registries-selector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -73,6 +73,7 @@ class KubernetesResourcePoolController {
|
|||
this.getEvents = this.getEvents.bind(this);
|
||||
this.onToggleLoadBalancersQuota = this.onToggleLoadBalancersQuota.bind(this);
|
||||
this.onToggleStorageQuota = this.onToggleStorageQuota.bind(this);
|
||||
this.onRegistriesChange = this.onRegistriesChange.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
|
@ -101,6 +102,12 @@ class KubernetesResourcePoolController {
|
|||
}
|
||||
/* #endregion */
|
||||
|
||||
onRegistriesChange(registries) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.Registries = registries;
|
||||
});
|
||||
}
|
||||
|
||||
onToggleLoadBalancersQuota(checked) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.formValues.UseLoadBalancersQuota = checked;
|
||||
|
|
|
@ -72,8 +72,8 @@
|
|||
class="form-group mt-4"
|
||||
ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && ($ctrl.isAdmin || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1))"
|
||||
>
|
||||
<div class="col-sm-12 vertical-center">
|
||||
<label for="group-access" class="control-label text-left col-sm-3 col-lg-2 !p-0">
|
||||
<div class="w-full vertical-center">
|
||||
<label for="group-access" class="control-label text-left col-sm-3 col-lg-2 !pt-0">
|
||||
Authorized teams
|
||||
<portainer-tooltip
|
||||
ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length > 0"
|
||||
|
@ -84,54 +84,45 @@
|
|||
message="'As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource.'"
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 20px">
|
||||
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
|
||||
</span>
|
||||
<div
|
||||
isteven-multi-select
|
||||
ng-if="($ctrl.isAdmin && $ctrl.availableTeams.length > 0) || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1)"
|
||||
class="col-sm-9 col-lg-10"
|
||||
input-model="$ctrl.availableTeams"
|
||||
output-model="$ctrl.formData.AuthorizedTeams"
|
||||
button-label="Name"
|
||||
item-label="Name"
|
||||
tick-property="selected"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
|
||||
data-cy="portainer-selectTeamAccess"
|
||||
>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted">
|
||||
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
|
||||
</span>
|
||||
|
||||
<por-access-control-form-team-selector
|
||||
ng-if="($ctrl.isAdmin && $ctrl.availableTeams.length > 0) || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1)"
|
||||
options="$ctrl.availableTeams"
|
||||
value="$ctrl.formData.AuthorizedTeams"
|
||||
input-id="'teams-selector'"
|
||||
on-change="($ctrl.onAuthorizedTeamsChange)"
|
||||
></por-access-control-form-team-selector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- !authorized-teams -->
|
||||
<!-- authorized-users -->
|
||||
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && $ctrl.isAdmin">
|
||||
<div class="col-sm-12 vertical-center">
|
||||
<label for="group-access" class="control-label text-left col-sm-3 col-lg-2 !p-0">
|
||||
<div class="w-full vertical-center">
|
||||
<label for="group-access" class="control-label text-left col-sm-3 col-lg-2 !pt-0">
|
||||
Authorized users
|
||||
<portainer-tooltip
|
||||
ng-if="$ctrl.isAdmin && $ctrl.availableUsers.length > 0"
|
||||
message="'You can select which user(s) will be able to manage this resource.'"
|
||||
></portainer-tooltip>
|
||||
</label>
|
||||
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 20px">
|
||||
You have not yet created any users. Head over to the <a ui-sref="portainer.users">Users view</a> to manage users.
|
||||
</span>
|
||||
<div
|
||||
isteven-multi-select
|
||||
ng-if="$ctrl.availableUsers.length > 0"
|
||||
class="col-sm-9 col-lg-10"
|
||||
input-model="$ctrl.availableUsers"
|
||||
output-model="$ctrl.formData.AuthorizedUsers"
|
||||
button-label="Username"
|
||||
item-label="Username"
|
||||
tick-property="selected"
|
||||
helper-elements="filter"
|
||||
search-property="Username"
|
||||
translation="{nothingSelected: 'Select one or more users', search: 'Search...'}"
|
||||
data-cy="portainer-selectUserAccess"
|
||||
>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted">
|
||||
You have not yet created any users. Head over to the <a ui-sref="portainer.users">Users view</a> to manage users.
|
||||
</span>
|
||||
|
||||
<por-access-control-form-user-selector
|
||||
ng-if="$ctrl.availableUsers.length > 0"
|
||||
options="$ctrl.availableUsers"
|
||||
value="$ctrl.formData.AuthorizedUsers"
|
||||
input-id="'users-selector'"
|
||||
on-change="($ctrl.onAuthorizedUsersChange)"
|
||||
></por-access-control-form-user-selector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,6 +15,9 @@ angular.module('portainer.app').controller('porAccessControlFormController', [
|
|||
|
||||
ctrl.RCO = RCO;
|
||||
|
||||
this.onAuthorizedTeamsChange = onAuthorizedTeamsChange.bind(this);
|
||||
this.onAuthorizedUsersChange = onAuthorizedUsersChange.bind(this);
|
||||
|
||||
ctrl.availableTeams = [];
|
||||
ctrl.availableUsers = [];
|
||||
|
||||
|
@ -31,18 +34,24 @@ angular.module('portainer.app').controller('porAccessControlFormController', [
|
|||
}
|
||||
|
||||
function setAuthorizedUsersAndTeams(authorizedUsers, authorizedTeams) {
|
||||
angular.forEach(ctrl.availableUsers, function (user) {
|
||||
var found = _.find(authorizedUsers, { Id: user.Id });
|
||||
if (found) {
|
||||
user.selected = true;
|
||||
}
|
||||
});
|
||||
ctrl.formData.AuthorizedTeams = authorizedTeams;
|
||||
ctrl.formData.AuthorizedUsers = authorizedUsers;
|
||||
}
|
||||
|
||||
angular.forEach(ctrl.availableTeams, function (team) {
|
||||
var found = _.find(authorizedTeams, { Id: team.Id });
|
||||
if (found) {
|
||||
team.selected = true;
|
||||
}
|
||||
function onAuthorizedTeamsChange(AuthorizedTeams) {
|
||||
onChange({ AuthorizedTeams });
|
||||
}
|
||||
|
||||
function onAuthorizedUsersChange(AuthorizedUsers) {
|
||||
onChange({ AuthorizedUsers });
|
||||
}
|
||||
|
||||
function onChange(formData) {
|
||||
$scope.$evalAsync(() => {
|
||||
ctrl.formData = {
|
||||
...ctrl.formData,
|
||||
...formData,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { porAccessManagement } from './por-access-management';
|
||||
import { porAccessManagementUsersSelector } from './por-access-management-users-selector';
|
||||
|
||||
export default angular
|
||||
.module('portainer.app.component.access-management', [])
|
||||
.component('porAccessManagement', porAccessManagement)
|
||||
.component('porAccessManagementUsersSelector', porAccessManagementUsersSelector).name;
|
||||
export default angular.module('portainer.app.component.access-management', []).component('porAccessManagement', porAccessManagement).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>
|
|
@ -13,7 +13,11 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<por-access-management-users-selector options="ctrl.availableUsersAndTeams" value="ctrl.formValues.multiselectOutput"></por-access-management-users-selector>
|
||||
<por-access-management-users-selector
|
||||
options="ctrl.availableUsersAndTeams"
|
||||
value="ctrl.formValues.multiselectOutput"
|
||||
on-change="(ctrl.onChangeUsersAndTeams)"
|
||||
></por-access-management-users-selector>
|
||||
|
||||
<div class="form-group" ng-if="ctrl.entityType !== 'registry'">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left"> Role </label>
|
||||
|
@ -38,7 +42,7 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm vertical-center"
|
||||
ng-disabled="(ctrl.availableUsersAndTeams | filter:{ticked:true}).length === 0 || ctrl.actionInProgress"
|
||||
ng-disabled="ctrl.formValues.multiselectOutput.length === 0 || ctrl.actionInProgress"
|
||||
ng-click="ctrl.authorizeAccess()"
|
||||
button-spinner="ctrl.actionInProgress"
|
||||
data-cy="access-createAccess"
|
||||
|
|
|
@ -6,13 +6,20 @@ import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
|||
|
||||
class PorAccessManagementController {
|
||||
/* @ngInject */
|
||||
constructor(Notifications, AccessService, RoleService) {
|
||||
Object.assign(this, { Notifications, AccessService, RoleService });
|
||||
constructor($scope, Notifications, AccessService, RoleService) {
|
||||
Object.assign(this, { $scope, Notifications, AccessService, RoleService });
|
||||
|
||||
this.limitedToBE = false;
|
||||
|
||||
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
|
||||
this.updateAction = this.updateAction.bind(this);
|
||||
this.onChangeUsersAndTeams = this.onChangeUsersAndTeams.bind(this);
|
||||
}
|
||||
|
||||
onChangeUsersAndTeams(value) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.formValues.multiselectOutput = value;
|
||||
});
|
||||
}
|
||||
|
||||
updateAction() {
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
|
@ -2,16 +2,19 @@ import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
|
|||
|
||||
angular.module('portainer.app').controller('StackDuplicationFormController', [
|
||||
'Notifications',
|
||||
function StackDuplicationFormController(Notifications) {
|
||||
'$scope',
|
||||
function StackDuplicationFormController(Notifications, $scope) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.environmentSelectorOptions = null;
|
||||
|
||||
ctrl.state = {
|
||||
duplicationInProgress: false,
|
||||
migrationInProgress: false,
|
||||
};
|
||||
|
||||
ctrl.formValues = {
|
||||
endpoint: null,
|
||||
endpointId: null,
|
||||
newName: '',
|
||||
};
|
||||
|
||||
|
@ -23,15 +26,24 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
|||
ctrl.migrateStack = migrateStack;
|
||||
ctrl.isMigrationButtonDisabled = isMigrationButtonDisabled;
|
||||
ctrl.isEndpointSelected = isEndpointSelected;
|
||||
ctrl.onChangeEnvironment = onChangeEnvironment;
|
||||
ctrl.$onChanges = $onChanges;
|
||||
|
||||
function isFormValidForMigration() {
|
||||
return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id;
|
||||
return ctrl.formValues.endpointId;
|
||||
}
|
||||
|
||||
function isFormValidForDuplication() {
|
||||
return isFormValidForMigration() && ctrl.formValues.newName && !ctrl.yamlError;
|
||||
}
|
||||
|
||||
function onChangeEnvironment(endpointId) {
|
||||
console.log({ endpointId });
|
||||
return $scope.$evalAsync(() => {
|
||||
ctrl.formValues.endpointId = endpointId;
|
||||
});
|
||||
}
|
||||
|
||||
function duplicateStack() {
|
||||
if (!ctrl.formValues.newName) {
|
||||
Notifications.error('Failure', null, 'Stack name is required for duplication');
|
||||
|
@ -40,7 +52,7 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
|||
ctrl.state.duplicationInProgress = true;
|
||||
ctrl
|
||||
.onDuplicate({
|
||||
endpointId: ctrl.formValues.endpoint.Id,
|
||||
endpointId: ctrl.formValues.endpointId,
|
||||
name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined,
|
||||
})
|
||||
.finally(function () {
|
||||
|
@ -52,7 +64,7 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
|||
ctrl.state.migrationInProgress = true;
|
||||
ctrl
|
||||
.onMigrate({
|
||||
endpointId: ctrl.formValues.endpoint.Id,
|
||||
endpointId: ctrl.formValues.endpointId,
|
||||
name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined,
|
||||
})
|
||||
.finally(function () {
|
||||
|
@ -65,11 +77,42 @@ angular.module('portainer.app').controller('StackDuplicationFormController', [
|
|||
}
|
||||
|
||||
function isTargetEndpointAndCurrentEquals() {
|
||||
return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id === ctrl.currentEndpointId;
|
||||
return ctrl.formValues.endpointId === ctrl.currentEndpointId;
|
||||
}
|
||||
|
||||
function isEndpointSelected() {
|
||||
return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id;
|
||||
return ctrl.formValues.endpointId;
|
||||
}
|
||||
|
||||
function $onChanges() {
|
||||
ctrl.environmentSelectorOptions = getOptions(ctrl.groups, ctrl.endpoints);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
function getOptions(groups, environments) {
|
||||
if (!groups || !environments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groupSet = environments.reduce((groupSet, environment) => {
|
||||
const groupEnvironments = groupSet[environment.GroupId] || [];
|
||||
|
||||
return {
|
||||
...groupSet,
|
||||
[environment.GroupId]: [...groupEnvironments, { label: environment.Name, value: environment.Id }],
|
||||
};
|
||||
}, {});
|
||||
|
||||
return Object.entries(groupSet).map(([groupId, environments]) => {
|
||||
const group = groups.find((group) => group.Id === parseInt(groupId, 10));
|
||||
if (!group) {
|
||||
throw new Error('missing group');
|
||||
}
|
||||
|
||||
return {
|
||||
label: group.Name,
|
||||
options: environments,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<endpoint-selector ng-if="$ctrl.endpoints && $ctrl.groups" model="$ctrl.formValues.endpoint" endpoints="$ctrl.endpoints" groups="$ctrl.groups"></endpoint-selector>
|
||||
<div class="form-group" ng-if="$ctrl.endpoints && $ctrl.groups">
|
||||
<por-select value="$ctrl.formValues.endpointId" on-change="($ctrl.onChangeEnvironment)" options="$ctrl.environmentSelectorOptions"></por-select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -17,6 +17,7 @@ type TeamAccessPolicies = Record<TeamId, AccessPolicy>;
|
|||
export type RegistryId = number;
|
||||
export interface Registry {
|
||||
Id: RegistryId;
|
||||
Name: string;
|
||||
}
|
||||
|
||||
interface RegistryAccess {
|
||||
|
|
|
@ -6,7 +6,8 @@ import AccessViewerPolicyModel from '../../models/access';
|
|||
|
||||
export default class AccessViewerController {
|
||||
/* @ngInject */
|
||||
constructor(Notifications, RoleService, UserService, GroupService, TeamService, TeamMembershipService, Authentication) {
|
||||
constructor($scope, Notifications, RoleService, UserService, GroupService, TeamService, TeamMembershipService, Authentication) {
|
||||
this.$scope = $scope;
|
||||
this.Notifications = Notifications;
|
||||
this.RoleService = RoleService;
|
||||
this.UserService = UserService;
|
||||
|
@ -17,40 +18,51 @@ export default class AccessViewerController {
|
|||
|
||||
this.limitedFeature = 'rbac-roles';
|
||||
this.users = [];
|
||||
this.selectedUserId = null;
|
||||
|
||||
this.onUserSelect = this.onUserSelect.bind(this);
|
||||
}
|
||||
|
||||
onUserSelect() {
|
||||
this.userRoles = [];
|
||||
const userRoles = {};
|
||||
const user = this.selectedUser;
|
||||
const userMemberships = _.filter(this.teamMemberships, { UserId: user.Id });
|
||||
onUserSelect(selectedUserId) {
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.userRoles = [];
|
||||
this.selectedUserId = selectedUserId;
|
||||
|
||||
for (const [, endpoint] of _.entries(this.endpoints)) {
|
||||
let role = this.getRoleFromUserEndpointPolicy(user, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
continue;
|
||||
const userRoles = {};
|
||||
const user = this.allUsers.find((user) => user.Id === selectedUserId);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
role = this.getRoleFromUserEndpointGroupPolicy(user, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
continue;
|
||||
const userMemberships = _.filter(this.teamMemberships, { UserId: user.value });
|
||||
|
||||
for (const [, endpoint] of _.entries(this.endpoints)) {
|
||||
let role = this.getRoleFromUserEndpointPolicy(user, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
continue;
|
||||
}
|
||||
|
||||
role = this.getRoleFromUserEndpointGroupPolicy(user, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
continue;
|
||||
}
|
||||
|
||||
role = this.getRoleFromTeamEndpointPolicies(userMemberships, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
continue;
|
||||
}
|
||||
|
||||
role = this.getRoleFromTeamEndpointGroupPolicies(userMemberships, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
}
|
||||
}
|
||||
|
||||
role = this.getRoleFromTeamEndpointPolicies(userMemberships, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
continue;
|
||||
}
|
||||
|
||||
role = this.getRoleFromTeamEndpointGroupPolicies(userMemberships, endpoint);
|
||||
if (role) {
|
||||
userRoles[endpoint.Id] = role;
|
||||
}
|
||||
}
|
||||
|
||||
this.userRoles = _.values(userRoles);
|
||||
this.userRoles = _.values(userRoles);
|
||||
});
|
||||
}
|
||||
|
||||
findLowestRole(policies) {
|
||||
|
@ -150,7 +162,8 @@ export default class AccessViewerController {
|
|||
this.roles = _.keyBy(await this.RoleService.roles(), 'Id');
|
||||
this.teams = _.keyBy(await this.TeamService.teams(), 'Id');
|
||||
this.teamMemberships = await this.TeamMembershipService.memberships();
|
||||
this.users = await this.teamMemberUsers(this.allUsers, this.teamMemberships);
|
||||
const teamUsers = await this.teamMemberUsers(this.allUsers, this.teamMemberships);
|
||||
this.users = teamUsers.map((user) => ({ label: user.Username, value: user.Id }));
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve accesses');
|
||||
}
|
||||
|
|
|
@ -12,14 +12,9 @@
|
|||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted" ng-if="$ctrl.users.length === 0"> No user available </span>
|
||||
<ui-select ng-if="$ctrl.users.length > 0" ng-model="$ctrl.selectedUser" ng-change="$ctrl.onUserSelect()">
|
||||
<ui-select-match placeholder="Select a user">
|
||||
<span>{{ $select.selected.Username }}</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="item in ($ctrl.users | filter: $select.search)">
|
||||
<span>{{ item.Username }}</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
|
||||
<por-select ng-if="$ctrl.users.length > 0" value="$ctrl.selectedUserId" options="$ctrl.users" on-change="($ctrl.onUserSelect)" placeholder="'Select a user'">
|
||||
</por-select>
|
||||
</div>
|
||||
</div>
|
||||
<access-viewer-datatable table-key="access_viewer" dataset="$ctrl.userRoles" order-by="EndpointName" is-admin="$ctrl.isAdmin"> </access-viewer-datatable>
|
||||
|
|
|
@ -17,6 +17,9 @@ import { withI18nSuspense } from '@/react-tools/withI18nSuspense';
|
|||
import { SettingsFDO } from '@/react/portainer/settings/EdgeComputeView/SettingsFDO';
|
||||
import { SettingsOpenAMT } from '@/react/portainer/settings/EdgeComputeView/SettingsOpenAMT';
|
||||
import { InternalAuth } from '@/react/portainer/settings/AuthenticationView/InternalAuth';
|
||||
import { PorAccessControlFormTeamSelector } from '@/react/portainer/access-control/PorAccessControlForm/TeamsSelector';
|
||||
import { PorAccessControlFormUserSelector } from '@/react/portainer/access-control/PorAccessControlForm/UsersSelector';
|
||||
import { PorAccessManagementUsersSelector } from '@/react/portainer/access-control/AccessManagement/PorAccessManagementUsersSelector';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { TagSelector } from '@@/TagSelector';
|
||||
|
@ -29,6 +32,8 @@ import { DashboardItem } from '@@/DashboardItem';
|
|||
import { SearchBar } from '@@/datatables/SearchBar';
|
||||
import { FallbackImage } from '@@/FallbackImage';
|
||||
import { BadgeIcon } from '@@/BoxSelector/BadgeIcon';
|
||||
import { TeamsSelector } from '@@/TeamsSelector';
|
||||
import { PortainerSelect } from '@@/form-components/PortainerSelect';
|
||||
|
||||
import { fileUploadField } from './file-upload-field';
|
||||
import { switchField } from './switch-field';
|
||||
|
@ -140,4 +145,54 @@ export const componentsModule = angular
|
|||
.component(
|
||||
'internalAuth',
|
||||
r2a(InternalAuth, ['onSaveSettings', 'isLoading', 'value', 'onChange'])
|
||||
)
|
||||
.component(
|
||||
'teamsSelector',
|
||||
r2a(TeamsSelector, [
|
||||
'onChange',
|
||||
'value',
|
||||
'dataCy',
|
||||
'inputId',
|
||||
'name',
|
||||
'placeholder',
|
||||
'teams',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'porAccessControlFormTeamSelector',
|
||||
r2a(PorAccessControlFormTeamSelector, [
|
||||
'inputId',
|
||||
'onChange',
|
||||
'options',
|
||||
'value',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'porAccessControlFormUserSelector',
|
||||
r2a(PorAccessControlFormUserSelector, [
|
||||
'inputId',
|
||||
'onChange',
|
||||
'options',
|
||||
'value',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'porSelect',
|
||||
r2a(PortainerSelect, [
|
||||
'name',
|
||||
'inputId',
|
||||
'placeholder',
|
||||
'disabled',
|
||||
'data-cy',
|
||||
'bindToBody',
|
||||
'value',
|
||||
'onChange',
|
||||
'options',
|
||||
'isMulti',
|
||||
'isClearable',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'porAccessManagementUsersSelector',
|
||||
r2a(PorAccessManagementUsersSelector, ['onChange', 'options', 'value'])
|
||||
).name;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export default class LdapCustomAdminGroupController {
|
||||
/* @ngInject */
|
||||
constructor($async, Notifications, LDAPService) {
|
||||
Object.assign(this, { $async, Notifications, LDAPService });
|
||||
constructor($async, $scope, Notifications, LDAPService) {
|
||||
Object.assign(this, { $async, $scope, Notifications, LDAPService });
|
||||
|
||||
this.groups = null;
|
||||
this.groupstest = null;
|
||||
|
@ -10,6 +10,7 @@ export default class LdapCustomAdminGroupController {
|
|||
this.onRemoveClick = this.onRemoveClick.bind(this);
|
||||
this.onAddClick = this.onAddClick.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.onAdminGroupChange = this.onAdminGroupChange.bind(this);
|
||||
}
|
||||
|
||||
onAddClick() {
|
||||
|
@ -24,7 +25,8 @@ export default class LdapCustomAdminGroupController {
|
|||
return this.$async(async () => {
|
||||
try {
|
||||
this.groups = null;
|
||||
this.groups = await this.onSearchClick();
|
||||
const groups = await this.onSearchClick();
|
||||
this.groups = groups.map((group) => ({ label: group.name, value: group.name }));
|
||||
this.enableAssignAdminGroup = this.groups && this.groups.length > 0;
|
||||
} catch (error) {
|
||||
this.Notifications.error('Failure', error, 'Failed to search groups');
|
||||
|
@ -32,14 +34,15 @@ export default class LdapCustomAdminGroupController {
|
|||
});
|
||||
}
|
||||
|
||||
onAdminGroupChange(value) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.selectedAdminGroups = value;
|
||||
});
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
if (this.settings.AdminAutoPopulate && this.settings.AdminGroups && this.settings.AdminGroups.length > 0) {
|
||||
const settings = {
|
||||
...this.settings,
|
||||
AdminGroupSearchSettings: this.settings.AdminGroupSearchSettings.map((search) => ({ ...search, GroupFilter: search.GroupFilter || this.defaultAdminGroupSearchFilter })),
|
||||
};
|
||||
|
||||
this.groups = await this.LDAPService.adminGroups(settings);
|
||||
await this.search();
|
||||
}
|
||||
|
||||
if (this.groups && this.groups.length > 0) {
|
||||
|
|
|
@ -114,22 +114,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="$ctrl.settings.AdminAutoPopulate">
|
||||
<div class="col-sm-12">
|
||||
<label for="group-access" class="control-label text-left"> Select Group(s) </label>
|
||||
<span
|
||||
class="ml-7"
|
||||
isteven-multi-select
|
||||
ng-if="$ctrl.enableAssignAdminGroup"
|
||||
input-model="$ctrl.groups"
|
||||
output-model="$ctrl.selectedAdminGroups"
|
||||
button-label="name"
|
||||
item-label="name"
|
||||
tick-property="selected"
|
||||
helper-elements="filter"
|
||||
search-property="name"
|
||||
translation="{nothingSelected: 'Select one or more groups', search: 'Search...'}"
|
||||
<div class="form-group" ng-if="$ctrl.settings.AdminAutoPopulate && $ctrl.groups">
|
||||
<label for="group-access" class="control-label text-left col-sm-2"> Select Group(s) </label>
|
||||
<div class="col-sm-8">
|
||||
<por-select
|
||||
data-cy="'group-access-selector'"
|
||||
input-id="'group-access'"
|
||||
value="$ctrl.selectedAdminGroups"
|
||||
on-change="($ctrl.onAdminGroupChange)"
|
||||
options="$ctrl.groups"
|
||||
placeholder="'Select one or more groups'"
|
||||
is-multi="true"
|
||||
>
|
||||
</span>
|
||||
</por-select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -105,25 +105,20 @@
|
|||
<!-- !admin-checkbox -->
|
||||
<!-- teams -->
|
||||
<div class="form-group" ng-if="!formValues.Administrator">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">Add to team(s)</label>
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left" for="teams-selector">Add to team(s)</label>
|
||||
<div class="col-sm-8">
|
||||
<span class="small text-muted" ng-if="teams.length === 0">
|
||||
You don't seem to have any teams to add users into. Head over to the <a ui-sref="portainer.teams">Teams view</a> to create some.
|
||||
</span>
|
||||
<span
|
||||
isteven-multi-select
|
||||
<teams-selector
|
||||
ng-if="teams.length > 0"
|
||||
input-model="teams"
|
||||
output-model="formValues.Teams"
|
||||
button-label="Name"
|
||||
item-label="Name"
|
||||
tick-property="ticked"
|
||||
helper-elements="filter"
|
||||
search-property="Name"
|
||||
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
|
||||
data-cy="user-teamSelect"
|
||||
>
|
||||
</span>
|
||||
value="formValues.TeamIds"
|
||||
teams="teams"
|
||||
placeholder="'Select one or more teams'"
|
||||
data-cy="'user-teamSelect'"
|
||||
on-change="(onChangeTeamIds)"
|
||||
input-id="'teams-selector'"
|
||||
></teams-selector>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !teams -->
|
||||
|
|
|
@ -23,7 +23,7 @@ angular.module('portainer.app').controller('UsersController', [
|
|||
Password: '',
|
||||
ConfirmPassword: '',
|
||||
Administrator: false,
|
||||
Teams: [],
|
||||
TeamIds: [],
|
||||
};
|
||||
|
||||
$scope.handleAdministratorChange = function (checked) {
|
||||
|
@ -32,6 +32,12 @@ angular.module('portainer.app').controller('UsersController', [
|
|||
});
|
||||
};
|
||||
|
||||
$scope.onChangeTeamIds = function (teamIds) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.TeamIds = teamIds;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.checkUsernameValidity = function () {
|
||||
var valid = true;
|
||||
for (var i = 0; i < $scope.users.length; i++) {
|
||||
|
@ -50,11 +56,7 @@ angular.module('portainer.app').controller('UsersController', [
|
|||
var username = $scope.formValues.Username;
|
||||
var password = $scope.formValues.Password;
|
||||
var role = $scope.formValues.Administrator ? 1 : 2;
|
||||
var teamIds = [];
|
||||
angular.forEach($scope.formValues.Teams, function (team) {
|
||||
teamIds.push(team.Id);
|
||||
});
|
||||
UserService.createUser(username, password, role, teamIds)
|
||||
UserService.createUser(username, password, role, $scope.formValues.TeamIds)
|
||||
.then(function success() {
|
||||
Notifications.success('User successfully created', username);
|
||||
$state.reload();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import angular from 'angular';
|
||||
import 'moment';
|
||||
import 'ui-select';
|
||||
import 'angular-sanitize';
|
||||
import 'ng-file-upload';
|
||||
import 'angular-messages';
|
||||
|
@ -18,7 +17,6 @@ import 'bootstrap/dist/js/bootstrap.js';
|
|||
import 'js-yaml/dist/js-yaml.js';
|
||||
import 'angular-ui-bootstrap';
|
||||
import 'angular-moment-picker';
|
||||
import 'angular-multiselect/isteven-multi-select.js';
|
||||
import 'angulartics/dist/angulartics.min.js';
|
||||
|
||||
window.angular = angular;
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
"angular-messages": "1.8.2",
|
||||
"angular-mocks": "1.8.2",
|
||||
"angular-moment-picker": "^0.10.2",
|
||||
"angular-multiselect": "https://github.com/portainer/angular-multi-select.git#5f6cb6f966b0bd89f211338fc41dcf70ecacbde7",
|
||||
"angular-resource": "1.8.2",
|
||||
"angular-sanitize": "1.8.2",
|
||||
"angular-ui-bootstrap": "~2.5.0",
|
||||
|
@ -139,7 +138,6 @@
|
|||
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"toastr": "^2.1.4",
|
||||
"ui-select": "^0.19.8",
|
||||
"uuid": "^3.3.2",
|
||||
"x256": "^0.0.2",
|
||||
"xterm": "^3.8.0",
|
||||
|
|
|
@ -5658,10 +5658,6 @@ angular-moment-picker@^0.10.2:
|
|||
angular "^1.3"
|
||||
moment "^2.16.0"
|
||||
|
||||
"angular-multiselect@https://github.com/portainer/angular-multi-select.git#5f6cb6f966b0bd89f211338fc41dcf70ecacbde7":
|
||||
version "4.0.0"
|
||||
resolved "https://github.com/portainer/angular-multi-select.git#5f6cb6f966b0bd89f211338fc41dcf70ecacbde7"
|
||||
|
||||
angular-resource@1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/angular-resource/-/angular-resource-1.8.2.tgz#fc1f1adb93dfb83e295d1cfff2a48cec4296e20a"
|
||||
|
@ -17991,11 +17987,6 @@ uglify-js@^3.1.4:
|
|||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.1.tgz#9403dc6fa5695a6172a91bc983ea39f0f7c9086d"
|
||||
integrity sha512-FAGKF12fWdkpvNJZENacOH0e/83eG6JyVQyanIJaBXCN1J11TUQv1T1/z8S+Z0CG0ZPk1nPcreF/c7lrTd0TEQ==
|
||||
|
||||
ui-select@^0.19.8:
|
||||
version "0.19.8"
|
||||
resolved "https://registry.yarnpkg.com/ui-select/-/ui-select-0.19.8.tgz#74860848a7fd8bc494d9856d2f62776ea98637c1"
|
||||
integrity sha1-dIYISKf9i8SU2YVtL2J3bqmGN8E=
|
||||
|
||||
unbox-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
|
||||
|
|
Loading…
Reference in New Issue