+
{{ task.ServiceName }}
Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}
Status: {{ task.Status.State }}
diff --git a/app/portainer/__module.js b/app/portainer/__module.js
index 828c7ef41..d04e12b6e 100644
--- a/app/portainer/__module.js
+++ b/app/portainer/__module.js
@@ -287,8 +287,8 @@ angular.module('portainer.app', [])
};
var stackCreation = {
- name: 'portainer.stacks.new',
- url: '/new',
+ name: 'portainer.newstack',
+ url: '/newstack',
views: {
'content@': {
templateUrl: 'app/portainer/views/stacks/create/createstack.html',
diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html
index 6c8dbbb79..5b640ea17 100644
--- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html
+++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html
@@ -11,7 +11,7 @@
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
Remove
-
diff --git a/app/portainer/components/endpoint-list/endpoint-list-controller.js b/app/portainer/components/endpoint-list/endpoint-list-controller.js
new file mode 100644
index 000000000..aab801410
--- /dev/null
+++ b/app/portainer/components/endpoint-list/endpoint-list-controller.js
@@ -0,0 +1,60 @@
+angular.module('portainer.app').controller('EndpointListController', [
+ function EndpointListController() {
+ var ctrl = this;
+ ctrl.state = {
+ textFilter: '',
+ filteredEndpoints: []
+ };
+
+ ctrl.$onChanges = $onChanges;
+ ctrl.onFilterChanged = onFilterChanged;
+
+ function $onChanges(changesObj) {
+ handleEndpointsChange(changesObj.endpoints);
+ }
+
+ function handleEndpointsChange(endpoints) {
+ if (!endpoints) {
+ return;
+ }
+ if (!endpoints.currentValue) {
+ return;
+ }
+
+ onFilterChanged();
+ }
+
+ function onFilterChanged() {
+ var filterValue = ctrl.state.textFilter;
+ ctrl.state.filteredEndpoints = filterEndpoints(
+ ctrl.endpoints,
+ filterValue
+ );
+ }
+
+ function filterEndpoints(endpoints, filterValue) {
+ if (!endpoints || !endpoints.length || !filterValue) {
+ return endpoints;
+ }
+ var keywords = filterValue.split(' ');
+ return _.filter(endpoints, function(endpoint) {
+ var statusString = convertStatusToString(endpoint.Status);
+ return _.every(keywords, function(keyword) {
+ var lowerCaseKeyword = keyword.toLowerCase();
+ return (
+ _.includes(endpoint.Name.toLowerCase(), lowerCaseKeyword) ||
+ _.includes(endpoint.GroupName.toLowerCase(), lowerCaseKeyword) ||
+ _.some(endpoint.Tags, function(tag) {
+ return _.includes(tag.toLowerCase(), lowerCaseKeyword);
+ }) ||
+ _.includes(statusString, keyword)
+ );
+ });
+ });
+ }
+
+ function convertStatusToString(status) {
+ return status === 1 ? 'up' : 'down';
+ }
+ }
+]);
diff --git a/app/portainer/components/endpoint-list/endpoint-list.js b/app/portainer/components/endpoint-list/endpoint-list.js
index d6a4bd33e..a622c3db4 100644
--- a/app/portainer/components/endpoint-list/endpoint-list.js
+++ b/app/portainer/components/endpoint-list/endpoint-list.js
@@ -1,10 +1,6 @@
angular.module('portainer.app').component('endpointList', {
templateUrl: 'app/portainer/components/endpoint-list/endpointList.html',
- controller: function() {
- this.state = {
- textFilter: ''
- };
- },
+ controller: 'EndpointListController',
bindings: {
titleText: '@',
titleIcon: '@',
diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html
index 9b4850dd0..2886916f1 100644
--- a/app/portainer/components/endpoint-list/endpointList.html
+++ b/app/portainer/components/endpoint-list/endpointList.html
@@ -16,12 +16,17 @@
-
+
Loading...
-
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js b/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js
new file mode 100644
index 000000000..4e96696e2
--- /dev/null
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form-controller.js
@@ -0,0 +1,76 @@
+angular.module('portainer.app').controller('StackDuplicationFormController', [
+ 'Notifications',
+ function StackDuplicationFormController(Notifications) {
+ var ctrl = this;
+
+ ctrl.state = {
+ duplicationInProgress: false,
+ migrationInProgress: false
+ };
+
+ ctrl.formValues = {
+ endpoint: null,
+ newName: ''
+ };
+
+ ctrl.isFormValidForDuplication = isFormValidForDuplication;
+ ctrl.isFormValidForMigration = isFormValidForMigration;
+ ctrl.duplicateStack = duplicateStack;
+ ctrl.migrateStack = migrateStack;
+ ctrl.isMigrationButtonDisabled = isMigrationButtonDisabled;
+
+ function isFormValidForMigration() {
+ return ctrl.formValues.endpoint && ctrl.formValues.endpoint.Id;
+ }
+
+ function isFormValidForDuplication() {
+ return isFormValidForMigration() && ctrl.formValues.newName;
+ }
+
+ function duplicateStack() {
+ if (!ctrl.formValues.newName) {
+ Notifications.error(
+ 'Failure',
+ null,
+ 'Stack name is required for duplication'
+ );
+ return;
+ }
+ ctrl.state.duplicationInProgress = true;
+ ctrl.onDuplicate({
+ endpointId: ctrl.formValues.endpoint.Id,
+ name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined
+ })
+ .finally(function() {
+ ctrl.state.duplicationInProgress = false;
+ });
+ }
+
+ function migrateStack() {
+ ctrl.state.migrationInProgress = true;
+ ctrl.onMigrate({
+ endpointId: ctrl.formValues.endpoint.Id,
+ name: ctrl.formValues.newName ? ctrl.formValues.newName : undefined
+ })
+ .finally(function() {
+ ctrl.state.migrationInProgress = false;
+ });
+ }
+
+ function isMigrationButtonDisabled() {
+ return (
+ !ctrl.isFormValidForMigration() ||
+ ctrl.state.duplicationInProgress ||
+ ctrl.state.migrationInProgress ||
+ isTargetEndpointAndCurrentEquals()
+ );
+ }
+
+ function isTargetEndpointAndCurrentEquals() {
+ return (
+ ctrl.formValues.endpoint &&
+ ctrl.formValues.endpoint.Id === ctrl.currentEndpointId
+ );
+ }
+ }
+]);
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form.html b/app/portainer/components/stack-duplication-form/stack-duplication-form.html
new file mode 100644
index 000000000..6e270b7b0
--- /dev/null
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form.html
@@ -0,0 +1,43 @@
+
+
+ Stack duplication / migration
+
+
+
\ No newline at end of file
diff --git a/app/portainer/components/stack-duplication-form/stack-duplication-form.js b/app/portainer/components/stack-duplication-form/stack-duplication-form.js
new file mode 100644
index 000000000..7f6180c39
--- /dev/null
+++ b/app/portainer/components/stack-duplication-form/stack-duplication-form.js
@@ -0,0 +1,12 @@
+angular.module('portainer.app').component('stackDuplicationForm', {
+ templateUrl:
+ 'app/portainer/components/stack-duplication-form/stack-duplication-form.html',
+ controller: 'StackDuplicationFormController',
+ bindings: {
+ onDuplicate: '&',
+ onMigrate: '&',
+ endpoints: '<',
+ groups: '<',
+ currentEndpointId: '<'
+ }
+});
diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js
index dafbc4379..37b7c10a9 100644
--- a/app/portainer/services/api/stackService.js
+++ b/app/portainer/services/api/stackService.js
@@ -4,6 +4,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
'use strict';
var service = {};
+
service.stack = function(id) {
var deferred = $q.defer();
@@ -33,7 +34,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.migrateSwarmStack = function(stack, targetEndpointId) {
+ service.migrateSwarmStack = function(stack, targetEndpointId, newName) {
var deferred = $q.defer();
EndpointProvider.setEndpointID(targetEndpointId);
@@ -45,8 +46,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
deferred.reject({ msg: 'Target endpoint is located in the same Swarm cluster as the current endpoint', err: null });
return;
}
-
- return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id }).$promise;
+ return Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, SwarmID: swarm.Id, Name: newName }).$promise;
})
.then(function success() {
deferred.resolve();
@@ -61,12 +61,12 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
- service.migrateComposeStack = function(stack, targetEndpointId) {
+ service.migrateComposeStack = function(stack, targetEndpointId, newName) {
var deferred = $q.defer();
EndpointProvider.setEndpointID(targetEndpointId);
- Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId }).$promise
+ Stack.migrate({ id: stack.Id, endpointId: stack.EndpointId }, { EndpointID: targetEndpointId, Name: newName }).$promise
.then(function success() {
deferred.resolve();
})
@@ -258,8 +258,7 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
var deferred = $q.defer();
SwarmService.swarm()
- .then(function success(data) {
- var swarm = data;
+ .then(function success(swarm) {
var payload = {
Name: name,
SwarmID: swarm.Id,
@@ -321,5 +320,10 @@ function StackServiceFactory($q, Stack, ResourceControlService, FileUploadServic
return deferred.promise;
};
+ service.duplicateStack = function duplicateStack(name, stackFileContent, env, endpointId, type) {
+ var action = type === 1 ? service.createSwarmStackFromFileContent : service.createComposeStackFromFileContent;
+ return action(name, stackFileContent, env, endpointId);
+ };
+
return service;
}]);
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html
index 85defd1e2..7a0c1427e 100644
--- a/app/portainer/views/stacks/edit/stack.html
+++ b/app/portainer/views/stacks/edit/stack.html
@@ -46,31 +46,15 @@