mirror of https://github.com/portainer/portainer
feat(container-stats): overhaul (#1183)
parent
b9b32f0526
commit
c0d282e85b
|
@ -23,6 +23,7 @@ angular.module('portainer', [
|
|||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containerStats',
|
||||
'serviceLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
|
@ -54,7 +55,6 @@ angular.module('portainer', [
|
|||
'settings',
|
||||
'settingsAuthentication',
|
||||
'sidebar',
|
||||
'stats',
|
||||
'swarm',
|
||||
'task',
|
||||
'team',
|
||||
|
@ -158,8 +158,8 @@ angular.module('portainer', [
|
|||
url: '^/containers/:id/stats',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
templateUrl: 'app/components/containerStats/containerStats.html',
|
||||
controller: 'ContainerStatsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
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'}},
|
||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||
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: {
|
||||
method: 'POST', params: {id: '@id', action: 'start'},
|
||||
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';
|
||||
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) {
|
||||
var deferred = $q.defer();
|
||||
Container.query({ all: all }).$promise
|
||||
|
@ -11,7 +26,7 @@ angular.module('portainer.services')
|
|||
deferred.resolve(containers);
|
||||
})
|
||||
.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;
|
||||
};
|
||||
|
@ -105,5 +120,35 @@ angular.module('portainer.services')
|
|||
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;
|
||||
}]);
|
||||
|
|
|
@ -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"
|
||||
],
|
||||
"dependencies": {
|
||||
"Chart.js": "1.0.2",
|
||||
"angular": "~1.5.0",
|
||||
"angular-cookies": "~1.5.0",
|
||||
"angular-bootstrap": "~2.5.0",
|
||||
|
@ -49,7 +48,8 @@
|
|||
"bootbox.js": "bootbox#^4.4.0",
|
||||
"angular-multi-select": "~4.0.0",
|
||||
"toastr": "~2.1.3",
|
||||
"xterm.js": "~2.8.1"
|
||||
"xterm.js": "~2.8.1",
|
||||
"chart.js": "~2.6.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.5.11"
|
||||
|
|
|
@ -5,15 +5,14 @@ js:
|
|||
- bower_components/bootstrap/dist/js/bootstrap.js
|
||||
- bower_components/angular-multi-select/isteven-multi-select.js
|
||||
- bower_components/bootbox.js/bootbox.js
|
||||
- bower_components/Chart.js/Chart.js
|
||||
- bower_components/filesize/lib/filesize.js
|
||||
- bower_components/lodash/dist/lodash.js
|
||||
- bower_components/moment/moment.js
|
||||
- bower_components/chart.js/dist/Chart.js
|
||||
- bower_components/splitargs/src/splitargs.js
|
||||
- bower_components/toastr/toastr.js
|
||||
- bower_components/xterm.js/dist/xterm.js
|
||||
- bower_components/xterm.js/dist/addons/fit/fit.js
|
||||
- assets/js/legend.js
|
||||
minified:
|
||||
- bower_components/jquery/dist/jquery.min.js
|
||||
- bower_components/bootstrap/dist/js/bootstrap.min.js
|
||||
|
@ -23,11 +22,11 @@ js:
|
|||
- bower_components/filesize/lib/filesize.min.js
|
||||
- bower_components/lodash/dist/lodash.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/toastr/toastr.min.js
|
||||
- bower_components/xterm.js/dist/xterm.js
|
||||
- bower_components/xterm.js/dist/addons/fit/fit.js
|
||||
- assets/js/legend.js
|
||||
css:
|
||||
regular:
|
||||
- bower_components/bootstrap/dist/css/bootstrap.css
|
||||
|
@ -71,4 +70,4 @@ angular:
|
|||
- bower_components/angular-ui-select/dist/select.min.js
|
||||
- bower_components/angular-ui-router/release/angular-ui-router.min.js
|
||||
- bower_components/angular-utils-pagination/dirPagination.js
|
||||
- bower_components/ng-file-upload/ng-file-upload.min.js
|
||||
- bower_components/ng-file-upload/ng-file-upload.min.js
|
||||
|
|
Loading…
Reference in New Issue