From 5851469cc007789bd508865979661c83ef371766 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 11 Jun 2016 01:25:09 +0800 Subject: [PATCH] support display bandwidth usage in chart --- app/index.html | 25 ++- app/langs/zh_CN.json | 1 + app/scripts/config/constants.js | 2 + app/scripts/config/defaultLanguage.js | 1 + app/scripts/controllers/main.js | 9 +- app/scripts/controllers/task-detail.js | 24 +-- app/scripts/directives/chart.js | 198 +++++++++++++++++++ app/scripts/filters/volumn.js | 33 ++-- app/scripts/services/ariaNgMonitorService.js | 159 +++++++++++++++ app/styles/aria-ng.css | 84 +++++++- app/views/task-detail.html | 13 +- 11 files changed, 498 insertions(+), 51 deletions(-) create mode 100644 app/scripts/directives/chart.js create mode 100644 app/scripts/services/ariaNgMonitorService.js diff --git a/app/index.html b/app/index.html index 4b307a9..a89bb56 100644 --- a/app/index.html +++ b/app/index.html @@ -205,14 +205,17 @@  
- - - - - - - - + + + + + + + + + +
@@ -238,8 +241,8 @@ - - + + @@ -277,6 +280,7 @@ + @@ -287,6 +291,7 @@ + diff --git a/app/langs/zh_CN.json b/app/langs/zh_CN.json index b68e1d6..ba30802 100644 --- a/app/langs/zh_CN.json +++ b/app/langs/zh_CN.json @@ -63,6 +63,7 @@ "Status": "状态", "Percent": "完成度", "Download / Upload Speed": "下载 / 上传速度", + "No Data": "无数据", "No connected peers": "没有连接到其他节点", "Failed to change some tasks state.": "修改一些任务状态时失败.", "Confirm Remove": "确认删除", diff --git a/app/scripts/config/constants.js b/app/scripts/config/constants.js index e1877b4..efa5520 100644 --- a/app/scripts/config/constants.js +++ b/app/scripts/config/constants.js @@ -5,6 +5,8 @@ title: 'Aria Ng', appPrefix: 'AriaNg', optionStorageKey: 'Options', + globalStatStorageCapacity: 120, + taskStatStorageCapacity: 300, lazySaveTimeout: 500 }).constant('ariaNgDefaultOptions', { language: 'en', diff --git a/app/scripts/config/defaultLanguage.js b/app/scripts/config/defaultLanguage.js index 4c39e2c..09e2c77 100644 --- a/app/scripts/config/defaultLanguage.js +++ b/app/scripts/config/defaultLanguage.js @@ -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', diff --git a/app/scripts/controllers/main.js b/app/scripts/controllers/main.js index a8f397e..a25bfd3 100644 --- a/app/scripts/controllers/main.js +++ b/app/scripts/controllers/main.js @@ -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) { @@ -10,13 +10,18 @@ $interval.cancel(globalStatRefreshPromise); return; } - + 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; }; diff --git a/app/scripts/controllers/task-detail.js b/app/scripts/controllers/task-detail.js index ca9075c..974673f 100644 --- a/app/scripts/controllers/task-detail.js +++ b/app/scripts/controllers/task-detail.js @@ -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); - } + 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); @@ -151,10 +151,6 @@ } }, true); }; - - $scope.loadBtPeers = function (task) { - $rootScope.loadPromise = refreshBtPeers(task, false); - }; $scope.loadTaskOption = function (task) { $rootScope.loadPromise = aria2TaskService.getTaskOptions(task.gid, function (response) { diff --git a/app/scripts/directives/chart.js b/app/scripts/directives/chart.js new file mode 100644 index 0000000..bfe6fbc --- /dev/null +++ b/app/scripts/directives/chart.js @@ -0,0 +1,198 @@ +(function () { + 'use strict'; + + angular.module('ariaNg').directive('ngChart', ['$window', 'chartTheme', function ($window, chartTheme) { + return { + restrict: 'E', + template: '
', + 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 = '
'; + + element.popover({ + container: options.ngContainer, + content: '
' + loadingIcon +'
', + html: true, + placement: options.ngPlacement, + template: '', + 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 + }; + }); +})(); diff --git a/app/scripts/filters/volumn.js b/app/scripts/filters/volumn.js index 1c266cb..193dd32 100644 --- a/app/scripts/filters/volumn.js +++ b/app/scripts/filters/volumn.js @@ -3,25 +3,30 @@ 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 { - for (var i = 1; i < units.length; i++) { - if (value >= 1024) { - value = value / 1024; - unit = units[i]; - } else { - break; - } - } - - value = numberFilter(value, 2); + 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; + unit = units[i]; + } else { + break; + } + } + + value = numberFilter(value, fractionSize); + return value + ' ' + unit; } }]); diff --git a/app/scripts/services/ariaNgMonitorService.js b/app/scripts/services/ariaNgMonitorService.js new file mode 100644 index 0000000..b78f273 --- /dev/null +++ b/app/scripts/services/ariaNgMonitorService.js @@ -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 '
' + $translate.instant('No Data') + '
'; + } + + 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 '
' + time + '
' + + '
' + downloadSpeed +'
' + + '
' + uploadSpeed + '
'; + } + }, + 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); + } + } + }]); +})(); diff --git a/app/styles/aria-ng.css b/app/styles/aria-ng.css index 5151cbe..1791d37 100644 --- a/app/styles/aria-ng.css +++ b/app/styles/aria-ng.css @@ -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; -} diff --git a/app/views/task-detail.html b/app/views/task-detail.html index 95d74d9..9033a21 100644 --- a/app/views/task-detail.html +++ b/app/views/task-detail.html @@ -11,7 +11,7 @@ Files
  • - Peers + Peers
  • @@ -61,7 +61,7 @@
    - +
    @@ -120,6 +120,13 @@
    +
    +
    +
    + +
    +
    +
    @@ -217,7 +224,7 @@
    -