mirror of https://github.com/portainer/portainer
feat(container-stats): overhaul (#1183)
parent
b9b32f0526
commit
c0d282e85b
|
@ -23,6 +23,7 @@ angular.module('portainer', [
|
||||||
'container',
|
'container',
|
||||||
'containerConsole',
|
'containerConsole',
|
||||||
'containerLogs',
|
'containerLogs',
|
||||||
|
'containerStats',
|
||||||
'serviceLogs',
|
'serviceLogs',
|
||||||
'containers',
|
'containers',
|
||||||
'createContainer',
|
'createContainer',
|
||||||
|
@ -54,7 +55,6 @@ angular.module('portainer', [
|
||||||
'settings',
|
'settings',
|
||||||
'settingsAuthentication',
|
'settingsAuthentication',
|
||||||
'sidebar',
|
'sidebar',
|
||||||
'stats',
|
|
||||||
'swarm',
|
'swarm',
|
||||||
'task',
|
'task',
|
||||||
'team',
|
'team',
|
||||||
|
@ -158,8 +158,8 @@ angular.module('portainer', [
|
||||||
url: '^/containers/:id/stats',
|
url: '^/containers/:id/stats',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: 'app/components/stats/stats.html',
|
templateUrl: 'app/components/containerStats/containerStats.html',
|
||||||
controller: 'StatsController'
|
controller: 'ContainerStatsController'
|
||||||
},
|
},
|
||||||
'sidebar@': {
|
'sidebar@': {
|
||||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Container statistics">
|
||||||
|
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-info-circle" title="About statistics">
|
||||||
|
</rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="small text-muted">
|
||||||
|
This view displays real-time statistics about the container <b>{{ container.Name|trimcontainername }}</b> as well as a list of the running processes
|
||||||
|
inside this container.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-3 col-md-2">
|
||||||
|
<select id="refreshRate" ng-model="state.refreshRate" ng-change="changeUpdateRepeater()" class="form-control">
|
||||||
|
<option value="5">5s</option>
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">60s</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-4 col-md-6 col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<div class="chart-container" style="position: relative;">
|
||||||
|
<canvas id="memoryChart" width="770" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-md-6 col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<div class="chart-container" style="position: relative;">
|
||||||
|
<canvas id="cpuChart" width="770" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 col-md-12 col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<div class="chart-container" style="position: relative;">
|
||||||
|
<canvas id="networkChart" width="770" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-12" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-tasks" title="Processes">
|
||||||
|
<div class="pull-right">
|
||||||
|
Items per page:
|
||||||
|
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</rd-widget-header>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th ng-repeat="title in processInfo.Titles">
|
||||||
|
<a ng-click="order(title)">
|
||||||
|
{{ title }}
|
||||||
|
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr dir-paginate="processDetails in state.filteredProcesses = (processInfo.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||||
|
<td ng-repeat="procInfo in processDetails track by $index">{{ procInfo }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!processInfo.Processes">
|
||||||
|
<td colspan="processInfo.Titles.length" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="state.filteredProcesses.length === 0">
|
||||||
|
<td colspan="processInfo.Titles.length" class="text-center text-muted">No processes available.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div ng-if="processInfo.Processes" class="pagination-controls">
|
||||||
|
<dir-pagination-controls></dir-pagination-controls>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,159 @@
|
||||||
|
angular.module('containerStats', [])
|
||||||
|
.controller('ContainerStatsController', ['$q', '$scope', '$stateParams', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'Pagination',
|
||||||
|
function ($q, $scope, $stateParams, $document, $interval, ContainerService, ChartService, Notifications, Pagination) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
refreshRate: '5'
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
|
||||||
|
$scope.sortType = 'CMD';
|
||||||
|
$scope.sortReverse = false;
|
||||||
|
|
||||||
|
$scope.order = function (sortType) {
|
||||||
|
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||||
|
$scope.sortType = sortType;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changePaginationCount = function() {
|
||||||
|
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
stopRepeater();
|
||||||
|
});
|
||||||
|
|
||||||
|
function stopRepeater() {
|
||||||
|
var repeater = $scope.repeater;
|
||||||
|
if (angular.isDefined(repeater)) {
|
||||||
|
$interval.cancel(repeater);
|
||||||
|
repeater = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNetworkChart(stats, chart) {
|
||||||
|
var rx = stats.Networks[0].rx_bytes;
|
||||||
|
var tx = stats.Networks[0].tx_bytes;
|
||||||
|
var label = moment(stats.Date).format('HH:mm:ss');
|
||||||
|
|
||||||
|
ChartService.UpdateNetworkChart(label, rx, tx, chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMemoryChart(stats, chart) {
|
||||||
|
var label = moment(stats.Date).format('HH:mm:ss');
|
||||||
|
var value = stats.MemoryUsage;
|
||||||
|
|
||||||
|
ChartService.UpdateMemoryChart(label, value, chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCPUChart(stats, chart) {
|
||||||
|
var label = moment(stats.Date).format('HH:mm:ss');
|
||||||
|
var value = calculateCPUPercentUnix(stats);
|
||||||
|
|
||||||
|
ChartService.UpdateCPUChart(label, value, chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCPUPercentUnix(stats) {
|
||||||
|
var cpuPercent = 0.0;
|
||||||
|
var cpuDelta = stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage;
|
||||||
|
var systemDelta = stats.CurrentCPUSystemUsage - stats.PreviousCPUSystemUsage;
|
||||||
|
|
||||||
|
if (systemDelta > 0.0 && cpuDelta > 0.0) {
|
||||||
|
cpuPercent = (cpuDelta / systemDelta) * stats.CPUCores * 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cpuPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.changeUpdateRepeater = function() {
|
||||||
|
var networkChart = $scope.networkChart;
|
||||||
|
var cpuChart = $scope.cpuChart;
|
||||||
|
var memoryChart = $scope.memoryChart;
|
||||||
|
|
||||||
|
stopRepeater();
|
||||||
|
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
||||||
|
$('#refreshRateChange').show();
|
||||||
|
$('#refreshRateChange').fadeOut(1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
function startChartUpdate(networkChart, cpuChart, memoryChart) {
|
||||||
|
$('#loadingViewSpinner').show();
|
||||||
|
$q.all({
|
||||||
|
stats: ContainerService.containerStats($stateParams.id),
|
||||||
|
top: ContainerService.containerTop($stateParams.id)
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
var stats = data.stats;
|
||||||
|
$scope.processInfo = data.top;
|
||||||
|
updateNetworkChart(stats, networkChart);
|
||||||
|
updateMemoryChart(stats, memoryChart);
|
||||||
|
updateCPUChart(stats, cpuChart);
|
||||||
|
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#loadingViewSpinner').hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpdateRepeater(networkChart, cpuChart, memoryChart) {
|
||||||
|
var refreshRate = $scope.state.refreshRate;
|
||||||
|
$scope.repeater = $interval(function() {
|
||||||
|
$q.all({
|
||||||
|
stats: ContainerService.containerStats($stateParams.id),
|
||||||
|
top: ContainerService.containerTop($stateParams.id)
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
var stats = data.stats;
|
||||||
|
$scope.processInfo = data.top;
|
||||||
|
updateNetworkChart(stats, networkChart);
|
||||||
|
updateMemoryChart(stats, memoryChart);
|
||||||
|
updateCPUChart(stats, cpuChart);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
stopRepeater();
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
|
||||||
|
});
|
||||||
|
}, refreshRate * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCharts() {
|
||||||
|
var networkChartCtx = $('#networkChart');
|
||||||
|
var networkChart = ChartService.CreateNetworkChart(networkChartCtx);
|
||||||
|
$scope.networkChart = networkChart;
|
||||||
|
|
||||||
|
var cpuChartCtx = $('#cpuChart');
|
||||||
|
var cpuChart = ChartService.CreateCPUChart(cpuChartCtx);
|
||||||
|
$scope.cpuChart = cpuChart;
|
||||||
|
|
||||||
|
var memoryChartCtx = $('#memoryChart');
|
||||||
|
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
|
||||||
|
$scope.memoryChart = memoryChart;
|
||||||
|
|
||||||
|
startChartUpdate(networkChart, cpuChart, memoryChart);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
$('#loadingViewSpinner').show();
|
||||||
|
|
||||||
|
ContainerService.container($stateParams.id)
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.container = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve container information');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#loadingViewSpinner').hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
$document.ready(function() {
|
||||||
|
initCharts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -1,94 +0,0 @@
|
||||||
<rd-header>
|
|
||||||
<rd-header-title title="Container stats"></rd-header-title>
|
|
||||||
<rd-header-content>
|
|
||||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
|
||||||
</rd-header-content>
|
|
||||||
</rd-header>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-body>
|
|
||||||
<div class="widget-icon grey pull-left">
|
|
||||||
<i class="fa fa-server"></i>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
|
||||||
<div class="comment">
|
|
||||||
Name
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<canvas id="cpu-stats-chart" width="770" height="230"></canvas>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<canvas id="memory-stats-chart" width="770" height="230"></canvas>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<canvas id="network-stats-chart" width="770" height="230"></canvas>
|
|
||||||
<div class="comment">
|
|
||||||
<div id="network-legend" style="margin-bottom: 20px;"></div>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-tasks" title="Processes">
|
|
||||||
<div class="pull-right">
|
|
||||||
Items per page:
|
|
||||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
|
||||||
<option value="0">All</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="25">25</option>
|
|
||||||
<option value="50">50</option>
|
|
||||||
<option value="100">100</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</rd-widget-header>
|
|
||||||
<rd-widget-body classes="no-padding">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th ng-repeat="title in containerTop.Titles">
|
|
||||||
<a ui-sref="stats({id: container.Id})" ng-click="order(title)">
|
|
||||||
{{title}}
|
|
||||||
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
|
||||||
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
|
||||||
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div ng-if="containerTop.Processes" class="pagination-controls">
|
|
||||||
<dir-pagination-controls></dir-pagination-controls>
|
|
||||||
</div>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,220 +0,0 @@
|
||||||
angular.module('stats', [])
|
|
||||||
.controller('StatsController', ['Pagination', '$scope', 'Notifications', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
|
|
||||||
function (Pagination, $scope, Notifications, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
|
|
||||||
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
|
|
||||||
// TODO: Force memory scale to 0 - max memory
|
|
||||||
$scope.ps_args = '';
|
|
||||||
$scope.state = {};
|
|
||||||
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
|
|
||||||
$scope.sortType = 'CMD';
|
|
||||||
$scope.sortReverse = false;
|
|
||||||
$scope.order = function (sortType) {
|
|
||||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
|
||||||
$scope.sortType = sortType;
|
|
||||||
};
|
|
||||||
$scope.changePaginationCount = function() {
|
|
||||||
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
|
|
||||||
};
|
|
||||||
$scope.getTop = function () {
|
|
||||||
ContainerTop.get($stateParams.id, {
|
|
||||||
ps_args: $scope.ps_args
|
|
||||||
}, function (data) {
|
|
||||||
$scope.containerTop = data;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
var destroyed = false;
|
|
||||||
var timeout;
|
|
||||||
$document.ready(function(){
|
|
||||||
var cpuLabels = [];
|
|
||||||
var cpuData = [];
|
|
||||||
var memoryLabels = [];
|
|
||||||
var memoryData = [];
|
|
||||||
var networkLabels = [];
|
|
||||||
var networkTxData = [];
|
|
||||||
var networkRxData = [];
|
|
||||||
for (var i = 0; i < 20; i++) {
|
|
||||||
cpuLabels.push('');
|
|
||||||
cpuData.push(0);
|
|
||||||
memoryLabels.push('');
|
|
||||||
memoryData.push(0);
|
|
||||||
networkLabels.push('');
|
|
||||||
networkTxData.push(0);
|
|
||||||
networkRxData.push(0);
|
|
||||||
}
|
|
||||||
var cpuDataset = { // CPU Usage
|
|
||||||
fillColor: 'rgba(151,187,205,0.5)',
|
|
||||||
strokeColor: 'rgba(151,187,205,1)',
|
|
||||||
pointColor: 'rgba(151,187,205,1)',
|
|
||||||
pointStrokeColor: '#fff',
|
|
||||||
data: cpuData
|
|
||||||
};
|
|
||||||
var memoryDataset = {
|
|
||||||
fillColor: 'rgba(151,187,205,0.5)',
|
|
||||||
strokeColor: 'rgba(151,187,205,1)',
|
|
||||||
pointColor: 'rgba(151,187,205,1)',
|
|
||||||
pointStrokeColor: '#fff',
|
|
||||||
data: memoryData
|
|
||||||
};
|
|
||||||
var networkRxDataset = {
|
|
||||||
label: 'Rx Bytes',
|
|
||||||
fillColor: 'rgba(151,187,205,0.5)',
|
|
||||||
strokeColor: 'rgba(151,187,205,1)',
|
|
||||||
pointColor: 'rgba(151,187,205,1)',
|
|
||||||
pointStrokeColor: '#fff',
|
|
||||||
data: networkRxData
|
|
||||||
};
|
|
||||||
var networkTxDataset = {
|
|
||||||
label: 'Tx Bytes',
|
|
||||||
fillColor: 'rgba(255,180,174,0.5)',
|
|
||||||
strokeColor: 'rgba(255,180,174,1)',
|
|
||||||
pointColor: 'rgba(255,180,174,1)',
|
|
||||||
pointStrokeColor: '#fff',
|
|
||||||
data: networkTxData
|
|
||||||
};
|
|
||||||
var networkLegendData = [
|
|
||||||
{
|
|
||||||
//value: '',
|
|
||||||
color: 'rgba(151,187,205,0.5)',
|
|
||||||
title: 'Rx Data'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
//value: '',
|
|
||||||
color: 'rgba(255,180,174,0.5)',
|
|
||||||
title: 'Tx Data'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
legend($('#network-legend').get(0), networkLegendData);
|
|
||||||
|
|
||||||
Chart.defaults.global.animationSteps = 30; // Lower from 60 to ease CPU load.
|
|
||||||
var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext('2d')).Line({
|
|
||||||
labels: cpuLabels,
|
|
||||||
datasets: [cpuDataset]
|
|
||||||
}, {
|
|
||||||
responsive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({
|
|
||||||
labels: memoryLabels,
|
|
||||||
datasets: [memoryDataset]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scaleLabel: function (valueObj) {
|
|
||||||
return humansizeFilter(parseInt(valueObj.value, 10), 2);
|
|
||||||
},
|
|
||||||
responsive: true
|
|
||||||
//scaleOverride: true,
|
|
||||||
//scaleSteps: 10,
|
|
||||||
//scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10),
|
|
||||||
//scaleStartValue: 0
|
|
||||||
});
|
|
||||||
var networkChart = new Chart($('#network-stats-chart').get(0).getContext('2d')).Line({
|
|
||||||
labels: networkLabels,
|
|
||||||
datasets: [networkRxDataset, networkTxDataset]
|
|
||||||
}, {
|
|
||||||
scaleLabel: function (valueObj) {
|
|
||||||
return humansizeFilter(parseInt(valueObj.value, 10), 2);
|
|
||||||
},
|
|
||||||
responsive: true
|
|
||||||
});
|
|
||||||
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
|
|
||||||
|
|
||||||
|
|
||||||
function updateStats() {
|
|
||||||
Container.stats({id: $stateParams.id}, function (d) {
|
|
||||||
var arr = Object.keys(d).map(function (key) {
|
|
||||||
return d[key];
|
|
||||||
});
|
|
||||||
if (arr.join('').indexOf('no such id') !== -1) {
|
|
||||||
Notifications.error('Unable to retrieve stats', {}, 'Is this container running?');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update graph with latest data
|
|
||||||
$scope.data = d;
|
|
||||||
updateCpuChart(d);
|
|
||||||
updateMemoryChart(d);
|
|
||||||
updateNetworkChart(d);
|
|
||||||
setUpdateStatsTimeout();
|
|
||||||
}, function () {
|
|
||||||
Notifications.error('Unable to retrieve stats', {}, 'Is this container running?');
|
|
||||||
setUpdateStatsTimeout();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$scope.$on('$destroy', function () {
|
|
||||||
destroyed = true;
|
|
||||||
$timeout.cancel(timeout);
|
|
||||||
});
|
|
||||||
|
|
||||||
updateStats();
|
|
||||||
|
|
||||||
function updateCpuChart(data) {
|
|
||||||
cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString());
|
|
||||||
cpuChart.removeData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMemoryChart(data) {
|
|
||||||
memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString());
|
|
||||||
memoryChart.removeData();
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastRxBytes = 0, lastTxBytes = 0;
|
|
||||||
|
|
||||||
function updateNetworkChart(data) {
|
|
||||||
// 1.9+ contains an object of networks, for now we'll just show stats for the first network
|
|
||||||
// TODO: Show graphs for all networks
|
|
||||||
if (data.networks) {
|
|
||||||
$scope.networkName = Object.keys(data.networks)[0];
|
|
||||||
data.network = data.networks[$scope.networkName];
|
|
||||||
}
|
|
||||||
if(data.network) {
|
|
||||||
var rxBytes = 0, txBytes = 0;
|
|
||||||
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
|
|
||||||
// These will be zero on first call, ignore to prevent large graph spike
|
|
||||||
rxBytes = data.network.rx_bytes - lastRxBytes;
|
|
||||||
txBytes = data.network.tx_bytes - lastTxBytes;
|
|
||||||
}
|
|
||||||
lastRxBytes = data.network.rx_bytes;
|
|
||||||
lastTxBytes = data.network.tx_bytes;
|
|
||||||
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
|
|
||||||
networkChart.removeData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateCPUPercent(stats) {
|
|
||||||
// Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208
|
|
||||||
var prevCpu = stats.precpu_stats;
|
|
||||||
var curCpu = stats.cpu_stats;
|
|
||||||
|
|
||||||
var cpuPercent = 0.0;
|
|
||||||
|
|
||||||
// calculate the change for the cpu usage of the container in between readings
|
|
||||||
var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage;
|
|
||||||
// calculate the change for the entire system between readings
|
|
||||||
var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage;
|
|
||||||
|
|
||||||
if (systemDelta > 0.0 && cpuDelta > 0.0) {
|
|
||||||
cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0;
|
|
||||||
}
|
|
||||||
return cpuPercent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUpdateStatsTimeout() {
|
|
||||||
if(!destroyed) {
|
|
||||||
timeout = $timeout(updateStats, 5000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Container.get({id: $stateParams.id}, function (d) {
|
|
||||||
$scope.container = d;
|
|
||||||
}, function (e) {
|
|
||||||
Notifications.error('Failure', e, 'Unable to retrieve container info');
|
|
||||||
});
|
|
||||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
|
||||||
if (endpointProvider !== 'VMWARE_VIC') {
|
|
||||||
$scope.getTop();
|
|
||||||
}
|
|
||||||
}]);
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
function ContainerStatsViewModel(data) {
|
||||||
|
this.Date = data.read;
|
||||||
|
this.MemoryUsage = data.memory_stats.usage;
|
||||||
|
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
|
||||||
|
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
|
||||||
|
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
|
||||||
|
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
|
||||||
|
if (data.cpu_stats.cpu_usage.percpu_usage) {
|
||||||
|
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
|
||||||
|
}
|
||||||
|
this.Networks = _.values(data.networks);
|
||||||
|
}
|
|
@ -13,7 +13,14 @@ angular.module('portainer.rest')
|
||||||
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
||||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||||
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
||||||
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
|
stats: {
|
||||||
|
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
|
||||||
|
timeout: 4500
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'top' },
|
||||||
|
timeout: 4500
|
||||||
|
},
|
||||||
start: {
|
start: {
|
||||||
method: 'POST', params: {id: '@id', action: 'start'},
|
method: 'POST', params: {id: '@id', action: 'start'},
|
||||||
transformResponse: genericHandler
|
transformResponse: genericHandler
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
angular.module('portainer.rest')
|
|
||||||
.factory('ContainerTop', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
|
||||||
'use strict';
|
|
||||||
return {
|
|
||||||
get: function (id, params, callback, errorCallback) {
|
|
||||||
$http({
|
|
||||||
method: 'GET',
|
|
||||||
url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/top',
|
|
||||||
params: {
|
|
||||||
ps_args: params.ps_args
|
|
||||||
}
|
|
||||||
}).success(callback).error(function (data, status, headers, config) {
|
|
||||||
console.log(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -0,0 +1,251 @@
|
||||||
|
angular.module('portainer.services')
|
||||||
|
.factory('ChartService', [function ChartService() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Max. number of items to display on a chart
|
||||||
|
var CHART_LIMIT = 600;
|
||||||
|
|
||||||
|
var service = {};
|
||||||
|
|
||||||
|
service.CreateCPUChart = function(context) {
|
||||||
|
return new Chart(context, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'CPU',
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||||
|
borderColor: 'rgba(151,187,205,0.6)',
|
||||||
|
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||||
|
pointBorderColor: 'rgba(151,187,205,1)',
|
||||||
|
pointRadius: 2,
|
||||||
|
borderWidth: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
animation: {
|
||||||
|
duration: 0
|
||||||
|
},
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
responsive: true,
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
position: 'nearest',
|
||||||
|
callbacks: {
|
||||||
|
label: function(tooltipItem, data) {
|
||||||
|
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
|
||||||
|
return percentageBasedTooltipLabel(datasetLabel, tooltipItem.yLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
animationDuration: 0
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
beginAtZero: true,
|
||||||
|
callback: percentageBasedAxisLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
service.CreateMemoryChart = function(context) {
|
||||||
|
return new Chart(context, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Memory',
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||||
|
borderColor: 'rgba(151,187,205,0.6)',
|
||||||
|
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||||
|
pointBorderColor: 'rgba(151,187,205,1)',
|
||||||
|
pointRadius: 2,
|
||||||
|
borderWidth: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
animation: {
|
||||||
|
duration: 0
|
||||||
|
},
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
responsive: true,
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
position: 'nearest',
|
||||||
|
callbacks: {
|
||||||
|
label: function(tooltipItem, data) {
|
||||||
|
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
|
||||||
|
return byteBasedTooltipLabel(datasetLabel, tooltipItem.yLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
animationDuration: 0
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
beginAtZero: true,
|
||||||
|
callback: byteBasedAxisLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
service.CreateNetworkChart = function(context) {
|
||||||
|
return new Chart(context, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'RX on eth0',
|
||||||
|
data: [],
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: 'rgba(151,187,205,0.4)',
|
||||||
|
borderColor: 'rgba(151,187,205,0.6)',
|
||||||
|
pointBackgroundColor: 'rgba(151,187,205,1)',
|
||||||
|
pointBorderColor: 'rgba(151,187,205,1)',
|
||||||
|
pointRadius: 2,
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'TX on eth0',
|
||||||
|
data: [],
|
||||||
|
fill: false,
|
||||||
|
backgroundColor: 'rgba(255,180,174,0.5)',
|
||||||
|
borderColor: 'rgba(255,180,174,0.7)',
|
||||||
|
pointBackgroundColor: 'rgba(255,180,174,1)',
|
||||||
|
pointBorderColor: 'rgba(255,180,174,1)',
|
||||||
|
pointRadius: 2,
|
||||||
|
borderWidth: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
animation: {
|
||||||
|
duration: 0
|
||||||
|
},
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
responsive: true,
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
position: 'average',
|
||||||
|
callbacks: {
|
||||||
|
label: function(tooltipItem, data) {
|
||||||
|
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
|
||||||
|
return byteBasedTooltipLabel(datasetLabel, tooltipItem.yLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
animationDuration: 0
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
beginAtZero: true,
|
||||||
|
callback: byteBasedAxisLabel
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
service.UpdateMemoryChart = function(label, value, chart) {
|
||||||
|
chart.data.labels.push(label);
|
||||||
|
chart.data.datasets[0].data.push(value);
|
||||||
|
|
||||||
|
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||||
|
chart.data.labels.pop();
|
||||||
|
chart.data.datasets[0].data.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.update(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
service.UpdateCPUChart = function(label, value, chart) {
|
||||||
|
chart.data.labels.push(label);
|
||||||
|
chart.data.datasets[0].data.push(value);
|
||||||
|
|
||||||
|
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||||
|
chart.data.labels.pop();
|
||||||
|
chart.data.datasets[0].data.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.update(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
service.UpdateNetworkChart = function(label, rx, tx, chart) {
|
||||||
|
chart.data.labels.push(label);
|
||||||
|
chart.data.datasets[0].data.push(rx);
|
||||||
|
chart.data.datasets[1].data.push(tx);
|
||||||
|
|
||||||
|
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||||
|
chart.data.labels.pop();
|
||||||
|
chart.data.datasets[0].data.pop();
|
||||||
|
chart.data.datasets[1].data.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.update(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
function byteBasedTooltipLabel(label, value) {
|
||||||
|
var processedValue = 0;
|
||||||
|
if (value > 5) {
|
||||||
|
processedValue = filesize(value, {base: 10, round: 1});
|
||||||
|
} else {
|
||||||
|
processedValue = value.toFixed(1) + 'B';
|
||||||
|
}
|
||||||
|
return label + ': ' + processedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function byteBasedAxisLabel(value, index, values) {
|
||||||
|
if (value > 5) {
|
||||||
|
return filesize(value, {base: 10, round: 1});
|
||||||
|
}
|
||||||
|
return value.toFixed(1) + 'B';
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentageBasedAxisLabel(value, index, values) {
|
||||||
|
if (value > 1) {
|
||||||
|
return Math.round(value) + '%';
|
||||||
|
}
|
||||||
|
return value.toFixed(1) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentageBasedTooltipLabel(label, value) {
|
||||||
|
var processedValue = 0;
|
||||||
|
if (value > 1) {
|
||||||
|
processedValue = Math.round(value);
|
||||||
|
} else {
|
||||||
|
processedValue = value.toFixed(1);
|
||||||
|
}
|
||||||
|
return label + ': ' + processedValue + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
return service;
|
||||||
|
}]);
|
|
@ -3,6 +3,21 @@ angular.module('portainer.services')
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
service.container = function(id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Container.get({ id: id }).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var container = new ContainerDetailsViewModel(data);
|
||||||
|
deferred.resolve(container);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({ msg: 'Unable to retrieve container information', err: err });
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
service.containers = function(all) {
|
service.containers = function(all) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
Container.query({ all: all }).$promise
|
Container.query({ all: all }).$promise
|
||||||
|
@ -11,7 +26,7 @@ angular.module('portainer.services')
|
||||||
deferred.resolve(containers);
|
deferred.resolve(containers);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to retriever containers', err: err });
|
deferred.reject({ msg: 'Unable to retrieve containers', err: err });
|
||||||
});
|
});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
@ -105,5 +120,35 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.containerStats = function(id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Container.stats({id: id}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var containerStats = new ContainerStatsViewModel(data);
|
||||||
|
deferred.resolve(containerStats);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.containerTop = function(id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Container.top({id: id}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var containerTop = data;
|
||||||
|
deferred.resolve(containerTop);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* legend.js v0.2.0
|
|
||||||
* License: MIT
|
|
||||||
*/
|
|
||||||
function legend(parent, data) {
|
|
||||||
parent.className = 'legend';
|
|
||||||
var datas = data.hasOwnProperty('datasets') ? data.datasets : data;
|
|
||||||
|
|
||||||
datas.forEach(function(d) {
|
|
||||||
var title = document.createElement('span');
|
|
||||||
title.className = 'title';
|
|
||||||
title.style.borderColor = d.hasOwnProperty('strokeColor') ? d.strokeColor : d.color;
|
|
||||||
title.style.borderStyle = 'solid';
|
|
||||||
parent.appendChild(title);
|
|
||||||
|
|
||||||
var text = document.createTextNode(d.title);
|
|
||||||
title.appendChild(text);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -24,7 +24,6 @@
|
||||||
"tests"
|
"tests"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Chart.js": "1.0.2",
|
|
||||||
"angular": "~1.5.0",
|
"angular": "~1.5.0",
|
||||||
"angular-cookies": "~1.5.0",
|
"angular-cookies": "~1.5.0",
|
||||||
"angular-bootstrap": "~2.5.0",
|
"angular-bootstrap": "~2.5.0",
|
||||||
|
@ -49,7 +48,8 @@
|
||||||
"bootbox.js": "bootbox#^4.4.0",
|
"bootbox.js": "bootbox#^4.4.0",
|
||||||
"angular-multi-select": "~4.0.0",
|
"angular-multi-select": "~4.0.0",
|
||||||
"toastr": "~2.1.3",
|
"toastr": "~2.1.3",
|
||||||
"xterm.js": "~2.8.1"
|
"xterm.js": "~2.8.1",
|
||||||
|
"chart.js": "~2.6.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"angular": "1.5.11"
|
"angular": "1.5.11"
|
||||||
|
|
|
@ -5,15 +5,14 @@ js:
|
||||||
- bower_components/bootstrap/dist/js/bootstrap.js
|
- bower_components/bootstrap/dist/js/bootstrap.js
|
||||||
- bower_components/angular-multi-select/isteven-multi-select.js
|
- bower_components/angular-multi-select/isteven-multi-select.js
|
||||||
- bower_components/bootbox.js/bootbox.js
|
- bower_components/bootbox.js/bootbox.js
|
||||||
- bower_components/Chart.js/Chart.js
|
|
||||||
- bower_components/filesize/lib/filesize.js
|
- bower_components/filesize/lib/filesize.js
|
||||||
- bower_components/lodash/dist/lodash.js
|
- bower_components/lodash/dist/lodash.js
|
||||||
- bower_components/moment/moment.js
|
- bower_components/moment/moment.js
|
||||||
|
- bower_components/chart.js/dist/Chart.js
|
||||||
- bower_components/splitargs/src/splitargs.js
|
- bower_components/splitargs/src/splitargs.js
|
||||||
- bower_components/toastr/toastr.js
|
- bower_components/toastr/toastr.js
|
||||||
- bower_components/xterm.js/dist/xterm.js
|
- bower_components/xterm.js/dist/xterm.js
|
||||||
- bower_components/xterm.js/dist/addons/fit/fit.js
|
- bower_components/xterm.js/dist/addons/fit/fit.js
|
||||||
- assets/js/legend.js
|
|
||||||
minified:
|
minified:
|
||||||
- bower_components/jquery/dist/jquery.min.js
|
- bower_components/jquery/dist/jquery.min.js
|
||||||
- bower_components/bootstrap/dist/js/bootstrap.min.js
|
- bower_components/bootstrap/dist/js/bootstrap.min.js
|
||||||
|
@ -23,11 +22,11 @@ js:
|
||||||
- bower_components/filesize/lib/filesize.min.js
|
- bower_components/filesize/lib/filesize.min.js
|
||||||
- bower_components/lodash/dist/lodash.min.js
|
- bower_components/lodash/dist/lodash.min.js
|
||||||
- bower_components/moment/min/moment.min.js
|
- bower_components/moment/min/moment.min.js
|
||||||
|
- bower_components/chart.js/dist/Chart.min.js
|
||||||
- bower_components/splitargs/src/splitargs.js
|
- bower_components/splitargs/src/splitargs.js
|
||||||
- bower_components/toastr/toastr.min.js
|
- bower_components/toastr/toastr.min.js
|
||||||
- bower_components/xterm.js/dist/xterm.js
|
- bower_components/xterm.js/dist/xterm.js
|
||||||
- bower_components/xterm.js/dist/addons/fit/fit.js
|
- bower_components/xterm.js/dist/addons/fit/fit.js
|
||||||
- assets/js/legend.js
|
|
||||||
css:
|
css:
|
||||||
regular:
|
regular:
|
||||||
- bower_components/bootstrap/dist/css/bootstrap.css
|
- bower_components/bootstrap/dist/css/bootstrap.css
|
||||||
|
|
Loading…
Reference in New Issue