-
+
-
-
- Items per page:
-
-
-
+
-
-
+
+
+
+
+
+
Container specification
+
+
+
+
+
+
+
+
+
+
+
+
+
Service specification
+
+
+
+
+
+
+
+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js
index 35b086847..438038087 100644
--- a/app/components/service/serviceController.js
+++ b/app/components/service/serviceController.js
@@ -1,6 +1,6 @@
angular.module('service', [])
-.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
-function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) {
+.controller('ServiceController', ['$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
+function ($scope, $stateParams, $state, $location, $anchorScroll, Service, ServiceHelper, Task, Node, Messages, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
@@ -10,7 +10,10 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
$scope.sortType = 'Status';
$scope.sortReverse = false;
- var previousServiceValues = {};
+ $scope.lastVersion = 0;
+
+ var originalService = {};
+ var previousServiceValues = [];
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@@ -35,85 +38,175 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
service.EditReplicas = false;
};
+ $scope.goToItem = function(hash) {
+ $anchorScroll(hash);
+ };
+
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
- service.hasChanges = true;
+ updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
};
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
var removedElement = service.EnvironmentVariables.splice(index, 1);
- service.hasChanges = service.hasChanges || removedElement !== null;
+ if (removedElement !== null) {
+ updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
+ }
};
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
- service.hasChanges = service.hasChanges || variable.value !== variable.originalValue;
+ if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) {
+ updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
+ }
};
$scope.addLabel = function addLabel(service) {
- service.hasChanges = true;
service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
+ updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
};
$scope.removeLabel = function removeLabel(service, index) {
var removedElement = service.ServiceLabels.splice(index, 1);
- service.hasChanges = service.hasChanges || removedElement !== null;
+ if (removedElement !== null) {
+ updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
+ }
};
$scope.updateLabel = function updateLabel(service, label) {
- service.hasChanges = service.hasChanges || label.value !== label.originalValue;
+ if (label.value !== label.originalValue || label.key !== label.originalKey) {
+ updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
+ }
};
$scope.addContainerLabel = function addContainerLabel(service) {
- service.hasChanges = true;
service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' });
+ updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
};
- $scope.removeContainerLabel = function removeContainerLabel(service, index) {
+ $scope.removeContainerLabel = function removeLabel(service, index) {
var removedElement = service.ServiceContainerLabels.splice(index, 1);
- service.hasChanges = service.hasChanges || removedElement !== null;
+ if (removedElement !== null) {
+ updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
+ }
+ };
+ $scope.updateContainerLabel = function updateLabel(service, label) {
+ if (label.value !== label.originalValue || label.key !== label.originalKey) {
+ updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
+ }
+ };
+ $scope.addMount = function addMount(service) {
+ service.ServiceMounts.push({Type: 'volume', Source: '', Target: '', ReadOnly: false });
+ updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
+ };
+ $scope.removeMount = function removeMount(service, index) {
+ var removedElement = service.ServiceMounts.splice(index, 1);
+ if (removedElement !== null) {
+ updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
+ }
+ };
+ $scope.updateMount = function updateMount(service, mount) {
+ updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
+ };
+ $scope.addPlacementConstraint = function addPlacementConstraint(service) {
+ service.ServiceConstraints.push({ key: '', operator: '==', value: '' });
+ updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
+ };
+ $scope.removePlacementConstraint = function removePlacementConstraint(service, index) {
+ var removedElement = service.ServiceConstraints.splice(index, 1);
+ if (removedElement !== null) {
+ updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
+ }
+ };
+ $scope.updatePlacementConstraint = function updatePlacementConstraint(service, constraint) {
+ updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
};
- $scope.changeParallelism = function changeParallelism(service) {
- updateServiceAttribute(service, 'UpdateParallelism', service.newServiceUpdateParallelism);
- service.EditParallelism = false;
+ $scope.addPublishedPort = function addPublishedPort(service) {
+ if (!service.Ports) {
+ service.Ports = [];
+ }
+ service.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp' });
};
- $scope.changeUpdateDelay = function changeUpdateDelay(service) {
- updateServiceAttribute(service, 'UpdateDelay', service.newServiceUpdateDelay);
- service.EditDelay = false;
+ $scope.updatePublishedPort = function updatePublishedPort(service, portMapping) {
+ updateServiceArray(service, 'Ports', service.Ports);
};
- $scope.changeUpdateFailureAction = function changeUpdateFailureAction(service) {
- updateServiceAttribute(service, 'UpdateFailureAction', service.newServiceUpdateFailureAction);
+ $scope.removePortPublishedBinding = function removePortPublishedBinding(service, index) {
+ var removedElement = service.Ports.splice(index, 1);
+ if (removedElement !== null) {
+ updateServiceArray(service, 'Ports', service.Ports);
+ }
};
- $scope.cancelChanges = function changeServiceImage(service) {
- Object.keys(previousServiceValues).forEach(function(attribute) {
- service[attribute] = previousServiceValues[attribute]; // reset service values
- service['newService' + attribute] = previousServiceValues[attribute]; // reset edit fields
+ $scope.cancelChanges = function cancelChanges(service, keys) {
+ if (keys) { // clean out the keys only from the list of modified keys
+ keys.forEach(function(key) {
+ var index = previousServiceValues.indexOf(key);
+ if (index >= 0) {
+ previousServiceValues.splice(index, 1);
+ }
+ });
+ } else { // clean out all changes
+ keys = Object.keys(service);
+ previousServiceValues = [];
+ }
+ keys.forEach(function(attribute) {
+ service[attribute] = originalService[attribute]; // reset service values
});
- previousServiceValues = {}; // clear out all changes
- // clear out environment variable changes
- service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
- service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
- service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
-
service.hasChanges = false;
};
+ $scope.hasChanges = function(service, elements) {
+ var hasChanges = false;
+ elements.forEach(function(key) {
+ hasChanges = hasChanges || (previousServiceValues.indexOf(key) >= 0);
+ });
+ return hasChanges;
+ };
+
$scope.updateService = function updateService(service) {
$('#loadServicesSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
- config.Name = service.newServiceName;
+ config.Name = service.Name;
config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
- config.TaskTemplate.ContainerSpec.Image = service.newServiceImage;
+ config.TaskTemplate.ContainerSpec.Image = service.Image;
+ config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets;
+
if (service.Mode === 'replicated') {
config.Mode.Replicated.Replicas = service.Replicas;
}
+ config.TaskTemplate.ContainerSpec.Mounts = service.ServiceMounts;
+ if (typeof config.TaskTemplate.Placement === 'undefined') {
+ config.TaskTemplate.Placement = {};
+ }
+ config.TaskTemplate.Placement.Constraints = translateKeyValueToConstraints(service.ServiceConstraints);
+
+ config.TaskTemplate.Resources = {
+ Limits: {
+ NanoCPUs: service.LimitNanoCPUs,
+ MemoryBytes: service.LimitMemoryBytes
+ },
+ Reservations: {
+ NanoCPUs: service.ReservationNanoCPUs,
+ MemoryBytes: service.ReservationMemoryBytes
+ }
+ };
config.UpdateConfig = {
- Parallelism: service.newServiceUpdateParallelism,
- Delay: service.newServiceUpdateDelay,
- FailureAction: service.newServiceUpdateFailureAction
+ Parallelism: service.UpdateParallelism,
+ Delay: service.UpdateDelay,
+ FailureAction: service.UpdateFailureAction
+ };
+ config.TaskTemplate.RestartPolicy = {
+ Condition: service.RestartCondition,
+ Delay: service.RestartDelay,
+ MaxAttempts: service.RestartMaxAttempts,
+ Window: service.RestartWindow
+ };
+ config.EndpointSpec = {
+ Mode: config.EndpointSpec.Mode || 'vip',
+ Ports: service.Ports
};
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Service successfully updated", "Service updated");
- $state.go('service', {id: service.Id}, {reload: true});
+ $scope.cancelChanges({});
+ fetchServiceDetails();
}, function (e) {
$('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Unable to update service");
@@ -138,22 +231,28 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
});
};
+ function translateServiceArrays(service) {
+ service.ServiceSecrets = service.Secrets;
+ service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
+ service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
+ service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
+ service.ServiceMounts = angular.copy(service.Mounts);
+ service.ServiceConstraints = translateConstraintsToKeyValue(service.Constraints);
+ }
+
function fetchServiceDetails() {
$('#loadingViewSpinner').show();
Service.get({id: $stateParams.id}, function (d) {
var service = new ServiceViewModel(d);
- service.newServiceName = service.Name;
- service.newServiceImage = service.Image;
- service.newServiceReplicas = service.Replicas;
- service.newServiceUpdateParallelism = service.UpdateParallelism;
- service.newServiceUpdateDelay = service.UpdateDelay;
- service.newServiceUpdateFailureAction = service.UpdateFailureAction;
-
- service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
- service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
- service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
+ $scope.isUpdating = $scope.lastVersion >= service.Version;
+ if (!$scope.isUpdating) {
+ $scope.lastVersion = service.Version;
+ }
+ translateServiceArrays(service);
$scope.service = service;
+ originalService = angular.copy(service);
+
Task.query({filters: {service: [service.Name]}}, function (tasks) {
Node.query({}, function (nodes) {
$scope.displayNode = true;
@@ -178,13 +277,15 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
});
}
- function updateServiceAttribute(service, name, newValue) {
- // ensure we only capture the original previous value, in case we update the attribute multiple times
- if (!previousServiceValues[name]) {
- previousServiceValues[name] = service[name];
+ $scope.updateServiceAttribute = function updateServiceAttribute(service, name) {
+ if (service[name] !== originalService[name] || !(name in originalService)) {
+ service.hasChanges = true;
}
- // update the attribute
- service[name] = newValue;
+ previousServiceValues.push(name);
+ };
+
+ function updateServiceArray(service, name) {
+ previousServiceValues.push(name);
service.hasChanges = true;
}
@@ -195,7 +296,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
var idx = variable.indexOf('=');
var keyValue = [variable.slice(0,idx), variable.slice(idx+1)];
var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
- variables.push({ key: keyValue[0], value: originalValue, originalValue: originalValue, added: true});
+ variables.push({ key: keyValue[0], value: originalValue, originalKey: keyValue[0], originalValue: originalValue, added: true});
});
return variables;
}
@@ -218,7 +319,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
var labels = [];
if (Labels) {
Object.keys(Labels).forEach(function(key) {
- labels.push({ key: key, value: Labels[key], originalValue: Labels[key], added: true});
+ labels.push({ key: key, value: Labels[key], originalKey: key, originalValue: Labels[key], added: true});
});
}
return labels;
@@ -233,5 +334,48 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
return Labels;
}
+ function translateConstraintsToKeyValue(constraints) {
+ function getOperator(constraint) {
+ var indexEquals = constraint.indexOf('==');
+ if (indexEquals >= 0) {
+ return [indexEquals, '=='];
+ }
+ return [constraint.indexOf('!='), '!='];
+ }
+ if (constraints) {
+ var keyValueConstraints = [];
+ constraints.forEach(function(constraint) {
+ var operatorIndices = getOperator(constraint);
+
+ var key = constraint.slice(0, operatorIndices[0]);
+ var operator = operatorIndices[1];
+ var value = constraint.slice(operatorIndices[0] + 2);
+
+ keyValueConstraints.push({
+ key: key,
+ value: value,
+ operator: operator,
+ originalKey: key,
+ originalValue: value
+ });
+ });
+ return keyValueConstraints;
+ }
+ return [];
+ }
+
+ function translateKeyValueToConstraints(keyValueConstraints) {
+ if (keyValueConstraints) {
+ var constraints = [];
+ keyValueConstraints.forEach(function(keyValueConstraint) {
+ if (keyValueConstraint.key && keyValueConstraint.key !== '' && keyValueConstraint.value && keyValueConstraint.value !== '') {
+ constraints.push(keyValueConstraint.key + keyValueConstraint.operator + keyValueConstraint.value);
+ }
+ });
+ return constraints;
+ }
+ return [];
+ }
+
fetchServiceDetails();
}]);
diff --git a/app/components/services/services.html b/app/components/services/services.html
index 02925a678..f68133fe0 100644
--- a/app/components/services/services.html
+++ b/app/components/services/services.html
@@ -58,6 +58,13 @@
+
+
+ Updated at
+
+
+
+ |
Ownership
@@ -85,6 +92,9 @@
+ |
+ {{ service.UpdatedAt|getisodate }}
+ |
diff --git a/app/models/service.js b/app/models/service.js
index 156faefef..c60c3faab 100644
--- a/app/models/service.js
+++ b/app/models/service.js
@@ -2,6 +2,8 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.Model = data;
this.Id = data.ID;
this.Name = data.Spec.Name;
+ this.CreatedAt = data.CreatedAt;
+ this.UpdatedAt = data.UpdatedAt;
this.Image = data.Spec.TaskTemplate.ContainerSpec.Image;
this.Version = data.Version.Index;
if (data.Spec.Mode.Replicated) {
@@ -16,20 +18,52 @@ function ServiceViewModel(data, runningTasks, nodes) {
if (runningTasks) {
this.Running = runningTasks.length;
}
+ if (data.Spec.TaskTemplate.Resources) {
+ if (data.Spec.TaskTemplate.Resources.Limits) {
+ this.LimitNanoCPUs = data.Spec.TaskTemplate.Resources.Limits.NanoCPUs;
+ this.LimitMemoryBytes = data.Spec.TaskTemplate.Resources.Limits.MemoryBytes;
+ }
+ if (data.Spec.TaskTemplate.Resources.Reservations) {
+ this.ReservationNanoCPUs = data.Spec.TaskTemplate.Resources.Reservations.NanoCPUs;
+ this.ReservationMemoryBytes = data.Spec.TaskTemplate.Resources.Reservations.MemoryBytes;
+ }
+ }
+
+ if (data.Spec.TaskTemplate.RestartPolicy) {
+ this.RestartCondition = data.Spec.TaskTemplate.RestartPolicy.Condition;
+ this.RestartDelay = data.Spec.TaskTemplate.RestartPolicy.Delay;
+ this.RestartMaxAttempts = data.Spec.TaskTemplate.RestartPolicy.MaxAttempts;
+ this.RestartWindow = data.Spec.TaskTemplate.RestartPolicy.Window;
+ } else {
+ this.RestartCondition = 'none';
+ this.RestartDelay = 0;
+ this.RestartMaxAttempts = 0;
+ this.RestartWindow = 0;
+ }
+ this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : [];
this.Labels = data.Spec.Labels;
- if (data.Spec.TaskTemplate.ContainerSpec) {
- this.ContainerLabels = data.Spec.TaskTemplate.ContainerSpec.Labels;
+
+ var containerSpec = data.Spec.TaskTemplate.ContainerSpec;
+ if (containerSpec) {
+ this.ContainerLabels = containerSpec.Labels;
+ this.Env = containerSpec.Env;
+ this.Mounts = containerSpec.Mounts || [];
+ this.User = containerSpec.User;
+ this.Dir = containerSpec.Dir;
+ this.Command = containerSpec.Command;
+ this.Secrets = containerSpec.Secrets;
}
- if (data.Spec.TaskTemplate.ContainerSpec.Env) {
- this.Env = data.Spec.TaskTemplate.ContainerSpec.Env;
+ if (data.Spec.EndpointSpec) {
+ this.Ports = data.Spec.EndpointSpec.Ports;
}
+
this.Mounts = [];
if (data.Spec.TaskTemplate.ContainerSpec.Mounts) {
this.Mounts = data.Spec.TaskTemplate.ContainerSpec.Mounts;
}
- if (data.Endpoint.Ports) {
- this.Ports = data.Endpoint.Ports;
- }
+
+ this.VirtualIPs = data.Endpoint ? data.Endpoint.VirtualIPs : [];
+
if (data.Spec.UpdateConfig) {
this.UpdateParallelism = (typeof data.Spec.UpdateConfig.Parallelism !== undefined) ? data.Spec.UpdateConfig.Parallelism || 0 : 1;
this.UpdateDelay = data.Spec.UpdateConfig.Delay || 0;
diff --git a/assets/css/app.css b/assets/css/app.css
index 9ea1b028c..4371ab229 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -125,6 +125,10 @@ a[ng-click]{
padding: 0 !important;
}
+.padding-top {
+ padding-top: 15px !important;
+}
+
.terminal-container {
width: 100%;
padding: 10px 5px;
|