mirror of https://github.com/portainer/portainer
feat(swarm-visualizer): add the swarm-visualizer view (#1190)
parent
be4f3ec81d
commit
87825f7ebb
19
app/app.js
19
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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> > <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>
|
|
@ -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();
|
||||
}]);
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue