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 }} + + +
+ Go to cluster visualizer +
+ + 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 }}
+
+
+ Filters +
+
+
+ + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+
+
{{ 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;