support display bandwidth usage in chart

This commit is contained in:
MaysWind 2016-06-11 01:25:09 +08:00
parent c451b396b0
commit 5851469cc0
11 changed files with 498 additions and 51 deletions

View file

@ -205,6 +205,8 @@
<span>&nbsp;</span>
<div class="pull-right">
<a class="global-status" ng-pop-chart ng-data="globalStatusContext.data" ng-container="body"
ng-placement="top" ng-trigger="click hover" ng-popover-class="global-status-chart">
<span class="realtime-speed">
<i class="icon-download fa fa-arrow-down"></i>
<span ng-bind="(globalStat.downloadSpeed | readableVolumn) + '/s'"></span>
@ -213,6 +215,7 @@
<i class="icon-upload fa fa-arrow-up"></i>
<span ng-bind="(globalStat.uploadSpeed | readableVolumn) + '/s'"></span>
</span>
</a>
</div>
</footer>
</div>
@ -238,8 +241,8 @@
<script src="../bower_components/moment/locale/zh-tw.js"></script>
<script src="../bower_components/moment-timezone/builds/moment-timezone-with-data-2010-2020.min.js"></script>
<!-- endbuild -->
<!-- build:js js/echarts.simple-3.1.10.min.js -->
<script src="../bower_components/echarts/dist/echarts.simple.min.js"></script>
<!-- build:js js/echarts-3.1.10.min.js -->
<script src="../bower_components/echarts/dist/echarts.min.js"></script>
<!-- endbuild -->
<!-- build:js js/plugins.min.js -->
<script src="../bower_components/AdminLTE/dist/js/app.min.js"></script>
@ -277,6 +280,7 @@
<script src="scripts/controllers/settings-ariang.js"></script>
<script src="scripts/controllers/settings-aria2.js"></script>
<script src="scripts/controllers/status.js"></script>
<script src="scripts/directives/chart.js"></script>
<script src="scripts/directives/placeholder.js"></script>
<script src="scripts/directives/setting.js"></script>
<script src="scripts/filters/dateDuration.js"></script>
@ -287,6 +291,7 @@
<script src="scripts/filters/volumn.js"></script>
<script src="scripts/services/ariaNgCommonService.js"></script>
<script src="scripts/services/ariaNgSettingService.js"></script>
<script src="scripts/services/ariaNgMonitorService.js"></script>
<script src="scripts/services/aria2TaskService.js"></script>
<script src="scripts/services/aria2SettingService.js"></script>
<script src="scripts/services/aria2RpcService.js"></script>

View file

@ -63,6 +63,7 @@
"Status": "状态",
"Percent": "完成度",
"Download / Upload Speed": "下载 / 上传速度",
"No Data": "无数据",
"No connected peers": "没有连接到其他节点",
"Failed to change some tasks state.": "修改一些任务状态时失败.",
"Confirm Remove": "确认删除",

View file

@ -5,6 +5,8 @@
title: 'Aria Ng',
appPrefix: 'AriaNg',
optionStorageKey: 'Options',
globalStatStorageCapacity: 120,
taskStatStorageCapacity: 300,
lazySaveTimeout: 500
}).constant('ariaNgDefaultOptions', {
language: 'en',

View file

@ -67,6 +67,7 @@
'Status': 'Status',
'Percent': 'Percent',
'Download / Upload Speed': 'Download / Upload Speed',
'No Data': 'No Data',
'No connected peers': 'No connected peers',
'Failed to change some tasks state.': 'Failed to change some tasks state.',
'Confirm Remove': 'Confirm Remove',

View file

@ -1,7 +1,7 @@
(function () {
'use strict';
angular.module('ariaNg').controller('MainController', ['$rootScope', '$scope', '$route', '$location', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $route, $location, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, aria2TaskService, aria2SettingService) {
angular.module('ariaNg').controller('MainController', ['$rootScope', '$scope', '$route', '$location', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'ariaNgMonitorService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $route, $location, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, ariaNgMonitorService, aria2TaskService, aria2SettingService) {
var globalStatRefreshPromise = null;
var refreshGlobalStat = function (silent) {
@ -13,10 +13,15 @@
if (response.success) {
$scope.globalStat = response.data;
ariaNgMonitorService.recordGlobalStat(response.data);
}
}, silent);
};
$scope.globalStatusContext = {
data: ariaNgMonitorService.getGlobalStatsData()
};
$scope.isTaskSelected = function () {
return $rootScope.taskContext.getSelectedTaskIds().length > 0;
};

View file

@ -1,7 +1,7 @@
(function () {
'use strict';
angular.module('ariaNg').controller('TaskDetailController', ['$rootScope', '$scope', '$routeParams', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $routeParams, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, aria2TaskService, aria2SettingService) {
angular.module('ariaNg').controller('TaskDetailController', ['$rootScope', '$scope', '$routeParams', '$interval', 'aria2RpcErrors', 'ariaNgCommonService', 'ariaNgSettingService', 'ariaNgMonitorService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $routeParams, $interval, aria2RpcErrors, ariaNgCommonService, ariaNgSettingService, ariaNgMonitorService, aria2TaskService, aria2SettingService) {
var tabOrders = ['overview', 'blocks', 'filelist', 'btpeers'];
var downloadTaskRefreshPromise = null;
var pauseDownloadTaskRefresh = false;
@ -32,7 +32,7 @@
$scope.peers = peers;
}
$scope.healthPercent = aria2TaskService.estimateHealthPercentFromPeers(task, $scope.peers);
$scope.context.healthPercent = aria2TaskService.estimateHealthPercentFromPeers(task, $scope.peers);
}, silent);
};
@ -53,9 +53,7 @@
var task = response.data;
if (task.status == 'active' && task.bittorrent) {
if ($scope.context.currentTab == 'btpeers') {
refreshBtPeers(task, true);
}
} else {
if (tabOrders.indexOf('btpeers') >= 0) {
tabOrders.splice(tabOrders.indexOf('btpeers'), 1);
@ -63,7 +61,7 @@
}
if (!$scope.task || $scope.task.status != task.status) {
$scope.availableOptions = getAvailableOptions(task.status, !!task.bittorrent);
$scope.context.availableOptions = getAvailableOptions(task.status, !!task.bittorrent);
}
$scope.task = ariaNgCommonService.copyObjectTo(task, $scope.task);
@ -71,16 +69,18 @@
$rootScope.taskContext.list = [$scope.task];
$rootScope.taskContext.selected = {};
$rootScope.taskContext.selected[$scope.task.gid] = true;
ariaNgMonitorService.recordStat(task.gid, task);
}, silent);
};
$scope.context = {
currentTab: 'overview'
currentTab: 'overview',
healthPercent: 0,
statusData: ariaNgMonitorService.getEmptyStatsData($routeParams.gid),
availableOptions: []
};
$scope.healthPercent = 0;
$scope.availableOptions = [];
$rootScope.swipeActions.extentLeftSwipe = function () {
var tabIndex = tabOrders.indexOf($scope.context.currentTab);
@ -152,10 +152,6 @@
}, true);
};
$scope.loadBtPeers = function (task) {
$rootScope.loadPromise = refreshBtPeers(task, false);
};
$scope.loadTaskOption = function (task) {
$rootScope.loadPromise = aria2TaskService.getTaskOptions(task.gid, function (response) {
if (response.success) {

View file

@ -0,0 +1,198 @@
(function () {
'use strict';
angular.module('ariaNg').directive('ngChart', ['$window', 'chartTheme', function ($window, chartTheme) {
return {
restrict: 'E',
template: '<div></div>',
scope: {
options: '=ngData'
},
link: function (scope, element, attrs) {
var options = {
ngTheme: 'default'
};
angular.extend(options, attrs);
var wrapper = element.find('div');
var wrapperParent = element.parent();
var parentHeight = wrapperParent.height();
var height = parseInt(attrs.height) || parentHeight || 200;
wrapper.css('height', height + 'px');
var chart = echarts.init(wrapper[0], chartTheme.get(options.ngTheme));
var setOptions = function (value) {
chart.setOption(value);
};
angular.element($window).on('resize', function () {
chart.resize();
scope.$apply();
});
scope.$watch(function () {
return scope.options;
}, function (value) {
if (value) {
setOptions(value);
}
}, true);
}
};
}]).directive('ngPopChart', ['$window', 'chartTheme', function ($window, chartTheme) {
return {
restrict: 'A',
scope: {
options: '=ngData'
},
link: function (scope, element, attrs) {
var options = {
ngTheme: 'default',
ngPopoverClass: '',
ngContainer: 'body',
ngTrigger: 'click',
ngPlacement: 'top'
};
angular.extend(options, attrs);
var chart = null;
var loadingIcon = '<div class="loading"><i class="fa fa-spinner fa-spin fa-2x"></i></div>';
element.popover({
container: options.ngContainer,
content: '<div class="chart-pop-wrapper"><div class="chart-pop ' + options.ngPopoverClass + '">' + loadingIcon +'</div></div>',
html: true,
placement: options.ngPlacement,
template: '<div class="popover chart-popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>',
trigger: options.ngTrigger
}).on('shown.bs.popover', function () {
var wrapper = angular.element('.chart-pop');
var wrapperParent = wrapper.parent();
var parentHeight = wrapperParent.height();
wrapper.empty();
var height = parseInt(attrs.height) || parentHeight || 200;
wrapper.css('height', height + 'px');
chart = echarts.init(wrapper[0], chartTheme.get(options.ngTheme));
}).on('hide.bs.popover', function () {
if (chart && chart.isDisposed()) {
chart.dispose();
}
}).on('hidden.bs.popover', function () {
angular.element('.chart-pop').empty().append(loadingIcon);
});
var setOptions = function (value) {
if (chart && !chart.isDisposed()) {
chart.setOption(value);
}
};
scope.$watch(function () {
return scope.options;
}, function (value) {
if (value) {
setOptions(value);
}
}, true);
}
};
}]).factory('chartTheme', ['chartDefaultTheme', function (chartDefaultTheme) {
var themes = {
defaultTheme: chartDefaultTheme
};
return {
get: function (name) {
return themes[name + 'Theme'] ? themes[name + 'Theme'] : {};
}
};
}]).factory('chartDefaultTheme', function () {
return {
color: ['#74a329', '#3a89e9'],
legend: {
top: 'bottom'
},
toolbox: {
show: false
},
tooltip: {
show: true,
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
axisPointer: {
type: 'line',
lineStyle: {
color: '#233333',
type: 'dashed',
width: 1
},
crossStyle: {
color: '#008acd',
width: 1
},
shadowStyle: {
color: 'rgba(200,200,200,0.2)'
}
}
},
grid: {
x: 40,
y: 20,
x2: 30,
y2: 50
},
categoryAxis: {
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
lineStyle: {
color: '#f3f3f3'
}
}
},
valueAxis: {
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
lineStyle: {
color: '#f3f3f3'
}
},
splitArea: {
show: false
}
},
line: {
itemStyle: {
normal: {
lineStyle: {
width: 2,
type: 'solid'
}
}
},
smooth: true,
symbolSize: 6
},
textStyle: {
fontFamily: 'Hiragino Sans GB, Microsoft YaHei, STHeiti, Helvetica Neue, Helvetica, Arial, sans-serif'
},
animationDuration: 500
};
});
})();

View file

@ -3,13 +3,19 @@
angular.module("ariaNg").filter('readableVolumn', ['numberFilter', function (numberFilter) {
var units = [ 'B', 'KB', 'MB', 'GB' ];
var defaultFractionSize = 2;
return function (value) {
return function (value, fractionSize) {
var unit = units[0];
if (!value) {
value = 0;
} else {
if (angular.isUndefined(fractionSize)) {
fractionSize = defaultFractionSize;
}
if (!angular.isNumber(value)) {
value = parseInt(value);
}
for (var i = 1; i < units.length; i++) {
if (value >= 1024) {
value = value / 1024;
@ -19,8 +25,7 @@
}
}
value = numberFilter(value, 2);
}
value = numberFilter(value, fractionSize);
return value + ' ' + unit;
}

View file

@ -0,0 +1,159 @@
(function () {
'use strict';
angular.module('ariaNg').factory('ariaNgMonitorService', ['$translate', 'moment', 'ariaNgConstants', 'readableVolumnFilter', function ($translate, moment, ariaNgConstants, readableVolumnFilter) {
var storagesInMemory = {};
var globalStorageKey = 'global';
var getStorageCapacity = function (key) {
if (key == globalStorageKey) {
return ariaNgConstants.globalStatStorageCapacity;
} else {
return ariaNgConstants.taskStatStorageCapacity;
}
};
var initStorage = function (key) {
var data = {
legend: {
show: false
},
grid: {
x: 50,
y: 10,
x2: 10,
y2: 10
},
tooltip: {
show: true,
formatter: function (params) {
if (params[0].name == '') {
return '<div>' + $translate.instant('No Data') + '</div>';
}
var time = moment(params[0].name, 'X').format('HH:mm:ss');
var uploadSpeed = readableVolumnFilter(params[0].value) + '/s';
var downloadSpeed = readableVolumnFilter(params[1].value) + '/s';
return '<div>' + time + '</div>'
+ '<div><i class="icon-download fa fa-arrow-down"></i> ' + downloadSpeed +'</div>'
+ '<div><i class="icon-upload fa fa-arrow-up"></i> ' + uploadSpeed + '</div>';
}
},
xAxis: {
data: [],
type: 'category',
boundaryGap: false,
axisLabel: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function (value) {
return readableVolumnFilter(value, 0);
}
}
},
series: [{
type: 'line',
areaStyle: {
normal: {
opacity: 0.1
}
},
smooth: true,
symbolSize: 6,
showAllSymbol: false,
data: []
}, {
type: 'line',
areaStyle: {
normal: {
opacity: 0.1
}
},
smooth: true,
symbolSize: 6,
showAllSymbol: false,
data: []
}]
};
var timeData = data.xAxis.data;
var uploadData = data.series[0].data;
var downloadData = data.series[1].data;
for (var i = 0; i < getStorageCapacity(key); i++) {
timeData.push('');
uploadData.push('');
downloadData.push('');
}
storagesInMemory[key] = data;
return data;
};
var isStorageExist = function (key) {
return !angular.isUndefined(storagesInMemory[key]);
};
var pushToStorage = function (key, stat) {
var storage = storagesInMemory[key];
var timeData = storage.xAxis.data;
var uploadData = storage.series[0].data;
var downloadData = storage.series[1].data;
if (timeData.length >= getStorageCapacity(key)) {
timeData.shift();
uploadData.shift();
downloadData.shift();
}
timeData.push(stat.time);
uploadData.push(stat.uploadSpeed);
downloadData.push(stat.downloadSpeed);
};
var getStorage = function (key) {
return storagesInMemory[key];
};
var removeStorage = function (key) {
delete storagesInMemory[key];
};
return {
recordStat: function (key, stat) {
if (!isStorageExist(key)) {
initStorage(key);
}
stat.time = moment().format('X');
pushToStorage(key, stat);
},
getStatsData: function (key) {
if (!isStorageExist(key)) {
initStorage(key);
}
return getStorage(key);
},
getEmptyStatsData: function (key) {
if (isStorageExist(key)) {
removeStorage(key);
}
return this.getStatsData(key);
},
recordGlobalStat: function (stat) {
return this.recordStat(globalStorageKey, stat);
},
getGlobalStatsData: function () {
return this.getStatsData(globalStorageKey);
}
}
}]);
})();

View file

@ -309,6 +309,17 @@ td {
content: "\f0c9";
}
.skin-aria-ng .global-status {
margin-right: 10px;
color: inherit;
}
.skin-aria-ng .global-status:hover {
border: 1px solid #ccc;
margin-right: 9px;
margin-top: -1px
}
.skin-aria-ng .progress-bar-primary {
background-color: #208fe5;
}
@ -555,6 +566,63 @@ td {
cursor: -webkit-grabbing;
}
/* global-status */
.global-status {
cursor: pointer;
}
.global-status > .realtime-speed {
padding: 0 15px 0 15px;
}
.global-status > .realtime-speed:first-child {
padding-left: 5px;
}
.global-status > .realtime-speed:last-child {
padding-right: 5px;
}
.global-status span.realtime-speed > i {
padding-right: 2px;
}
/* chart */
.chart-popover {
max-width: 320px;
}
.chart-popover .popover-content {
padding: 0;
}
.chart-pop-wrapper {
padding-left: 4px;
padding-right: 4px;
overflow-x: hidden;
}
.chart-pop {
display: table;
}
.chart-pop .loading {
width: 100%;
height: 100%;
display: table-cell;
text-align: center;
vertical-align: middle;
}
.global-status-chart {
width: 312px;
height: 200px;
}
.task-status-chart-wrapper {
overflow-x: hidden;
}
/* task-table */
.task-table {
margin-left: 15px;
@ -689,6 +757,14 @@ td {
background-color: #f5f5f5;
}
.skin-aria-ng .settings-table > div.row.no-background {
background-color: inherit;
}
.skin-aria-ng .settings-table > div.row.no-hover:hover {
background-color: inherit;
}
.settings-table .input-group-addon {
background-color: #eee;
}
@ -755,11 +831,3 @@ td {
}
}
/* miscellaneous */
span.realtime-speed {
padding: 0 15px 0 15px;
}
span.realtime-speed > i {
padding-right: 2px;
}

View file

@ -11,7 +11,7 @@
<a class="pointer-cursor" ng-click="context.currentTab = 'filelist'" translate>Files</a>
</li>
<li ng-class="{'active': context.currentTab == 'btpeers'}" ng-if="task && task.status == 'active' && task.bittorrent">
<a class="pointer-cursor" ng-click="context.currentTab = 'btpeers';loadBtPeers(task);" translate>Peers</a>
<a class="pointer-cursor" ng-click="context.currentTab = 'btpeers';" translate>Peers</a>
</li>
<li ng-class="{'active': context.currentTab == 'settings'}" ng-if="task && (task.status == 'active' || task.status == 'waiting' || task.status == 'paused')" class="slim">
<a class="pointer-cursor" ng-click="context.currentTab = 'settings';loadTaskOption(task);">
@ -61,7 +61,7 @@
<span ng-bind="('Completed Percent' | translate) + (task.status == 'active' && task.bittorrent ? ' (' + ('Health Percent' | translate) + ')' : '')"></span>
</div>
<div class="setting-value col-sm-8">
<span ng-bind="(task.completePercent | percent: 2) + '%' + (task.status == 'active' && task.bittorrent ? ' (' + (healthPercent | percent: 2) + '%' + ')' : '')"></span>
<span ng-bind="(task.completePercent | percent: 2) + '%' + (task.status == 'active' && task.bittorrent ? ' (' + (context.healthPercent | percent: 2) + '%' + ')' : '')"></span>
</div>
</div>
<div class="row" ng-if="task">
@ -120,6 +120,13 @@
<span ng-bind="task.dir"></span>
</div>
</div>
<div class="row no-hover no-background" ng-if="task && task.status == 'active'">
<div class="col-sm-12">
<div class="task-status-chart-wrapper">
<ng-chart ng-data="context.statusData" height="200"></ng-chart>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane" ng-class="{'active': context.currentTab == 'blocks'}">
@ -217,7 +224,7 @@
</div>
<div class="tab-pane" ng-class="{'active': context.currentTab == 'settings'}" ng-if="task && (task.status == 'active' || task.status == 'waiting' || task.status == 'paused')">
<div class="settings-table settings-table-firstrow-noborder">
<ng-setting ng-repeat="option in availableOptions" option="option"
<ng-setting ng-repeat="option in context.availableOptions" option="option"
ng-model="options[option.key]" on-change-value="setOption(key, value, optionStatus)"></ng-setting>
</div>
</div>