feat(swarm-visualizer): add the swarm-visualizer view (#1190)

pull/1191/head
Anthony Lapenna 2017-09-14 08:04:59 +02:00 committed by GitHub
parent be4f3ec81d
commit 87825f7ebb
9 changed files with 307 additions and 3 deletions

View File

@ -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();

View File

@ -58,6 +58,13 @@
<td>Go version</td>
<td>{{ docker.GoVersion }}</td>
</tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="swarm.visualizer"><i class="fa fa-object-group space-right" aria-hidden="true"></i>Go to cluster visualizer</a>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>

View File

@ -0,0 +1,83 @@
<rd-header>
<rd-header-title title="Swarm visualizer">
<a data-toggle="tooltip" title="Refresh" ui-sref="swarm.visualizer" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="swarm">Swarm</a> &gt; <a ui-sref="swarm.visualizer">Cluster visualizer</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster information">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-primary" ng-click="state.ShowInformationPanel = true;" ng-if="!state.ShowInformationPanel">Show</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="state.ShowInformationPanel = false;" ng-if="state.ShowInformationPanel">Hide</button>
</div>
</rd-widget-header>
<rd-widget-body ng-if="state.ShowInformationPanel">
<table class="table">
<tbody>
<tr>
<td>Nodes</td>
<td>{{ nodes.length }}</td>
</tr>
<tr>
<td>Services</td>
<td>{{ services.length }}</td>
</tr>
<tr>
<td>Tasks</td>
<td>{{ tasks.length }}</td>
</tr>
</tbody>
</table>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
Filters
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Only display running tasks
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="state.DisplayOnlyRunningTasks"><i></i>
</label>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="visualizerData">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster visualizer"></rd-widget-header>
<rd-widget-body>
<div class="visualizer_container">
<div class="node" ng-repeat="node in visualizerData.nodes track by $index">
<div class="node_info">
<div><b>{{ node.Hostname }}</b></div>
<div>{{ node.Role }}</div>
</div>
<div class="tasks">
<div class="task task_{{ task.Status.State | visualizerTask }}" ng-repeat="task in node.Tasks | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
<div class="service_name">{{ task.ServiceName }}</div>
<div>Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}</div>
<div>Status: {{ task.Status.State }}</div>
<div>Update: {{ task.Updated | getisodate }}</div>
</div>
</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -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();
}]);

View File

@ -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) {

View File

@ -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

View File

@ -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();

View File

@ -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();

View File

@ -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;