mirror of
https://github.com/portainer/portainer.git
synced 2025-07-22 23:09:41 +02:00
refactor(edge/groups): migrate view to react [EE-2219] (#11758)
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:s390x platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled
Some checks failed
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
ci / build_images (map[arch:arm platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Has been cancelled
ci / build_images (map[arch:s390x platform:linux version:]) (push) Has been cancelled
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:linux]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Has been cancelled
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Has been cancelled
Test / test-server (map[arch:arm64 platform:linux]) (push) Has been cancelled
ci / build_manifests (push) Has been cancelled
This commit is contained in:
parent
b7cde35c3d
commit
9c70a43ac3
39 changed files with 579 additions and 386 deletions
|
@ -35,7 +35,7 @@ angular
|
||||||
url: '/new',
|
url: '/new',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'createEdgeGroupView',
|
component: 'edgeGroupsCreateView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -45,7 +45,7 @@ angular
|
||||||
url: '/:groupId',
|
url: '/:groupId',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'editEdgeGroupView',
|
component: 'edgeGroupsItemView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
<form class="form-horizontal" name="EdgeGroupForm" ng-submit="$ctrl.handleSubmit()">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="group_name" class="col-sm-3 col-lg-2 control-label required text-left"> Name </label>
|
|
||||||
<div class="col-sm-9 col-lg-10">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
data-cy="edgeGroupCreate-groupNameInput"
|
|
||||||
class="form-control"
|
|
||||||
id="group_name"
|
|
||||||
name="group_name"
|
|
||||||
ng-model="$ctrl.model.Name"
|
|
||||||
required
|
|
||||||
auto-focus
|
|
||||||
placeholder="e.g. mygroup"
|
|
||||||
/>
|
|
||||||
<div class="help-block" ng-show="EdgeGroupForm.group_name.$invalid">
|
|
||||||
<div class="small text-warning">
|
|
||||||
<div ng-messages="EdgeGroupForm.group_name.$error">
|
|
||||||
<p ng-message="required" class="vertical-center"> <pr-icon icon="'alert-triangle'" class-name="'icon-sm icon-warning'"></pr-icon> This field is required. </p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Group type </div>
|
|
||||||
|
|
||||||
<box-selector slim="true" value="$ctrl.model.Dynamic" on-change="($ctrl.onChangeDynamic)" options="$ctrl.groupTypeOptions" radio-name="'groupTypeDynamic'"></box-selector>
|
|
||||||
|
|
||||||
<!-- StaticGroup -->
|
|
||||||
<div ng-if="!$ctrl.model.Dynamic">
|
|
||||||
<div ng-if="!$ctrl.noEndpoints">
|
|
||||||
<!-- environments -->
|
|
||||||
<div class="col-sm-12 form-section-title"> Associated environments </div>
|
|
||||||
<div class="form-group">
|
|
||||||
<associated-edge-environments-selector value="$ctrl.model.Endpoints" on-change="($ctrl.onChangeEnvironments)"></associated-edge-environments-selector>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" ng-if="$ctrl.noEndpoints">
|
|
||||||
<div class="col-sm-12 small text-muted">
|
|
||||||
No Edge environments are available. Head over to the <a ui-sref="portainer.endpoints">Environments view</a> to add environments.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !StaticGroup -->
|
|
||||||
|
|
||||||
<!-- DynamicGroup -->
|
|
||||||
<div ng-if="$ctrl.model.Dynamic">
|
|
||||||
<div class="col-sm-12 form-section-title"> Tags </div>
|
|
||||||
|
|
||||||
<box-selector slim="true" value="$ctrl.model.PartialMatch" on-change="($ctrl.onChangePartialMatch)" options="$ctrl.tagOptions" radio-name="'partialMatch'"></box-selector>
|
|
||||||
|
|
||||||
<tag-selector ng-if="$ctrl.model.TagIds" value="$ctrl.model.TagIds" on-change="($ctrl.onChangeTags)"> </tag-selector>
|
|
||||||
|
|
||||||
<edge-group-association-table
|
|
||||||
title="'Associated environments by tags'"
|
|
||||||
empty-content-message="'No associated available'"
|
|
||||||
query="$ctrl.dynamicQuery"
|
|
||||||
></edge-group-association-table>
|
|
||||||
</div>
|
|
||||||
<!-- !DynamicGroup -->
|
|
||||||
|
|
||||||
<!-- actions -->
|
|
||||||
<div class="col-sm-12 form-section-title"> Actions </div>
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
ng-disabled="$ctrl.actionInProgress || !EdgeGroupForm.$valid"
|
|
||||||
button-spinner="$ctrl.actionInProgress"
|
|
||||||
data-cy="edgeGroupCreate-addGroupButton"
|
|
||||||
>
|
|
||||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
|
||||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { confirmDestructive } from '@@/modals/confirm';
|
|
||||||
import { EdgeTypes } from '@/react/portainer/environments/types';
|
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
|
||||||
import { tagOptions } from '@/react/edge/edge-groups/CreateView/tag-options';
|
|
||||||
import { groupTypeOptions } from '@/react/edge/edge-groups/CreateView/group-type-options';
|
|
||||||
|
|
||||||
export class EdgeGroupFormController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($async, $scope) {
|
|
||||||
this.$async = $async;
|
|
||||||
this.$scope = $scope;
|
|
||||||
|
|
||||||
this.groupTypeOptions = groupTypeOptions;
|
|
||||||
this.tagOptions = tagOptions;
|
|
||||||
|
|
||||||
this.dynamicQuery = {
|
|
||||||
types: EdgeTypes,
|
|
||||||
tagIds: [],
|
|
||||||
tagsPartialMatch: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onChangeEnvironments = this.onChangeEnvironments.bind(this);
|
|
||||||
this.onChangeTags = this.onChangeTags.bind(this);
|
|
||||||
this.onChangeDynamic = this.onChangeDynamic.bind(this);
|
|
||||||
this.onChangeModel = this.onChangeModel.bind(this);
|
|
||||||
this.onChangePartialMatch = this.onChangePartialMatch.bind(this);
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
|
||||||
|
|
||||||
$scope.$watch(
|
|
||||||
() => this.model,
|
|
||||||
() => {
|
|
||||||
if (this.model.Dynamic) {
|
|
||||||
this.dynamicQuery = {
|
|
||||||
types: EdgeTypes,
|
|
||||||
tagIds: this.model.TagIds,
|
|
||||||
tagsPartialMatch: this.model.PartialMatch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeModel(model) {
|
|
||||||
return this.$scope.$evalAsync(() => {
|
|
||||||
this.model = {
|
|
||||||
...this.model,
|
|
||||||
...model,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangePartialMatch(value) {
|
|
||||||
return this.onChangeModel({ PartialMatch: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeDynamic(value) {
|
|
||||||
this.onChangeModel({ Dynamic: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeTags(value) {
|
|
||||||
this.onChangeModel({ TagIds: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeEnvironments(value, meta) {
|
|
||||||
return this.$async(async () => {
|
|
||||||
if (meta.type === 'remove' && this.pageType === 'edit') {
|
|
||||||
const confirmed = await confirmDestructive({
|
|
||||||
title: 'Confirm action',
|
|
||||||
message: 'Removing the environment from this group will remove its corresponding edge stacks',
|
|
||||||
confirmButton: buildConfirmButton('Confirm'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onChangeModel({ Endpoints: value });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSubmit() {
|
|
||||||
this.formAction(this.model);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import { EdgeGroupFormController } from './groupFormController';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('edgeGroupForm', {
|
|
||||||
templateUrl: './groupForm.html',
|
|
||||||
controller: EdgeGroupFormController,
|
|
||||||
bindings: {
|
|
||||||
model: '<',
|
|
||||||
formActionLabel: '@',
|
|
||||||
formAction: '<',
|
|
||||||
actionInProgress: '<',
|
|
||||||
loaded: '<',
|
|
||||||
pageType: '@',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,14 +1,13 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
|
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
||||||
|
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
||||||
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
|
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
|
||||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||||
import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm';
|
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
|
||||||
import { EdgeGroupAssociationTable } from '@/react/edge/components/EdgeGroupAssociationTable';
|
|
||||||
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
|
||||||
|
|
||||||
import { edgeJobsModule } from './edge-jobs';
|
import { edgeJobsModule } from './edge-jobs';
|
||||||
|
|
||||||
|
@ -57,20 +56,12 @@ const ngModule = angular
|
||||||
'fieldSettings',
|
'fieldSettings',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
|
||||||
'edgeGroupAssociationTable',
|
|
||||||
r2a(withReactQuery(EdgeGroupAssociationTable), [
|
|
||||||
'onClickRow',
|
|
||||||
'query',
|
|
||||||
'title',
|
|
||||||
'data-cy',
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.component(
|
.component(
|
||||||
'associatedEdgeEnvironmentsSelector',
|
'associatedEdgeEnvironmentsSelector',
|
||||||
r2a(withReactQuery(AssociatedEdgeEnvironmentsSelector), [
|
r2a(withReactQuery(AssociatedEdgeEnvironmentsSelector), [
|
||||||
'onChange',
|
'onChange',
|
||||||
'value',
|
'value',
|
||||||
|
'error',
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
20
app/edge/react/views/groups.ts
Normal file
20
app/edge/react/views/groups.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
|
import { ListView } from '@/react/edge/edge-groups/ListView';
|
||||||
|
import { CreateView } from '@/react/edge/edge-groups/CreateView/CreateView';
|
||||||
|
import { ItemView } from '@/react/edge/edge-groups/ItemView/ItemView';
|
||||||
|
|
||||||
|
export const groupsModule = angular
|
||||||
|
.module('portainer.edge.react.views.groups', [])
|
||||||
|
.component('edgeGroupsView', r2a(withUIRouter(withCurrentUser(ListView)), []))
|
||||||
|
.component(
|
||||||
|
'edgeGroupsCreateView',
|
||||||
|
r2a(withUIRouter(withCurrentUser(CreateView)), [])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'edgeGroupsItemView',
|
||||||
|
r2a(withUIRouter(withCurrentUser(ItemView)), [])
|
||||||
|
).name;
|
|
@ -5,23 +5,20 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { WaitingRoomView } from '@/react/edge/edge-devices/WaitingRoomView';
|
import { WaitingRoomView } from '@/react/edge/edge-devices/WaitingRoomView';
|
||||||
import { ListView as EdgeGroupsListView } from '@/react/edge/edge-groups/ListView';
|
|
||||||
|
|
||||||
import { templatesModule } from './templates';
|
import { templatesModule } from './templates';
|
||||||
import { jobsModule } from './jobs';
|
import { jobsModule } from './jobs';
|
||||||
import { stacksModule } from './edge-stacks';
|
import { stacksModule } from './edge-stacks';
|
||||||
|
import { groupsModule } from './groups';
|
||||||
|
|
||||||
export const viewsModule = angular
|
export const viewsModule = angular
|
||||||
.module('portainer.edge.react.views', [
|
.module('portainer.edge.react.views', [
|
||||||
templatesModule,
|
templatesModule,
|
||||||
jobsModule,
|
jobsModule,
|
||||||
stacksModule,
|
stacksModule,
|
||||||
|
groupsModule,
|
||||||
])
|
])
|
||||||
.component(
|
.component(
|
||||||
'waitingRoomView',
|
'waitingRoomView',
|
||||||
r2a(withUIRouter(withReactQuery(withCurrentUser(WaitingRoomView))), [])
|
r2a(withUIRouter(withReactQuery(withCurrentUser(WaitingRoomView))), [])
|
||||||
)
|
|
||||||
.component(
|
|
||||||
'edgeGroupsView',
|
|
||||||
r2a(withUIRouter(withCurrentUser(EdgeGroupsListView)), [])
|
|
||||||
).name;
|
).name;
|
||||||
|
|
|
@ -5,11 +5,7 @@ angular.module('portainer.edge').factory('EdgeGroups', function EdgeGroupsFactor
|
||||||
API_ENDPOINT_EDGE_GROUPS + '/:id/:action',
|
API_ENDPOINT_EDGE_GROUPS + '/:id/:action',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
create: { method: 'POST', ignoreLoadingBar: true },
|
|
||||||
query: { method: 'GET', isArray: true },
|
query: { method: 'GET', isArray: true },
|
||||||
get: { method: 'GET', params: { id: '@id' } },
|
|
||||||
update: { method: 'PUT', params: { id: '@Id' } },
|
|
||||||
remove: { method: 'DELETE', params: { id: '@id' } },
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,25 +3,9 @@ import angular from 'angular';
|
||||||
angular.module('portainer.edge').factory('EdgeGroupService', function EdgeGroupServiceFactory(EdgeGroups) {
|
angular.module('portainer.edge').factory('EdgeGroupService', function EdgeGroupServiceFactory(EdgeGroups) {
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.group = function group(groupId) {
|
|
||||||
return EdgeGroups.get({ id: groupId }).$promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
service.groups = function groups() {
|
service.groups = function groups() {
|
||||||
return EdgeGroups.query({}).$promise;
|
return EdgeGroups.query({}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.remove = function remove(groupId) {
|
|
||||||
return EdgeGroups.remove({ id: groupId }).$promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
service.create = function create(group) {
|
|
||||||
return EdgeGroups.create(group).$promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
service.update = function update(group) {
|
|
||||||
return EdgeGroups.update(group).$promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<page-header title="'Create edge group'" breadcrumbs="[{label:'Edge groups', link:'edge.groups'}, 'Add edge group']"> </page-header>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<edge-group-form
|
|
||||||
loaded="$ctrl.state.loaded"
|
|
||||||
page-type="create"
|
|
||||||
form-action-label="Add edge group"
|
|
||||||
form-action="$ctrl.createGroup"
|
|
||||||
groups="$ctrl.endpointGroups"
|
|
||||||
model="$ctrl.model"
|
|
||||||
></edge-group-form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,47 +0,0 @@
|
||||||
export class CreateEdgeGroupController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(EdgeGroupService, GroupService, Notifications, $state, $async) {
|
|
||||||
this.EdgeGroupService = EdgeGroupService;
|
|
||||||
this.GroupService = GroupService;
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.$state = $state;
|
|
||||||
this.$async = $async;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
actionInProgress: false,
|
|
||||||
loaded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.model = {
|
|
||||||
Name: '',
|
|
||||||
Endpoints: [],
|
|
||||||
Dynamic: false,
|
|
||||||
TagIds: [],
|
|
||||||
PartialMatch: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.createGroup = this.createGroup.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async $onInit() {
|
|
||||||
const endpointGroups = await this.GroupService.groups();
|
|
||||||
|
|
||||||
this.endpointGroups = endpointGroups;
|
|
||||||
this.state.loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createGroup(model) {
|
|
||||||
return this.$async(async () => {
|
|
||||||
this.state.actionInProgress = true;
|
|
||||||
try {
|
|
||||||
await this.EdgeGroupService.create(model);
|
|
||||||
this.Notifications.success('Success', 'Edge group successfully created');
|
|
||||||
this.$state.go('edge.groups');
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to create edge group');
|
|
||||||
} finally {
|
|
||||||
this.state.actionInProgress = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import { CreateEdgeGroupController } from './createEdgeGroupViewController';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('createEdgeGroupView', {
|
|
||||||
templateUrl: './createEdgeGroupView.html',
|
|
||||||
controller: CreateEdgeGroupController,
|
|
||||||
});
|
|
|
@ -1,19 +0,0 @@
|
||||||
<page-header title="'Edit edge group'" breadcrumbs="[{label:'Edge Groups', link:'edge.groups'}, $ctrl.model.Name]"> </page-header>
|
|
||||||
|
|
||||||
<div class="row" ng-if="$ctrl.model">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<edge-group-form
|
|
||||||
loaded="$ctrl.state.loaded"
|
|
||||||
page-type="edit"
|
|
||||||
form-action-label="Save edge group"
|
|
||||||
form-action="$ctrl.updateGroup"
|
|
||||||
endpoints="$ctrl.endpoints"
|
|
||||||
groups="$ctrl.endpointGroups"
|
|
||||||
model="$ctrl.model"
|
|
||||||
></edge-group-form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,44 +0,0 @@
|
||||||
export class EditEdgeGroupController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor(EdgeGroupService, GroupService, Notifications, $state, $async) {
|
|
||||||
this.EdgeGroupService = EdgeGroupService;
|
|
||||||
this.GroupService = GroupService;
|
|
||||||
this.Notifications = Notifications;
|
|
||||||
this.$state = $state;
|
|
||||||
this.$async = $async;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
actionInProgress: false,
|
|
||||||
loaded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateGroup = this.updateGroup.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async $onInit() {
|
|
||||||
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.endpointGroups = endpointGroups;
|
|
||||||
this.model = group;
|
|
||||||
this.state.loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateGroup(group) {
|
|
||||||
return this.$async(async () => {
|
|
||||||
this.state.actionInProgress = true;
|
|
||||||
try {
|
|
||||||
await this.EdgeGroupService.update(group);
|
|
||||||
this.Notifications.success('Success', 'Edge group successfully updated');
|
|
||||||
this.$state.go('edge.groups');
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to update edge group');
|
|
||||||
} finally {
|
|
||||||
this.state.actionInProgress = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import { EditEdgeGroupController } from './editEdgeGroupViewController';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('editEdgeGroupView', {
|
|
||||||
templateUrl: './editEdgeGroupView.html',
|
|
||||||
controller: EditEdgeGroupController,
|
|
||||||
});
|
|
|
@ -70,6 +70,7 @@ export const ngModule = angular
|
||||||
'allowCreate',
|
'allowCreate',
|
||||||
'onChange',
|
'onChange',
|
||||||
'value',
|
'value',
|
||||||
|
'errors',
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
10
app/react/components/Redirect.tsx
Normal file
10
app/react/components/Redirect.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export function Redirect({ to, params = {} }: { to: string; params?: object }) {
|
||||||
|
const router = useRouter();
|
||||||
|
useEffect(() => {
|
||||||
|
router.stateService.go(to, params);
|
||||||
|
}, [params, router.stateService, to]);
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import { useCreateTagMutation, useTags } from '@/portainer/tags/queries';
|
||||||
import { Creatable, Select } from '@@/form-components/ReactSelect';
|
import { Creatable, Select } from '@@/form-components/ReactSelect';
|
||||||
import { FormControl } from '@@/form-components/FormControl';
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
import { ArrayError } from '@@/form-components/InputList/InputList';
|
||||||
|
|
||||||
import { TagButton } from '../TagButton';
|
import { TagButton } from '../TagButton';
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ interface Props {
|
||||||
value: TagId[];
|
value: TagId[];
|
||||||
allowCreate?: boolean;
|
allowCreate?: boolean;
|
||||||
onChange(value: TagId[]): void;
|
onChange(value: TagId[]): void;
|
||||||
|
errors?: ArrayError<TagId[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
|
@ -20,7 +22,12 @@ interface Option {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TagSelector({ value, allowCreate = false, onChange }: Props) {
|
export function TagSelector({
|
||||||
|
value,
|
||||||
|
allowCreate = false,
|
||||||
|
onChange,
|
||||||
|
errors,
|
||||||
|
}: Props) {
|
||||||
// change the struct because react-select has a bug with Creatable (https://github.com/JedWatson/react-select/issues/3417#issuecomment-461868989)
|
// change the struct because react-select has a bug with Creatable (https://github.com/JedWatson/react-select/issues/3417#issuecomment-461868989)
|
||||||
const tagsQuery = useTags({
|
const tagsQuery = useTags({
|
||||||
select: (tags) => tags?.map((opt) => ({ label: opt.Name, value: opt.ID })),
|
select: (tags) => tags?.map((opt) => ({ label: opt.Name, value: opt.ID })),
|
||||||
|
@ -62,6 +69,7 @@ export function TagSelector({ value, allowCreate = false, onChange }: Props) {
|
||||||
<>
|
<>
|
||||||
{value.length > 0 && (
|
{value.length > 0 && (
|
||||||
<FormControl label="Selected tags">
|
<FormControl label="Selected tags">
|
||||||
|
<div data-cy="selected-tags">
|
||||||
{selectedTags.map((tag) => (
|
{selectedTags.map((tag) => (
|
||||||
<TagButton
|
<TagButton
|
||||||
key={tag.value}
|
key={tag.value}
|
||||||
|
@ -71,10 +79,19 @@ export function TagSelector({ value, allowCreate = false, onChange }: Props) {
|
||||||
onRemove={() => handleRemove(tag.value)}
|
onRemove={() => handleRemove(tag.value)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormControl label="Tags" inputId="tags-selector">
|
<FormControl
|
||||||
|
label="Tags"
|
||||||
|
inputId="tags-selector"
|
||||||
|
errors={
|
||||||
|
typeof errors === 'string'
|
||||||
|
? errors
|
||||||
|
: errors?.map((e) => e?.toString())
|
||||||
|
}
|
||||||
|
>
|
||||||
<SelectComponent
|
<SelectComponent
|
||||||
inputId="tags-selector"
|
inputId="tags-selector"
|
||||||
value={[] as { label: string; value: number }[]}
|
value={[] as { label: string; value: number }[]}
|
||||||
|
|
|
@ -11,6 +11,7 @@ interface Props extends AutomationTestingProps {
|
||||||
loadingText: string;
|
loadingText: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
|
errors?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormActions({
|
export function FormActions({
|
||||||
|
@ -19,6 +20,7 @@ export function FormActions({
|
||||||
isLoading,
|
isLoading,
|
||||||
children,
|
children,
|
||||||
isValid,
|
isValid,
|
||||||
|
errors,
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
|
@ -35,6 +37,13 @@ export function FormActions({
|
||||||
>
|
>
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|
||||||
|
{!isValid && (
|
||||||
|
<div className="hidden" data-cy="errors">
|
||||||
|
{JSON.stringify(errors)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types';
|
import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
import { ArrayError } from '@@/form-components/InputList/InputList';
|
||||||
|
|
||||||
import { EdgeGroupAssociationTable } from './EdgeGroupAssociationTable';
|
import { EdgeGroupAssociationTable } from './EdgeGroupAssociationTable';
|
||||||
|
|
||||||
export function AssociatedEdgeEnvironmentsSelector({
|
export function AssociatedEdgeEnvironmentsSelector({
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
|
error,
|
||||||
}: {
|
}: {
|
||||||
onChange: (
|
onChange: (
|
||||||
value: EnvironmentId[],
|
value: EnvironmentId[],
|
||||||
meta: { type: 'add' | 'remove'; value: EnvironmentId }
|
meta: { type: 'add' | 'remove'; value: EnvironmentId }
|
||||||
) => void;
|
) => void;
|
||||||
value: EnvironmentId[];
|
value: EnvironmentId[];
|
||||||
|
error?: ArrayError<Array<EnvironmentId>>;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -20,6 +25,14 @@ export function AssociatedEdgeEnvironmentsSelector({
|
||||||
environment entry to move it from one table to the other.
|
environment entry to move it from one table to the other.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<FormError>
|
||||||
|
{typeof error === 'string' ? error : error.join(', ')}
|
||||||
|
</FormError>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="col-sm-12 mt-4">
|
<div className="col-sm-12 mt-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
|
|
|
@ -2,15 +2,15 @@ import { createColumnHelper } from '@tanstack/react-table';
|
||||||
import { truncate } from 'lodash';
|
import { truncate } from 'lodash';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useTags } from '@/portainer/tags/queries';
|
||||||
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
|
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
||||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
|
||||||
import { useTags } from '@/portainer/tags/queries';
|
|
||||||
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
|
||||||
import { AutomationTestingProps } from '@/types';
|
import { AutomationTestingProps } from '@/types';
|
||||||
|
|
||||||
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
|
|
||||||
import { Datatable, TableRow } from '@@/datatables';
|
import { Datatable, TableRow } from '@@/datatables';
|
||||||
|
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
type DecoratedEnvironment = Environment & {
|
type DecoratedEnvironment = Environment & {
|
||||||
Tags: string[];
|
Tags: string[];
|
||||||
|
@ -41,12 +41,12 @@ const columns = [
|
||||||
export function EdgeGroupAssociationTable({
|
export function EdgeGroupAssociationTable({
|
||||||
title,
|
title,
|
||||||
query,
|
query,
|
||||||
onClickRow,
|
onClickRow = () => {},
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
query: EnvironmentsQueryParams;
|
query: EnvironmentsQueryParams;
|
||||||
onClickRow: (env: Environment) => void;
|
onClickRow?: (env: Environment) => void;
|
||||||
} & AutomationTestingProps) {
|
} & AutomationTestingProps) {
|
||||||
const tableState = useTableStateWithoutStorage('Name');
|
const tableState = useTableStateWithoutStorage('Name');
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { useCreateGroupMutation } from '@/react/edge/edge-groups/queries/useCreateEdgeGroupMutation';
|
import { useCreateEdgeGroupMutation } from '@/react/edge/edge-groups/queries/useCreateEdgeGroupMutation';
|
||||||
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
|
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
|
||||||
|
|
||||||
import { CreatableSelector } from './CreatableSelector';
|
import { CreatableSelector } from './CreatableSelector';
|
||||||
|
|
||||||
export function EdgeGroupsSelector() {
|
export function EdgeGroupsSelector() {
|
||||||
const createMutation = useCreateGroupMutation();
|
const createMutation = useCreateEdgeGroupMutation();
|
||||||
|
|
||||||
const edgeGroupsQuery = useEdgeGroups({
|
const edgeGroupsQuery = useEdgeGroups({
|
||||||
select: (edgeGroups) =>
|
select: (edgeGroups) =>
|
||||||
|
|
55
app/react/edge/edge-groups/CreateView/CreateView.tsx
Normal file
55
app/react/edge/edge-groups/CreateView/CreateView.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { Widget } from '@@/Widget';
|
||||||
|
|
||||||
|
import { useCreateEdgeGroupMutation } from '../queries/useCreateEdgeGroupMutation';
|
||||||
|
import { EdgeGroupForm } from '../components/EdgeGroupForm/EdgeGroupForm';
|
||||||
|
|
||||||
|
export function CreateView() {
|
||||||
|
const mutation = useCreateEdgeGroupMutation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Create edge group"
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: 'Edge groups', link: 'edge.groups' },
|
||||||
|
'Add edge group',
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget>
|
||||||
|
<Widget.Body>
|
||||||
|
<EdgeGroupForm
|
||||||
|
onSubmit={({ environmentIds, ...values }) => {
|
||||||
|
mutation.mutate(
|
||||||
|
{
|
||||||
|
endpoints: environmentIds,
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifySuccess(
|
||||||
|
'Success',
|
||||||
|
'Edge group successfully created'
|
||||||
|
);
|
||||||
|
router.stateService.go('^');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isLoading={mutation.isLoading}
|
||||||
|
/>
|
||||||
|
</Widget.Body>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
72
app/react/edge/edge-groups/ItemView/ItemView.tsx
Normal file
72
app/react/edge/edge-groups/ItemView/ItemView.tsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { useCurrentStateAndParams, useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { Widget } from '@@/Widget';
|
||||||
|
import { Redirect } from '@@/Redirect';
|
||||||
|
|
||||||
|
import { useUpdateEdgeGroupMutation } from '../queries/useUpdateEdgeGroupMutation';
|
||||||
|
import { EdgeGroupForm } from '../components/EdgeGroupForm/EdgeGroupForm';
|
||||||
|
import { useEdgeGroup } from '../queries/useEdgeGroup';
|
||||||
|
|
||||||
|
export function ItemView() {
|
||||||
|
const {
|
||||||
|
params: { groupId: id },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
const groupQuery = useEdgeGroup(id);
|
||||||
|
const mutation = useUpdateEdgeGroupMutation();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (groupQuery.isError) {
|
||||||
|
return <Redirect to="edge.groups" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const group = groupQuery.data;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Edit edge group"
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: 'Edge groups', link: 'edge.groups' },
|
||||||
|
group.Name,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget>
|
||||||
|
<Widget.Body>
|
||||||
|
<EdgeGroupForm
|
||||||
|
group={group}
|
||||||
|
onSubmit={({ environmentIds, ...values }) => {
|
||||||
|
mutation.mutate(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
endpoints: environmentIds,
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifySuccess(
|
||||||
|
'Success',
|
||||||
|
'Edge group successfully updated'
|
||||||
|
);
|
||||||
|
router.stateService.go('^');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isLoading={mutation.isLoading}
|
||||||
|
/>
|
||||||
|
</Widget.Body>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
import { EdgeGroupAssociationTable } from '@/react/edge/components/EdgeGroupAssociationTable';
|
||||||
|
import { EdgeTypes } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { BoxSelector } from '@@/BoxSelector';
|
||||||
|
import { TagSelector } from '@@/TagSelector';
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
|
||||||
|
import { tagOptions } from './tag-options';
|
||||||
|
import { FormValues } from './types';
|
||||||
|
|
||||||
|
export function DynamicGroupFieldset() {
|
||||||
|
const { values, setFieldValue, errors } = useFormikContext<FormValues>();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormSection title="Tags">
|
||||||
|
<BoxSelector
|
||||||
|
slim
|
||||||
|
value={values.partialMatch}
|
||||||
|
onChange={(partialMatch) =>
|
||||||
|
setFieldValue('partialMatch', partialMatch)
|
||||||
|
}
|
||||||
|
options={tagOptions}
|
||||||
|
radioName="partialMatch"
|
||||||
|
/>
|
||||||
|
<TagSelector
|
||||||
|
value={values.tagIds}
|
||||||
|
onChange={(tagIds) => setFieldValue('tagIds', tagIds)}
|
||||||
|
errors={errors.tagIds}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<EdgeGroupAssociationTable
|
||||||
|
data-cy="edgeGroupCreate-associatedEnvironmentsTable"
|
||||||
|
title="Associated environments by tags"
|
||||||
|
query={{
|
||||||
|
types: EdgeTypes,
|
||||||
|
tagIds: values.tagIds,
|
||||||
|
tagsPartialMatch: values.partialMatch,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { Form, Formik, useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { BoxSelector } from '@@/BoxSelector';
|
||||||
|
import { FormActions } from '@@/form-components/FormActions';
|
||||||
|
|
||||||
|
import { EdgeGroup } from '../../types';
|
||||||
|
|
||||||
|
import { groupTypeOptions } from './group-type-options';
|
||||||
|
import { FormValues } from './types';
|
||||||
|
import { useValidation } from './useValidation';
|
||||||
|
import { DynamicGroupFieldset } from './DynamicGroupFieldset';
|
||||||
|
import { StaticGroupFieldset } from './StaticGroupFieldset';
|
||||||
|
import { NameField } from './NameField';
|
||||||
|
|
||||||
|
export function EdgeGroupForm({
|
||||||
|
onSubmit,
|
||||||
|
isLoading,
|
||||||
|
group,
|
||||||
|
}: {
|
||||||
|
onSubmit: (values: FormValues) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
group?: EdgeGroup;
|
||||||
|
}) {
|
||||||
|
const validation = useValidation({ id: group?.Id });
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
validationSchema={validation}
|
||||||
|
validateOnMount
|
||||||
|
initialValues={
|
||||||
|
group
|
||||||
|
? {
|
||||||
|
dynamic: group.Dynamic,
|
||||||
|
environmentIds: group.Endpoints,
|
||||||
|
name: group.Name,
|
||||||
|
partialMatch: group.PartialMatch,
|
||||||
|
tagIds: group.TagIds,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: '',
|
||||||
|
dynamic: false,
|
||||||
|
environmentIds: [],
|
||||||
|
partialMatch: false,
|
||||||
|
tagIds: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<InnerForm isLoading={isLoading} isCreate={!group} />
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InnerForm({
|
||||||
|
isLoading,
|
||||||
|
isCreate,
|
||||||
|
}: {
|
||||||
|
isLoading: boolean;
|
||||||
|
isCreate: boolean;
|
||||||
|
}) {
|
||||||
|
const { values, setFieldValue, isValid, errors } =
|
||||||
|
useFormikContext<FormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form className="form-horizontal">
|
||||||
|
<NameField errors={errors.name} />
|
||||||
|
|
||||||
|
<FormSection title="Group type">
|
||||||
|
<BoxSelector
|
||||||
|
slim
|
||||||
|
value={values.dynamic}
|
||||||
|
onChange={(dynamic) => setFieldValue('dynamic', dynamic)}
|
||||||
|
options={groupTypeOptions}
|
||||||
|
radioName="groupTypeDynamic"
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
{values.dynamic ? <DynamicGroupFieldset /> : <StaticGroupFieldset />}
|
||||||
|
|
||||||
|
<FormActions
|
||||||
|
submitLabel={isCreate ? 'Add edge group' : 'Save edge group'}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isValid={isValid}
|
||||||
|
data-cy="edgeGroupCreate-addGroupButton"
|
||||||
|
loadingText="In progress..."
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Field, FormikErrors } from 'formik';
|
||||||
|
import { string } from 'yup';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
import { Input } from '@@/form-components/Input';
|
||||||
|
|
||||||
|
import { useEdgeGroups } from '../../queries/useEdgeGroups';
|
||||||
|
import { EdgeGroup } from '../../types';
|
||||||
|
|
||||||
|
export function NameField({ errors }: { errors?: FormikErrors<string> }) {
|
||||||
|
return (
|
||||||
|
<FormControl label="Name" required errors={errors} inputId="group_name">
|
||||||
|
<Field
|
||||||
|
as={Input}
|
||||||
|
name="name"
|
||||||
|
placeholder="e.g. mygroup"
|
||||||
|
data-cy="edgeGroupCreate-groupNameInput"
|
||||||
|
id="group_name"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNameValidation(id?: EdgeGroup['Id']) {
|
||||||
|
const edgeGroupsQuery = useEdgeGroups();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
string()
|
||||||
|
.required('Name is required')
|
||||||
|
.test({
|
||||||
|
name: 'is-unique',
|
||||||
|
test: (value) =>
|
||||||
|
!edgeGroupsQuery.data?.find(
|
||||||
|
(group) => group.Name === value && group.Id !== id
|
||||||
|
),
|
||||||
|
message: 'Name must be unique',
|
||||||
|
}),
|
||||||
|
[edgeGroupsQuery.data, id]
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
|
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
||||||
|
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { confirmDestructive } from '@@/modals/confirm';
|
||||||
|
import { buildConfirmButton } from '@@/modals/utils';
|
||||||
|
|
||||||
|
import { FormValues } from './types';
|
||||||
|
|
||||||
|
export function StaticGroupFieldset({ isEdit }: { isEdit?: boolean }) {
|
||||||
|
const { values, setFieldValue, errors } = useFormikContext<FormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSection title="Associated environments">
|
||||||
|
<div className="form-group">
|
||||||
|
<AssociatedEdgeEnvironmentsSelector
|
||||||
|
value={values.environmentIds}
|
||||||
|
error={errors.environmentIds}
|
||||||
|
onChange={async (environmentIds, meta) => {
|
||||||
|
if (meta.type === 'remove' && isEdit) {
|
||||||
|
const confirmed = await confirmDestructive({
|
||||||
|
title: 'Confirm action',
|
||||||
|
message:
|
||||||
|
'Removing the environment from this group will remove its corresponding edge stacks',
|
||||||
|
confirmButton: buildConfirmButton('Confirm'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFieldValue('environmentIds', environmentIds);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { TagId } from '@/portainer/tags/types';
|
||||||
|
|
||||||
|
export interface FormValues {
|
||||||
|
name: string;
|
||||||
|
dynamic: boolean;
|
||||||
|
environmentIds: EnvironmentId[];
|
||||||
|
partialMatch: boolean;
|
||||||
|
tagIds: TagId[];
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { SchemaOf, array, boolean, number, object } from 'yup';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { EdgeGroup } from '../../types';
|
||||||
|
|
||||||
|
import { FormValues } from './types';
|
||||||
|
import { useNameValidation } from './NameField';
|
||||||
|
|
||||||
|
export function useValidation({
|
||||||
|
id,
|
||||||
|
}: { id?: EdgeGroup['Id'] } = {}): SchemaOf<FormValues> {
|
||||||
|
const nameValidation = useNameValidation(id);
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
object({
|
||||||
|
name: nameValidation,
|
||||||
|
dynamic: boolean().default(false),
|
||||||
|
environmentIds: array(number().required()),
|
||||||
|
partialMatch: boolean().default(false),
|
||||||
|
tagIds: array(number().required()).when('dynamic', {
|
||||||
|
is: true,
|
||||||
|
then: (schema) => schema.min(1, 'Tags are required'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
[nameValidation]
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
base: () => ['edge', 'groups'] as const,
|
base: () => ['edge', 'groups'] as const,
|
||||||
|
item: (id: number) => [...queryKeys.base(), id] as const,
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,7 +34,7 @@ export async function createEdgeGroup(requestPayload: CreateGroupPayload) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateGroupMutation() {
|
export function useCreateEdgeGroupMutation() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
|
|
47
app/react/edge/edge-groups/queries/useEdgeGroup.ts
Normal file
47
app/react/edge/edge-groups/queries/useEdgeGroup.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import {
|
||||||
|
EnvironmentId,
|
||||||
|
EnvironmentType,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
import { withError } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeGroup } from '../types';
|
||||||
|
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
|
||||||
|
export interface EdgeGroupListItemResponse extends EdgeGroup {
|
||||||
|
EndpointTypes: Array<EnvironmentType>;
|
||||||
|
HasEdgeStack?: boolean;
|
||||||
|
HasEdgeJob?: boolean;
|
||||||
|
HasEdgeConfig?: boolean;
|
||||||
|
TrustedEndpoints: Array<EnvironmentId>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEdgeGroup(id: EdgeGroup['Id']) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<EdgeGroup>(buildUrl({ id }));
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err as Error, 'Failed fetching edge groups');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEdgeGroup<T = EdgeGroup>(
|
||||||
|
id?: EdgeGroup['Id'],
|
||||||
|
{
|
||||||
|
select,
|
||||||
|
}: {
|
||||||
|
select?: (groups: EdgeGroup) => T;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.item(id!),
|
||||||
|
queryFn: () => getEdgeGroup(id!),
|
||||||
|
select,
|
||||||
|
enabled: !!id,
|
||||||
|
...withError('Failed fetching edge group'),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { TagId } from '@/portainer/tags/types';
|
||||||
|
import {
|
||||||
|
mutationOptions,
|
||||||
|
withError,
|
||||||
|
withInvalidate,
|
||||||
|
} from '@/react-tools/react-query';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { EdgeGroup } from '../types';
|
||||||
|
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
interface UpdateGroupPayload {
|
||||||
|
id: EdgeGroup['Id'];
|
||||||
|
name: string;
|
||||||
|
dynamic: boolean;
|
||||||
|
tagIds?: TagId[];
|
||||||
|
endpoints?: EnvironmentId[];
|
||||||
|
partialMatch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateEdgeGroup({
|
||||||
|
id,
|
||||||
|
...requestPayload
|
||||||
|
}: UpdateGroupPayload) {
|
||||||
|
try {
|
||||||
|
const { data: group } = await axios.put<EdgeGroup>(
|
||||||
|
buildUrl({ id }),
|
||||||
|
requestPayload
|
||||||
|
);
|
||||||
|
return group;
|
||||||
|
} catch (e) {
|
||||||
|
throw parseAxiosError(e as Error, 'Failed to update Edge group');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateEdgeGroupMutation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(
|
||||||
|
updateEdgeGroup,
|
||||||
|
mutationOptions(
|
||||||
|
withError('Failed to update Edge group'),
|
||||||
|
withInvalidate(queryClient, [queryKeys.base()])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue