diff --git a/app/app.js b/app/app.js
index 49e424aff..a71659b44 100644
--- a/app/app.js
+++ b/app/app.js
@@ -56,6 +56,7 @@ angular.module('portainer', [
'settingsAuthentication',
'sidebar',
'swarm',
+ 'swarmVisualizer',
'task',
'team',
'teams',
@@ -735,7 +736,7 @@ angular.module('portainer', [
}
})
.state('swarm', {
- url: '/swarm/',
+ url: '/swarm',
views: {
'content@': {
templateUrl: 'app/components/swarm/swarm.html',
@@ -746,7 +747,21 @@ angular.module('portainer', [
controller: 'SidebarController'
}
}
- });
+ })
+ .state('swarm.visualizer', {
+ url: '/visualizer',
+ views: {
+ 'content@': {
+ templateUrl: 'app/components/swarmVisualizer/swarmVisualizer.html',
+ controller: 'SwarmVisualizerController'
+ },
+ 'sidebar@': {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
+ ;
}])
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) {
EndpointProvider.initialize();
diff --git a/app/components/swarm/swarm.html b/app/components/swarm/swarm.html
index f07b3d296..c585b0cb3 100644
--- a/app/components/swarm/swarm.html
+++ b/app/components/swarm/swarm.html
@@ -58,6 +58,13 @@
Go version |
{{ docker.GoVersion }} |
+
+
+
+ |
+
diff --git a/app/components/swarmVisualizer/swarmVisualizer.html b/app/components/swarmVisualizer/swarmVisualizer.html
new file mode 100644
index 000000000..373f9f899
--- /dev/null
+++ b/app/components/swarmVisualizer/swarmVisualizer.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+ Swarm > Cluster visualizer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nodes |
+ {{ nodes.length }} |
+
+
+ Services |
+ {{ services.length }} |
+
+
+ Tasks |
+ {{ tasks.length }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ node.Hostname }}
+
{{ node.Role }}
+
+
+
+
{{ task.ServiceName }}
+
Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}
+
Status: {{ task.Status.State }}
+
Update: {{ task.Updated | getisodate }}
+
+
+
+
+
+
+
+
diff --git a/app/components/swarmVisualizer/swarmVisualizerController.js b/app/components/swarmVisualizer/swarmVisualizerController.js
new file mode 100644
index 000000000..c4aabeb4c
--- /dev/null
+++ b/app/components/swarmVisualizer/swarmVisualizerController.js
@@ -0,0 +1,74 @@
+angular.module('swarmVisualizer', [])
+.controller('SwarmVisualizerController', ['$q', '$scope', '$document', 'NodeService', 'ServiceService', 'TaskService', 'Notifications',
+function ($q, $scope, $document, NodeService, ServiceService, TaskService, Notifications) {
+
+ $scope.state = {
+ ShowInformationPanel: true,
+ DisplayOnlyRunningTasks: false
+ };
+
+ function assignServiceName(services, tasks) {
+ for (var i = 0; i < services.length; i++) {
+ var service = services[i];
+
+ for (var j = 0; j < tasks.length; j++) {
+ var task = tasks[j];
+
+ if (task.ServiceId === service.Id) {
+ task.ServiceName = service.Name;
+ }
+ }
+ }
+ }
+
+ function assignTasksToNode(nodes, tasks) {
+ for (var i = 0; i < nodes.length; i++) {
+ var node = nodes[i];
+ node.Tasks = [];
+
+ for (var j = 0; j < tasks.length; j++) {
+ var task = tasks[j];
+
+ if (task.NodeId === node.Id) {
+ node.Tasks.push(task);
+ }
+ }
+ }
+ }
+
+ function prepareVisualizerData(nodes, services, tasks) {
+ var visualizerData = {};
+
+ assignServiceName(services, tasks);
+ assignTasksToNode(nodes, tasks);
+
+ visualizerData.nodes = nodes;
+ $scope.visualizerData = visualizerData;
+ }
+
+ function initView() {
+ $('#loadingViewSpinner').show();
+ $q.all({
+ nodes: NodeService.nodes(),
+ services: ServiceService.services(),
+ tasks: TaskService.tasks()
+ })
+ .then(function success(data) {
+ var nodes = data.nodes;
+ $scope.nodes = nodes;
+ var services = data.services;
+ $scope.services = services;
+ var tasks = data.tasks;
+ $scope.tasks = tasks;
+ prepareVisualizerData(nodes, services, tasks);
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to initialize cluster visualizer');
+ })
+ .finally(function final() {
+ $('#loadingViewSpinner').hide();
+ });
+ }
+
+ initView();
+}]);
diff --git a/app/filters/filters.js b/app/filters/filters.js
index b4cac3fe0..e1d61bd95 100644
--- a/app/filters/filters.js
+++ b/app/filters/filters.js
@@ -37,6 +37,20 @@ angular.module('portainer.filters', [])
}
};
})
+.filter('visualizerTask', function () {
+ 'use strict';
+ return function (text) {
+ var status = _.toLower(text);
+ if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'complete', 'preparing'])) {
+ return 'info';
+ } else if (includeString(status, ['pending'])) {
+ return 'warning';
+ } else if (includeString(status, ['shutdown', 'failed', 'rejected'])) {
+ return 'stopped';
+ }
+ return 'running';
+ };
+})
.filter('taskstatusbadge', function () {
'use strict';
return function (text) {
diff --git a/app/services/docker/nodeService.js b/app/services/docker/nodeService.js
index 2fb394a58..6ccafb318 100644
--- a/app/services/docker/nodeService.js
+++ b/app/services/docker/nodeService.js
@@ -3,7 +3,7 @@ angular.module('portainer.services')
'use strict';
var service = {};
- service.nodes = function(id) {
+ service.nodes = function() {
var deferred = $q.defer();
Node.query({}).$promise
diff --git a/app/services/docker/serviceService.js b/app/services/docker/serviceService.js
index 939f1875e..0020c3412 100644
--- a/app/services/docker/serviceService.js
+++ b/app/services/docker/serviceService.js
@@ -3,6 +3,23 @@ angular.module('portainer.services')
'use strict';
var service = {};
+ service.services = function() {
+ var deferred = $q.defer();
+
+ Service.query().$promise
+ .then(function success(data) {
+ var services = data.map(function (item) {
+ return new ServiceViewModel(item);
+ });
+ deferred.resolve(services);
+ })
+ .catch(function error(err) {
+ deferred.reject({ msg: 'Unable to retrieve services', err: err });
+ });
+
+ return deferred.promise;
+ };
+
service.service = function(id) {
var deferred = $q.defer();
diff --git a/app/services/docker/taskService.js b/app/services/docker/taskService.js
index 55b9b4f67..44dab1922 100644
--- a/app/services/docker/taskService.js
+++ b/app/services/docker/taskService.js
@@ -3,6 +3,23 @@ angular.module('portainer.services')
'use strict';
var service = {};
+ service.tasks = function() {
+ var deferred = $q.defer();
+
+ Task.query().$promise
+ .then(function success(data) {
+ var tasks = data.map(function (item) {
+ return new TaskViewModel(item);
+ });
+ deferred.resolve(tasks);
+ })
+ .catch(function error(err) {
+ deferred.reject({ msg: 'Unable to retrieve tasks', err: err });
+ });
+
+ return deferred.promise;
+ };
+
service.task = function(id) {
var deferred = $q.defer();
diff --git a/assets/css/app.css b/assets/css/app.css
index 795b5a722..74964a26b 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -474,6 +474,83 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
}
}
+.visualizer_container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-around;
+}
+
+.visualizer_container .node {
+ border: 1px dashed #337ab7;
+ background-color: rgb(51, 122, 183);
+ background-color: rgba(51, 122, 183, 0.1);
+ border-radius: 4px;
+ box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5);
+ padding: 15px;
+ margin: 5px;
+}
+
+.visualizer_container .node .node_info {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: center;
+ border-bottom: 1px solid #777;
+ padding-bottom: 10px;
+}
+
+.visualizer_container .node .tasks {
+ display: flex;
+ flex-direction: column;
+ margin-top: 5px;
+}
+
+.visualizer_container .node .tasks .task {
+ border: 1px solid #333333;
+ border-radius: 2px;
+ box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5);
+ padding: 10px;
+ margin: 5px;
+}
+
+.visualizer_container .node .tasks .task div {
+ padding: 2px;
+}
+
+.visualizer_container .node .tasks .task_running {
+ border: 2px solid #23ae89;
+ border-radius: 4px;
+ background-color: rgb(35, 174, 137);
+ background-color: rgba(35, 174, 137, 0.2);
+}
+
+.visualizer_container .node .tasks .task_stopped {
+ border: 2px solid #ae2323;
+ border-radius: 4px;
+ background-color: rgb(174, 35, 35);
+ background-color: rgba(174, 35, 35, 0.2);
+}
+
+.visualizer_container .node .tasks .task_warning {
+ border: 2px solid #f0ad4e;
+ border-radius: 4px;
+ background-color: rgb(240, 173, 78);
+ background-color: rgba(240, 173, 78, 0.2);
+}
+
+.visualizer_container .node .tasks .task_info {
+ border: 2px solid #46b8da;
+ border-radius: 4px;
+ background-color: rgb(70, 184, 218);
+ background-color: rgba(70, 184, 218, 0.2);
+}
+
+.visualizer_container .node .tasks .task .service_name {
+ text-align: center;
+ margin-bottom: 5px;
+}
+
/*bootbox override*/
.modal-open {
padding-right: 0 !important;