diff --git a/.storybook/preview.js b/.storybook/preview.js
index 0d13e4731..0e3e673af 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -3,6 +3,7 @@ import '../app/assets/css';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
import { handlers } from '@/setup-tests/server-handlers';
+import { QueryClient, QueryClientProvider } from 'react-query';
// Initialize MSW
initMSW({
@@ -31,11 +32,17 @@ export const parameters = {
},
};
+const testQueryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+});
+
export const decorators = [
(Story) => (
-
-
-
+
+
+
+
+
),
mswDecorator,
];
diff --git a/app/assets/css/app.css b/app/assets/css/app.css
index 7866fac0f..13536a402 100644
--- a/app/assets/css/app.css
+++ b/app/assets/css/app.css
@@ -807,13 +807,6 @@ json-tree .branch-preview {
}
/* !spinkit override */
-/* uib-typeahead override */
-#scrollable-dropdown-menu .dropdown-menu {
- max-height: 300px;
- overflow-y: auto;
-}
-/* !uib-typeahead override */
-
.kubectl-shell {
display: block;
text-align: center;
diff --git a/app/edge/components/group-form/groupForm.html b/app/edge/components/group-form/groupForm.html
index efba72b60..0c4c0cf46 100644
--- a/app/edge/components/group-form/groupForm.html
+++ b/app/edge/components/group-form/groupForm.html
@@ -93,12 +93,9 @@
-
+
+
+
Associated environments by tags
this.model,
@@ -34,6 +36,12 @@ export class EdgeGroupFormController {
);
}
+ onChangeTags(value) {
+ return this.$scope.$evalAsync(() => {
+ this.model.TagIds = value;
+ });
+ }
+
associateEndpoint(endpoint) {
if (!_.includes(this.model.Endpoints, endpoint.Id)) {
this.model.Endpoints = [...this.model.Endpoints, endpoint.Id];
diff --git a/app/edge/components/group-form/index.js b/app/edge/components/group-form/index.js
index 98316f88e..2da9bfd7d 100644
--- a/app/edge/components/group-form/index.js
+++ b/app/edge/components/group-form/index.js
@@ -8,7 +8,6 @@ angular.module('portainer.edge').component('edgeGroupForm', {
bindings: {
model: '<',
groups: '<',
- tags: '<',
formActionLabel: '@',
formAction: '<',
actionInProgress: '<',
diff --git a/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupView.html b/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupView.html
index 2732b58e0..8a3cada8e 100644
--- a/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupView.html
+++ b/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupView.html
@@ -13,9 +13,7 @@
form-action-label="Add edge group"
form-action="$ctrl.createGroup"
groups="$ctrl.endpointGroups"
- tags="$ctrl.tags"
model="$ctrl.model"
- on-change-tags="($ctrl.onChangeTags)"
>
diff --git a/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupViewController.js b/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupViewController.js
index ffa6a8a2c..89d2951e0 100644
--- a/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupViewController.js
+++ b/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupViewController.js
@@ -1,9 +1,8 @@
export class CreateEdgeGroupController {
/* @ngInject */
- constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async) {
+ constructor(EdgeGroupService, GroupService, Notifications, $state, $async) {
this.EdgeGroupService = EdgeGroupService;
this.GroupService = GroupService;
- this.TagService = TagService;
this.Notifications = Notifications;
this.$state = $state;
this.$async = $async;
@@ -26,8 +25,8 @@ export class CreateEdgeGroupController {
}
async $onInit() {
- const [tags, endpointGroups] = await Promise.all([this.TagService.tags(), this.GroupService.groups()]);
- this.tags = tags;
+ const endpointGroups = await this.GroupService.groups();
+
this.endpointGroups = endpointGroups;
this.state.loaded = true;
}
diff --git a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupView.html b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupView.html
index ba26f203f..fe4c0e97a 100644
--- a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupView.html
+++ b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupView.html
@@ -14,7 +14,6 @@
form-action="$ctrl.updateGroup"
endpoints="$ctrl.endpoints"
groups="$ctrl.endpointGroups"
- tags="$ctrl.tags"
model="$ctrl.model"
>
diff --git a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js
index d2c910c31..0a0868a9b 100644
--- a/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js
+++ b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js
@@ -1,9 +1,8 @@
export class EditEdgeGroupController {
/* @ngInject */
- constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async) {
+ constructor(EdgeGroupService, GroupService, Notifications, $state, $async) {
this.EdgeGroupService = EdgeGroupService;
this.GroupService = GroupService;
- this.TagService = TagService;
this.Notifications = Notifications;
this.$state = $state;
this.$async = $async;
@@ -18,13 +17,12 @@ export class EditEdgeGroupController {
}
async $onInit() {
- const [tags, endpointGroups, group] = await Promise.all([this.TagService.tags(), this.GroupService.groups(), this.EdgeGroupService.group(this.$state.params.groupId)]);
+ const [endpointGroups, group] = await Promise.all([this.GroupService.groups(), this.EdgeGroupService.group(this.$state.params.groupId)]);
if (!group) {
this.Notifications.error('Failed to find edge group', {});
this.$state.go('edge.groups');
}
- this.tags = tags;
this.endpointGroups = endpointGroups;
this.model = group;
this.state.loaded = true;
diff --git a/app/portainer/components/forms/group-form/group-form.js b/app/portainer/components/forms/group-form/group-form.js
index 4e07f2b19..e5f815655 100644
--- a/app/portainer/components/forms/group-form/group-form.js
+++ b/app/portainer/components/forms/group-form/group-form.js
@@ -9,13 +9,11 @@ angular.module('portainer.app').component('groupForm', {
pageType: '@',
model: '=',
availableEndpoints: '=',
- availableTags: '<',
associatedEndpoints: '=',
addLabelAction: '<',
removeLabelAction: '<',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
- onCreateTag: '<',
},
});
diff --git a/app/portainer/components/forms/group-form/groupForm.html b/app/portainer/components/forms/group-form/groupForm.html
index 987650c56..72024899a 100644
--- a/app/portainer/components/forms/group-form/groupForm.html
+++ b/app/portainer/components/forms/group-form/groupForm.html
@@ -23,17 +23,9 @@
Metadata
-
-
-
-
-
+
+
+
Associated environments
diff --git a/app/portainer/components/forms/group-form/groupFormController.js b/app/portainer/components/forms/group-form/groupFormController.js
index 9f9eb7acf..70c390301 100644
--- a/app/portainer/components/forms/group-form/groupFormController.js
+++ b/app/portainer/components/forms/group-form/groupFormController.js
@@ -3,8 +3,9 @@ import angular from 'angular';
class GroupFormController {
/* @ngInject */
- constructor($q, EndpointService, GroupService, Notifications, Authentication) {
+ constructor($q, $scope, EndpointService, GroupService, Notifications, Authentication) {
this.$q = $q;
+ this.$scope = $scope;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.Notifications = Notifications;
@@ -13,6 +14,13 @@ class GroupFormController {
this.associateEndpoint = this.associateEndpoint.bind(this);
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
this.getPaginatedEndpointsByGroup = this.getPaginatedEndpointsByGroup.bind(this);
+ this.onChangeTags = this.onChangeTags.bind(this);
+ }
+
+ onChangeTags(value) {
+ return this.$scope.$evalAsync(() => {
+ this.model.TagIds = value;
+ });
}
$onInit() {
diff --git a/app/portainer/components/tag-selector/tag-selector.js b/app/portainer/components/tag-selector/tag-selector.js
deleted file mode 100644
index 8cc489bba..000000000
--- a/app/portainer/components/tag-selector/tag-selector.js
+++ /dev/null
@@ -1,10 +0,0 @@
-angular.module('portainer.app').component('tagSelector', {
- templateUrl: './tagSelector.html',
- controller: 'TagSelectorController',
- bindings: {
- tags: '<',
- model: '=',
- onCreate: '<',
- allowCreate: '<',
- },
-});
diff --git a/app/portainer/components/tag-selector/tagSelector.html b/app/portainer/components/tag-selector/tagSelector.html
deleted file mode 100644
index 03c495c4a..000000000
--- a/app/portainer/components/tag-selector/tagSelector.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
Selected tags
-
-
- {{ tag.Name }}
-
-
-
-
-
-
-
-
Tags
-
-
- No tags available.
-
-
-
- No tags matching your filter.
-
diff --git a/app/portainer/components/tag-selector/tagSelectorController.js b/app/portainer/components/tag-selector/tagSelectorController.js
deleted file mode 100644
index ff681825c..000000000
--- a/app/portainer/components/tag-selector/tagSelectorController.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import angular from 'angular';
-import _ from 'lodash-es';
-
-class TagSelectorController {
- /* @ngInject */
- constructor() {
- this.state = {
- selectedValue: '',
- selectedTags: [],
- noResult: false,
- };
- }
-
- 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;
- }
-
- 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;
- }
-
- return filteredTags.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);
diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts
index 212e4f87e..181912b77 100644
--- a/app/portainer/react/components/index.ts
+++ b/app/portainer/react/components/index.ts
@@ -1,6 +1,11 @@
import angular from 'angular';
-export const componentsModule = angular.module(
- 'portainer.app.react.components',
- []
-).name;
+import { r2a } from '@/react-tools/react2angular';
+import { TagSelector } from '@/react/components/TagSelector';
+
+export const componentsModule = angular
+ .module('portainer.app.react.components', [])
+ .component(
+ 'tagSelector',
+ r2a(TagSelector, ['allowCreate', 'onChange', 'value'])
+ ).name;
diff --git a/app/portainer/tags/tags.service.ts b/app/portainer/tags/tags.service.ts
index 7fe24ca26..74ba6b1ca 100644
--- a/app/portainer/tags/tags.service.ts
+++ b/app/portainer/tags/tags.service.ts
@@ -13,7 +13,7 @@ export async function getTags() {
export async function createTag(name: string) {
try {
- const { data: tag } = await axios.post(buildUrl(), { name });
+ const { data: tag } = await axios.post
(buildUrl(), { name });
return tag;
} catch (err) {
throw parseAxiosError(err as Error, 'Unable to create tag');
diff --git a/app/portainer/views/devices/import/importDevice.html b/app/portainer/views/devices/import/importDevice.html
index 79d01d9b5..3f5ed7a2b 100644
--- a/app/portainer/views/devices/import/importDevice.html
+++ b/app/portainer/views/devices/import/importDevice.html
@@ -188,12 +188,9 @@
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
Actions
-
-
-
-
-
+
+
+
Security
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js
index 328f031a8..39380fd8f 100644
--- a/app/portainer/views/endpoints/edit/endpointController.js
+++ b/app/portainer/views/endpoints/edit/endpointController.js
@@ -20,7 +20,7 @@ function EndpointController(
clipboard,
EndpointService,
GroupService,
- TagService,
+
Notifications,
Authentication,
SettingsService,
@@ -28,6 +28,7 @@ function EndpointController(
) {
$scope.onChangeCheckInInterval = onChangeCheckInInterval;
$scope.setFieldValue = setFieldValue;
+ $scope.onChangeTags = onChangeTags;
$scope.state = {
uploadInProgress: false,
@@ -51,26 +52,12 @@ function EndpointController(
$('#copyNotificationEdgeKey').show().fadeOut(2500);
};
- $scope.onCreateTag = function onCreateTag(tagName) {
- return $async(onCreateTagAsync, tagName);
- };
-
$scope.onToggleAllowSelfSignedCerts = function onToggleAllowSelfSignedCerts(checked) {
return $scope.$evalAsync(() => {
$scope.state.allowSelfSignedCerts = checked;
});
};
- 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.onDisassociateEndpoint = async function () {
ModalService.confirmDisassociate((confirmed) => {
if (confirmed) {
@@ -98,6 +85,10 @@ function EndpointController(
setFieldValue('EdgeCheckinInterval', value);
}
+ function onChangeTags(value) {
+ setFieldValue('TagIds', value);
+ }
+
function setFieldValue(name, value) {
return $scope.$evalAsync(() => {
$scope.endpoint = {
@@ -229,12 +220,7 @@ function EndpointController(
async function initView() {
return $async(async () => {
try {
- const [endpoint, groups, tags, settings] = await Promise.all([
- EndpointService.endpoint($transition$.params().id),
- GroupService.groups(),
- TagService.tags(),
- SettingsService.settings(),
- ]);
+ const [endpoint, groups, settings] = await Promise.all([EndpointService.endpoint($transition$.params().id), GroupService.groups(), SettingsService.settings()]);
if (endpoint.URL.indexOf('unix://') === 0 || endpoint.URL.indexOf('npipe://') === 0) {
$scope.endpointType = 'local';
@@ -254,7 +240,6 @@ function EndpointController(
$scope.endpoint = endpoint;
$scope.initialTagIds = endpoint.TagIds.slice();
$scope.groups = groups;
- $scope.availableTags = tags;
configureState();
diff --git a/app/portainer/views/groups/create/createGroupController.js b/app/portainer/views/groups/create/createGroupController.js
index c09201fa9..881e24a8f 100644
--- a/app/portainer/views/groups/create/createGroupController.js
+++ b/app/portainer/views/groups/create/createGroupController.js
@@ -1,6 +1,6 @@
import { EndpointGroupDefaultModel } from '../../../models/group';
-angular.module('portainer.app').controller('CreateGroupController', function CreateGroupController($async, $scope, $state, GroupService, TagService, Notifications) {
+angular.module('portainer.app').controller('CreateGroupController', function CreateGroupController($async, $scope, $state, GroupService, Notifications) {
$scope.state = {
actionInProgress: false,
};
@@ -28,31 +28,10 @@ angular.module('portainer.app').controller('CreateGroupController', function Cre
});
};
- $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) => {
- $scope.availableTags = tags;
- $scope.associatedEndpoints = [];
- $scope.model = new EndpointGroupDefaultModel();
- $scope.loaded = true;
- })
- .catch((err) => {
- Notifications.error('Failure', err, 'Unable to retrieve tags');
- });
+ $scope.associatedEndpoints = [];
+ $scope.model = new EndpointGroupDefaultModel();
+ $scope.loaded = true;
}
initView();
diff --git a/app/portainer/views/groups/create/creategroup.html b/app/portainer/views/groups/create/creategroup.html
index f19069355..9aa982758 100644
--- a/app/portainer/views/groups/create/creategroup.html
+++ b/app/portainer/views/groups/create/creategroup.html
@@ -12,14 +12,12 @@
page-type="create"
model="model"
available-endpoints="availableEndpoints"
- available-tags="availableTags"
associated-endpoints="associatedEndpoints"
add-label-action="addLabel"
remove-label-action="removeLabel"
form-action="create"
form-action-label="Create the group"
action-in-progress="state.actionInProgress"
- on-create-tag="(onCreateTag)"
>
diff --git a/app/portainer/views/groups/edit/group.html b/app/portainer/views/groups/edit/group.html
index ddbb927d1..40f5c246a 100644
--- a/app/portainer/views/groups/edit/group.html
+++ b/app/portainer/views/groups/edit/group.html
@@ -12,14 +12,12 @@
page-type="edit"
model="group"
available-endpoints="availableEndpoints"
- available-tags="availableTags"
associated-endpoints="associatedEndpoints"
add-label-action="addLabel"
remove-label-action="removeLabel"
form-action="update"
form-action-label="Update the group"
action-in-progress="state.actionInProgress"
- on-create-tag="(onCreateTag)"
>
diff --git a/app/portainer/views/groups/edit/groupController.js b/app/portainer/views/groups/edit/groupController.js
index bfb7ea418..571f78e9f 100644
--- a/app/portainer/views/groups/edit/groupController.js
+++ b/app/portainer/views/groups/edit/groupController.js
@@ -1,4 +1,4 @@
-angular.module('portainer.app').controller('GroupController', function GroupController($q, $async, $scope, $state, $transition$, GroupService, TagService, Notifications) {
+angular.module('portainer.app').controller('GroupController', function GroupController($q, $scope, $state, $transition$, GroupService, Notifications) {
$scope.state = {
actionInProgress: false,
};
@@ -20,30 +20,14 @@ angular.module('portainer.app').controller('GroupController', function GroupCont
});
};
- $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;
$q.all({
group: GroupService.group(groupId),
- tags: TagService.tags(),
})
.then(function success(data) {
$scope.group = data.group;
- $scope.availableTags = data.tags;
$scope.loaded = true;
})
.catch(function error(err) {
diff --git a/app/react/components/TagSelector/TagSelector.tsx b/app/react/components/TagSelector/TagSelector.tsx
index 559bfeff6..d268f2a0c 100644
--- a/app/react/components/TagSelector/TagSelector.tsx
+++ b/app/react/components/TagSelector/TagSelector.tsx
@@ -62,18 +62,19 @@ export function TagSelector({ value, allowCreate = false, onChange }: Props) {
{value.length > 0 && (
{selectedTags.map((tag) => (
-
+ handleRemove(tag.value)}
+ key={tag.value}
+ >
{tag.label}
-
- handleRemove(tag.value)}
- >
-
-
-
+
+
))}
)}
diff --git a/app/setup-tests/server-handlers.ts b/app/setup-tests/server-handlers.ts
index 267b20da1..d84bb9c5a 100644
--- a/app/setup-tests/server-handlers.ts
+++ b/app/setup-tests/server-handlers.ts
@@ -12,6 +12,11 @@ import { createMockTeams, createMockUsers } from '../react-tools/test-mocks';
import { azureHandlers } from './setup-handlers/azure';
+const tags: Tag[] = [
+ { ID: 1, Name: 'tag1' },
+ { ID: 2, Name: 'tag2' },
+];
+
const licenseInfo: LicenseInfo = {
nodes: 1000,
type: LicenseType.Subscription,
@@ -48,11 +53,11 @@ export const handlers = [
};
return res(ctx.json(group));
}),
- rest.get('/api/tags', (req, res, ctx) => {
- const tags: Tag[] = [
- { ID: 1, Name: 'tag1' },
- { ID: 2, Name: 'tag2' },
- ];
- return res(ctx.json(tags));
+ rest.get('/api/tags', (req, res, ctx) => res(ctx.json(tags))),
+ rest.post<{ name: string }>('/api/tags', (req, res, ctx) => {
+ const tagName = req.body.name;
+ const tag = { ID: tags.length + 1, Name: tagName };
+ tags.push(tag);
+ return res(ctx.json(tag));
}),
];