create tag from tag selector (#3640)

* feat(tags): add button to save tag when doesn't exist

* feat(endpoints): allow the creating of tags in endpoint edit

* feat(groups): allow user to create tags in create group

* feat(groups): allow user to create tags in edit group

* feat(endpoint): allow user to create tags from endpoint create

* feat(tags): allow the creation of a new tag from dropdown

* feat(tag): replace "add" with "create"

* feat(tags): show tags input when not tags

* feat(tags): hide create message when not allowed

* refactor(tags): replace component controller with class

* refactor(tags): replace native methods with lodash

* refactor(tags): remove unused onChangeTags function

* refactor(tags): remove on-change binding

* style(tags): remove white space

* refactor(endpoint-groups): move controller to separate file

* fix(groups): allow admin to create tag in group form

* refactor(endpoints): wrap async function with try catch and $async

* style(tags): wrap arrow function args with parenthesis

* refactor(endpoints): return $async functions

* refactor(tags): throw error in the format Notification expects
pull/3691/head
Chaim Lev-Ari 2020-04-08 10:56:24 +03:00 committed by GitHub
parent dd6262cf69
commit db8b3d6e5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 253 additions and 147 deletions

View File

@ -1,92 +1,5 @@
import _ from 'lodash-es';
import angular from 'angular';
class GroupFormController {
/* @ngInject */
constructor($q, EndpointService, GroupService, Notifications) {
this.$q = $q;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.Notifications = Notifications;
this.associateEndpoint = this.associateEndpoint.bind(this);
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
this.getPaginatedEndpointsByGroup = this.getPaginatedEndpointsByGroup.bind(this);
}
$onInit() {
this.state = {
available: {
limit: '10',
filter: '',
pageNumber: 1,
totalCount: 0
},
associated: {
limit: '10',
filter: '',
pageNumber: 1,
totalCount: 0
}
};
}
associateEndpoint(endpoint) {
if (this.pageType === 'create' && !_.includes(this.associatedEndpoints, endpoint)) {
this.associatedEndpoints.push(endpoint);
} else if (this.pageType === 'edit') {
this.GroupService.addEndpoint(this.model.Id, endpoint)
.then(() => {
this.Notifications.success('Success', 'Endpoint successfully added to group');
this.reloadTablesContent();
})
.catch((err) => this.Notifications.error('Error', err, 'Unable to add endpoint to group'));
}
}
dissociateEndpoint(endpoint) {
if (this.pageType === 'create') {
_.remove(this.associatedEndpoints, (item) => item.Id === endpoint.Id);
} else if (this.pageType === 'edit') {
this.GroupService.removeEndpoint(this.model.Id, endpoint.Id)
.then(() => {
this.Notifications.success('Success', 'Endpoint successfully removed from group');
this.reloadTablesContent();
})
.catch((err) => this.Notifications.error('Error', err, 'Unable to remove endpoint from group'));
}
}
reloadTablesContent() {
this.getPaginatedEndpointsByGroup(this.pageType, 'available');
this.getPaginatedEndpointsByGroup(this.pageType, 'associated');
this.GroupService.group(this.model.Id)
.then((data) => {
this.model = data;
})
}
getPaginatedEndpointsByGroup(pageType, tableType) {
if (tableType === 'available') {
const context = this.state.available;
const start = (context.pageNumber - 1) * context.limit + 1;
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, 1)
.then((data) => {
this.availableEndpoints = data.value;
this.state.available.totalCount = data.totalCount;
});
} else if (tableType === 'associated' && pageType === 'edit') {
const groupId = this.model.Id ? this.model.Id : 1;
const context = this.state.associated;
const start = (context.pageNumber - 1) * context.limit + 1;
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, groupId)
.then((data) => {
this.associatedEndpoints = data.value;
this.state.associated.totalCount = data.totalCount;
});
}
// ignore (associated + create) group as there is no backend pagination for this table
}
}
import GroupFormController from './groupFormController';
angular.module('portainer.app').component('groupForm', {
templateUrl: './groupForm.html',
@ -102,6 +15,7 @@ angular.module('portainer.app').component('groupForm', {
removeLabelAction: '<',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<'
actionInProgress: '<',
onCreateTag: '<'
}
});

View File

@ -28,9 +28,11 @@
<!-- tags -->
<div class="form-group">
<tag-selector
ng-if="$ctrl.availableTags && $ctrl.model.TagIds"
ng-if="$ctrl.model && $ctrl.availableTags"
tags="$ctrl.availableTags"
model="$ctrl.model.TagIds"
allow-create="$ctrl.state.allowCreateTag"
on-create="$ctrl.onCreateTag"
></tag-selector>
</div>
<!-- !tags -->

View File

@ -0,0 +1,94 @@
import _ from 'lodash-es';
import angular from 'angular';
class GroupFormController {
/* @ngInject */
constructor($q, EndpointService, GroupService, Notifications, Authentication) {
this.$q = $q;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.Notifications = Notifications;
this.Authentication = Authentication;
this.associateEndpoint = this.associateEndpoint.bind(this);
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
this.getPaginatedEndpointsByGroup = this.getPaginatedEndpointsByGroup.bind(this);
}
$onInit() {
this.state = {
available: {
limit: '10',
filter: '',
pageNumber: 1,
totalCount: 0
},
associated: {
limit: '10',
filter: '',
pageNumber: 1,
totalCount: 0
},
allowCreateTag: this.Authentication.isAdmin()
};
}
associateEndpoint(endpoint) {
if (this.pageType === 'create' && !_.includes(this.associatedEndpoints, endpoint)) {
this.associatedEndpoints.push(endpoint);
} else if (this.pageType === 'edit') {
this.GroupService.addEndpoint(this.model.Id, endpoint)
.then(() => {
this.Notifications.success('Success', 'Endpoint successfully added to group');
this.reloadTablesContent();
})
.catch((err) => this.Notifications.error('Error', err, 'Unable to add endpoint to group'));
}
}
dissociateEndpoint(endpoint) {
if (this.pageType === 'create') {
_.remove(this.associatedEndpoints, (item) => item.Id === endpoint.Id);
} else if (this.pageType === 'edit') {
this.GroupService.removeEndpoint(this.model.Id, endpoint.Id)
.then(() => {
this.Notifications.success('Success', 'Endpoint successfully removed from group');
this.reloadTablesContent();
})
.catch((err) => this.Notifications.error('Error', err, 'Unable to remove endpoint from group'));
}
}
reloadTablesContent() {
this.getPaginatedEndpointsByGroup(this.pageType, 'available');
this.getPaginatedEndpointsByGroup(this.pageType, 'associated');
this.GroupService.group(this.model.Id)
.then((data) => {
this.model = data;
})
}
getPaginatedEndpointsByGroup(pageType, tableType) {
if (tableType === 'available') {
const context = this.state.available;
const start = (context.pageNumber - 1) * context.limit + 1;
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, 1)
.then((data) => {
this.availableEndpoints = data.value;
this.state.available.totalCount = data.totalCount;
});
} else if (tableType === 'associated' && pageType === 'edit') {
const groupId = this.model.Id ? this.model.Id : 1;
const context = this.state.associated;
const start = (context.pageNumber - 1) * context.limit + 1;
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, groupId)
.then((data) => {
this.associatedEndpoints = data.value;
this.state.associated.totalCount = data.totalCount;
});
}
// ignore (associated + create) group as there is no backend pagination for this table
}
}
angular.module('portainer.app').controller('GroupFormController', GroupFormController);
export default GroupFormController;

View File

@ -4,5 +4,7 @@ angular.module('portainer.app').component('tagSelector', {
bindings: {
tags: '<',
model: '=',
onCreate: '<',
allowCreate: '<'
},
});

View File

@ -15,24 +15,24 @@
<label for="tags" class="col-sm-3 col-lg-2 control-label text-left">
Tags
</label>
<div class="col-sm-9 col-lg-10" ng-if="$ctrl.tags.length > 0">
<div class="col-sm-9 col-lg-10" ng-if="$ctrl.allowCreate || $ctrl.tags.length > 0">
<input
type="text" ng-model="$ctrl.state.selectedValue"
id="tags" class="form-control"
placeholder="Select tags..."
uib-typeahead="tag.Id as tag.Name for tag in $ctrl.tags | filter: $ctrl.filterSelected | filter:$viewValue | limitTo:7"
uib-typeahead="tag.Id as tag.Name for tag in $ctrl.filterTags($viewValue)"
typeahead-on-select="$ctrl.selectTag($item, $model, $label)"
typeahead-no-results="$ctrl.state.noResult"
typeahead-show-hint="true" typeahead-min-length="0"
/>
</div>
<div class="col-sm-9 col-lg-10" ng-if="$ctrl.tags.length === 0">
<div class="col-sm-9 col-lg-10" ng-if="!$ctrl.allowCreate && $ctrl.tags.length === 0">
<span class="small text-muted">
No tags available.
</span>
</div>
</div>
<div class="col-sm-offset-3 col-lg-offset-2 col-sm-12" ng-if="$ctrl.state.noResult" style="margin-top: 2px;">
<div class="col-sm-offset-3 col-lg-offset-2 col-sm-12" ng-if="!$ctrl.allowCreate && $ctrl.state.noResult" style="margin-top: 2px;">
<span class="small text-muted">
No tags matching your filter.
</span>

View File

@ -1,35 +1,62 @@
import angular from 'angular';
import _ from 'lodash-es';
angular.module('portainer.app').controller('TagSelectorController', function() {
this.$onInit = function() {
this.state.selectedTags = _.map(this.model, (id) => _.find(this.tags, (t) => t.Id === id));
};
this.state = {
selectedValue: '',
selectedTags: [],
noResult: false,
};
this.selectTag = function($item) {
this.state.selectedValue = '';
this.model.push($item.Id);
this.state.selectedTags.push($item);
};
this.removeTag = function removeTag(tag) {
_.remove(this.state.selectedTags, { Id: tag.Id });
_.remove(this.model, (id) => id === tag.Id);
};
this.filterSelected = filterSelected.bind(this);
function filterSelected($item) {
if (!this.model) {
return true;
}
return !_.includes(this.model, $item.Id);
class TagSelectorController {
/* @ngInject */
constructor() {
this.state = {
selectedValue: '',
selectedTags: [],
noResult: false,
};
}
window._remove = _.remove;
});
removeTag(tag) {
_.remove(this.model, (id) => tag.Id === id);
_.remove(this.state.selectedTags, { Id: tag.Id });
}
selectTag($item) {
this.state.selectedValue = '';
if ($item.create && this.allowCreate) {
this.onCreate($item.value);
return;
}
this.state.selectedTags.push($item);
this.model.push($item.Id);
}
filterTags(searchValue) {
let filteredTags = _.filter(this.tags, (tag) => !_.includes(this.model, tag.Id));
if (!searchValue) {
return filteredTags.slice(0, 7);
}
const exactTag = _.find(this.tags, (tag) => tag.Name === searchValue);
filteredTags = _.filter(filteredTags, (tag) => _.includes(tag.Name.toLowerCase(), searchValue.toLowerCase()));
if (exactTag || !this.allowCreate) {
return filteredTags.slice(0, 7);
}
return filteredTags.slice(0, 6).concat({ Name: `Create "${searchValue}"`, create: true, value: searchValue });
}
generateSelectedTags(model, tags) {
this.state.selectedTags = _.map(model, (id) => _.find(tags, (t) => t.Id === id));
}
$onInit() {
this.generateSelectedTags(this.model, this.tags);
}
$onChanges({ tags, model }) {
const tagsValue = tags && tags.currentValue ? tags.currentValue : this.tags;
const modelValue = model && model.currentValue ? model.currentValue : this.model;
if (modelValue && tagsValue) {
this.generateSelectedTags(modelValue, tagsValue);
}
}
}
export default TagSelectorController;
angular.module('portainer.app').controller('TagSelectorController', TagSelectorController);

View File

@ -35,12 +35,16 @@ angular.module('portainer.app')
return deferred.promise;
};
service.createTag = function(name) {
service.createTag = async function(name) {
var payload = {
Name: name
};
return Tags.create({}, payload).$promise;
try {
const tag = await Tags.create({}, payload).$promise;
return new TagViewModel(tag);
} catch(err) {
throw { msg: 'Unable to create tag', err };
}
};
service.deleteTag = function(id) {

View File

@ -1,12 +1,12 @@
import {EndpointSecurityFormData} from '../../../components/endpointSecurity/porEndpointSecurityModel';
angular.module('portainer.app')
.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'Notifications',
function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService, TagService, Notifications) {
angular.module('portainer.app').controller('CreateEndpointController',
function CreateEndpointController($async, $q, $scope, $state, $filter, clipboard, EndpointService, GroupService, TagService, Notifications, Authentication) {
$scope.state = {
EnvironmentType: 'agent',
actionInProgress: false
actionInProgress: false,
allowCreateTag: Authentication.isAdmin()
};
$scope.formValues = {
@ -84,6 +84,20 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService,
createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds);
};
$scope.onCreateTag = function onCreateTag(tagName) {
return $async(onCreateTagAsync, tagName);
}
async function onCreateTagAsync(tagName) {
try {
const tag = await TagService.createTag(tagName);
$scope.availableTags = $scope.availableTags.concat(tag);
$scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id);
} catch(err) {
Notifications.error('Failue', err, 'Unable to create tag');
}
}
function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) {
$scope.state.actionInProgress = true;
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds)
@ -133,4 +147,4 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService,
}
initView();
}]);
});

View File

@ -259,10 +259,12 @@
<!-- tags -->
<div class="form-group">
<tag-selector
ng-if="availableTags"
ng-if="formValues && availableTags"
tags="availableTags"
model="formValues.TagIds"
></tag-selector>
allow-create="state.allowCreateTag"
on-create="onCreateTag"
></tag-selector>
</div>
<!-- !tags -->
<div class="col-sm-12 form-section-title">

View File

@ -165,9 +165,11 @@
<!-- tags -->
<div class="form-group">
<tag-selector
ng-if="availableTags && endpoint.TagIds"
ng-if="endpoint && availableTags"
tags="availableTags"
model="endpoint.TagIds"
on-create="onCreateTag"
allow-create="state.allowCreate"
></tag-selector>
</div>
<!-- !tags -->

View File

@ -3,8 +3,8 @@ import uuidv4 from 'uuid/v4';
import {EndpointSecurityFormData} from '../../../components/endpointSecurity/porEndpointSecurityModel';
angular.module('portainer.app')
.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'EndpointProvider', 'Notifications',
function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, GroupService, TagService, EndpointProvider, Notifications) {
.controller('EndpointController',
function EndpointController($async, $q, $scope, $state, $transition$, $filter, clipboard, EndpointService, GroupService, TagService, EndpointProvider, Notifications, Authentication) {
if (!$scope.applicationState.application.endpointManagement) {
$state.go('portainer.endpoints');
@ -13,13 +13,16 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService,
$scope.state = {
uploadInProgress: false,
actionInProgress: false,
deploymentTab: 0
deploymentTab: 0,
allowCreate: Authentication.isAdmin()
};
$scope.formValues = {
SecurityFormData: new EndpointSecurityFormData()
};
$scope.copyEdgeAgentDeploymentCommand = function() {
if ($scope.state.deploymentTab === 0) {
clipboard.copyText('docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host --restart always -e EDGE=1 -e EDGE_ID=' + $scope.randomEdgeID + ' -e EDGE_KEY=' + $scope.endpoint.EdgeKey +' -e CAP_HOST_MANAGEMENT=1 -v portainer_agent_data:/data --name portainer_edge_agent portainer/agent');
@ -34,6 +37,20 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService,
$('#copyNotificationEdgeKey').show().fadeOut(2500);
};
$scope.onCreateTag = function onCreateTag(tagName) {
return $async(onCreateTagAsync, tagName);
}
async function onCreateTagAsync(tagName) {
try {
const tag = await TagService.createTag(tagName);
$scope.availableTags = $scope.availableTags.concat(tag);
$scope.endpoint.TagIds = $scope.endpoint.TagIds.concat(tag.Id);
} catch(err) {
Notifications.error('Failue', err, 'Unable to create tag');
}
}
$scope.updateEndpoint = function() {
var endpoint = $scope.endpoint;
var securityData = $scope.formValues.SecurityFormData;
@ -120,4 +137,4 @@ function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService,
}
initView();
}]);
});

View File

@ -1,8 +1,7 @@
import {EndpointGroupDefaultModel} from '../../../models/group';
angular.module('portainer.app')
.controller('CreateGroupController', ['$q', '$scope', '$state', 'GroupService', 'EndpointService', 'TagService', 'Notifications',
function ($q, $scope, $state, GroupService, EndpointService, TagService, Notifications) {
.controller('CreateGroupController', function CreateGroupController($async, $scope, $state, GroupService, TagService, Notifications) {
$scope.state = {
actionInProgress: false
@ -31,6 +30,20 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific
});
};
$scope.onCreateTag = function onCreateTag(tagName) {
return $async(onCreateTagAsync, tagName);
}
async function onCreateTagAsync(tagName) {
try {
const tag = await TagService.createTag(tagName);
$scope.availableTags = $scope.availableTags.concat(tag);
$scope.model.TagIds = $scope.model.TagIds.concat(tag.Id);
} catch(err) {
Notifications.error('Failue', err, 'Unable to create tag');
}
}
function initView() {
TagService.tags()
.then((tags) => {
@ -45,4 +58,4 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific
}
initView();
}]);
});

View File

@ -21,6 +21,7 @@
form-action="create"
form-action-label="Create the group"
action-in-progress="state.actionInProgress"
on-create-tag="onCreateTag"
></group-form>
</rd-widget-body>
</rd-widget>

View File

@ -21,6 +21,7 @@
form-action="update"
form-action-label="Update the group"
action-in-progress="state.actionInProgress"
on-create-tag="onCreateTag"
></group-form>
</rd-widget-body>
</rd-widget>

View File

@ -1,6 +1,5 @@
angular.module('portainer.app')
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'TagService', 'Notifications',
function ($q, $scope, $state, $transition$, GroupService, TagService, Notifications) {
angular.module('portainer.app').controller('GroupController',
function GroupController($q, $async, $scope, $state, $transition$, GroupService, TagService, Notifications) {
$scope.state = {
actionInProgress: false
@ -23,6 +22,20 @@ function ($q, $scope, $state, $transition$, GroupService, TagService, Notificati
});
};
$scope.onCreateTag = function onCreateTag(tagName) {
return $async(onCreateTagAsync, tagName);
}
async function onCreateTagAsync(tagName) {
try {
const tag = await TagService.createTag(tagName);
$scope.availableTags = $scope.availableTags.concat(tag);
$scope.group.TagIds = $scope.group.TagIds.concat(tag.Id);
} catch(err) {
Notifications.error('Failue', err, 'Unable to create tag');
}
}
function initView() {
var groupId = $transition$.params().id;
@ -41,4 +54,4 @@ function ($q, $scope, $state, $transition$, GroupService, TagService, Notificati
}
initView();
}]);
});