support rpc auth secret token

This commit is contained in:
MaysWind 2016-06-04 15:33:40 +08:00
parent 244aac0f3a
commit 0c9dbbf9e2
11 changed files with 146 additions and 169 deletions

View file

@ -74,6 +74,7 @@
"Aria2 RPC Host": "Aria2 RPC 主机", "Aria2 RPC Host": "Aria2 RPC 主机",
"Aria2 RPC Port": "Aria2 RPC 端口", "Aria2 RPC Port": "Aria2 RPC 端口",
"Aria2 RPC Protocol": "Aria2 RPC 协议", "Aria2 RPC Protocol": "Aria2 RPC 协议",
"Aria2 RPC Secret Token": "Aria2 RPC 密钥",
"Global Stat Refresh Interval": "全局状态刷新间隔", "Global Stat Refresh Interval": "全局状态刷新间隔",
"Download Task Refresh Interval": "下载任务刷新间隔", "Download Task Refresh Interval": "下载任务刷新间隔",
"Aria2 Version": "Aria2 版本", "Aria2 Version": "Aria2 版本",

View file

@ -11,11 +11,13 @@
rpcHost: '', rpcHost: '',
rpcPort: '6800', rpcPort: '6800',
protocol: 'http', protocol: 'http',
secret: '',
globalStatRefreshInterval: 1000, globalStatRefreshInterval: 1000,
downloadTaskRefreshInterval: 1000 downloadTaskRefreshInterval: 1000
}).constant('aria2RpcConstants', { }).constant('aria2RpcConstants', {
rpcServiceVersion: '2.0', rpcServiceVersion: '2.0',
rpcServiceName: 'aria2', rpcServiceName: 'aria2',
rpcSystemServiceName: 'system' rpcSystemServiceName: 'system',
rpcTokenPrefix: 'token:'
}); });
})(); })();

View file

@ -78,6 +78,7 @@
'Aria2 RPC Host': 'Aria2 RPC Host', 'Aria2 RPC Host': 'Aria2 RPC Host',
'Aria2 RPC Port': 'Aria2 RPC Port', 'Aria2 RPC Port': 'Aria2 RPC Port',
'Aria2 RPC Protocol': 'Aria2 RPC Protocol', 'Aria2 RPC Protocol': 'Aria2 RPC Protocol',
'Aria2 RPC Secret Token': 'Aria2 RPC Secret Token',
'Global Stat Refresh Interval': 'Global Stat Refresh Interval', 'Global Stat Refresh Interval': 'Global Stat Refresh Interval',
'Download Task Refresh Interval': 'Download Task Refresh Interval', 'Download Task Refresh Interval': 'Download Task Refresh Interval',
'Aria2 Version': 'Aria2 Version', 'Aria2 Version': 'Aria2 Version',

View file

@ -7,7 +7,7 @@
var pauseDownloadTaskRefresh = false; var pauseDownloadTaskRefresh = false;
var needRequestWholeInfo = true; var needRequestWholeInfo = true;
var refreshDownloadTask = function () { var refreshDownloadTask = function (silent) {
if (pauseDownloadTaskRefresh) { if (pauseDownloadTaskRefresh) {
return; return;
} }
@ -28,7 +28,7 @@
aria2TaskService.processDownloadTasks($rootScope.taskContext.list); aria2TaskService.processDownloadTasks($rootScope.taskContext.list);
$rootScope.taskContext.enableSelectAll = $rootScope.taskContext.list.length > 1; $rootScope.taskContext.enableSelectAll = $rootScope.taskContext.list.length > 1;
} }
}); }, silent);
}; };
$scope.filterByTaskName = function (task) { $scope.filterByTaskName = function (task) {
@ -55,7 +55,7 @@
if (ariaNgSettingService.getDownloadTaskRefreshInterval() > 0) { if (ariaNgSettingService.getDownloadTaskRefreshInterval() > 0) {
downloadTaskRefreshPromise = $interval(function () { downloadTaskRefreshPromise = $interval(function () {
refreshDownloadTask(); refreshDownloadTask(true);
}, ariaNgSettingService.getDownloadTaskRefreshInterval()); }, ariaNgSettingService.getDownloadTaskRefreshInterval());
} }
@ -84,6 +84,6 @@
} }
}); });
$rootScope.loadPromise = refreshDownloadTask(); $rootScope.loadPromise = refreshDownloadTask(false);
}]); }]);
})(); })();

View file

@ -4,10 +4,10 @@
angular.module('ariaNg').controller('MainController', ['$rootScope', '$scope', '$route', '$interval', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $route, $interval, ariaNgCommonService, ariaNgSettingService, aria2TaskService, aria2SettingService) { angular.module('ariaNg').controller('MainController', ['$rootScope', '$scope', '$route', '$interval', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2TaskService', 'aria2SettingService', function ($rootScope, $scope, $route, $interval, ariaNgCommonService, ariaNgSettingService, aria2TaskService, aria2SettingService) {
var globalStatRefreshPromise = null; var globalStatRefreshPromise = null;
var refreshGlobalStat = function () { var refreshGlobalStat = function (silent) {
return aria2SettingService.getGlobalStat(function (result) { return aria2SettingService.getGlobalStat(function (result) {
$scope.globalStat = result; $scope.globalStat = result;
}); }, silent);
}; };
$scope.isTaskSelected = function () { $scope.isTaskSelected = function () {
@ -94,7 +94,7 @@
if (ariaNgSettingService.getGlobalStatRefreshInterval() > 0) { if (ariaNgSettingService.getGlobalStatRefreshInterval() > 0) {
globalStatRefreshPromise = $interval(function () { globalStatRefreshPromise = $interval(function () {
refreshGlobalStat(); refreshGlobalStat(true);
}, ariaNgSettingService.getGlobalStatRefreshInterval()); }, ariaNgSettingService.getGlobalStatRefreshInterval());
} }
@ -104,6 +104,6 @@
} }
}); });
refreshGlobalStat(); refreshGlobalStat(false);
}]); }]);
})(); })();

View file

@ -15,20 +15,20 @@
return aria2SettingService.getSpecifiedOptions(keys); return aria2SettingService.getSpecifiedOptions(keys);
}; };
var refreshBtPeers = function (task) { var refreshBtPeers = function (task, silent) {
return aria2TaskService.getBtTaskPeers(task.gid, function (result) { return aria2TaskService.getBtTaskPeers(task.gid, function (result) {
if (!ariaNgCommonService.extendArray(result, $scope.peers, 'peerId')) { if (!ariaNgCommonService.extendArray(result, $scope.peers, 'peerId')) {
$scope.peers = result; $scope.peers = result;
} }
$scope.healthPercent = aria2TaskService.estimateHealthPercentFromPeers(task, $scope.peers); $scope.healthPercent = aria2TaskService.estimateHealthPercentFromPeers(task, $scope.peers);
}); }, silent);
}; };
var refreshDownloadTask = function () { var refreshDownloadTask = function (silent) {
return aria2TaskService.getTaskStatus($routeParams.gid, function (result) { return aria2TaskService.getTaskStatus($routeParams.gid, function (result) {
if (result.status == 'active' && result.bittorrent) { if (result.status == 'active' && result.bittorrent) {
refreshBtPeers(result); refreshBtPeers(result, true);
} else { } else {
if (tabOrders.indexOf('btpeers') >= 0) { if (tabOrders.indexOf('btpeers') >= 0) {
tabOrders.splice(tabOrders.indexOf('btpeers'), 1); tabOrders.splice(tabOrders.indexOf('btpeers'), 1);
@ -44,7 +44,7 @@
$rootScope.taskContext.list = [$scope.task]; $rootScope.taskContext.list = [$scope.task];
$rootScope.taskContext.selected = {}; $rootScope.taskContext.selected = {};
$rootScope.taskContext.selected[$scope.task.gid] = true; $rootScope.taskContext.selected[$scope.task.gid] = true;
}); }, silent);
}; };
$scope.context = { $scope.context = {
@ -116,7 +116,7 @@
if (ariaNgSettingService.getDownloadTaskRefreshInterval() > 0) { if (ariaNgSettingService.getDownloadTaskRefreshInterval() > 0) {
downloadTaskRefreshPromise = $interval(function () { downloadTaskRefreshPromise = $interval(function () {
refreshDownloadTask(); refreshDownloadTask(true);
}, ariaNgSettingService.getDownloadTaskRefreshInterval()); }, ariaNgSettingService.getDownloadTaskRefreshInterval());
} }
@ -126,6 +126,6 @@
} }
}); });
$rootScope.loadPromise = refreshDownloadTask(); $rootScope.loadPromise = refreshDownloadTask(false);
}]); }]);
})(); })();

View file

@ -4,19 +4,40 @@
angular.module('ariaNg').factory('aria2RpcService', ['$q', 'aria2RpcConstants', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2HttpRpcService', 'aria2WebSocketRpcService', function ($q, aria2RpcConstants, ariaNgCommonService, ariaNgSettingService, aria2HttpRpcService, aria2WebSocketRpcService) { angular.module('ariaNg').factory('aria2RpcService', ['$q', 'aria2RpcConstants', 'ariaNgCommonService', 'ariaNgSettingService', 'aria2HttpRpcService', 'aria2WebSocketRpcService', function ($q, aria2RpcConstants, ariaNgCommonService, ariaNgSettingService, aria2HttpRpcService, aria2WebSocketRpcService) {
var protocol = ariaNgSettingService.getProtocol(); var protocol = ariaNgSettingService.getProtocol();
var checkIsSystemMethod = function (methodName) {
return methodName.indexOf(aria2RpcConstants.rpcSystemServiceName + '.') == 0;
};
var getAria2MethodFullName = function (methodName) { var getAria2MethodFullName = function (methodName) {
return aria2RpcConstants.rpcServiceName + '.' + methodName; return aria2RpcConstants.rpcServiceName + '.' + methodName;
}; };
var invoke = function (method, context) { var invoke = function (method, context) {
var isSystemMethod = checkIsSystemMethod(method);
var rpcSecretToken = ariaNgSettingService.getSecret();
var finalParams = [];
if (rpcSecretToken && !isSystemMethod) {
finalParams.push(aria2RpcConstants.rpcTokenPrefix + rpcSecretToken);
}
if (angular.isArray(context.params) && context.params.length > 0) {
for (var i = 0; i < context.params.length; i++) {
finalParams.push(context.params[i]);
}
}
context.uniqueId = ariaNgCommonService.generateUniqueId(); context.uniqueId = ariaNgCommonService.generateUniqueId();
context.requestBody = { context.requestBody = {
jsonrpc: aria2RpcConstants.rpcServiceVersion, jsonrpc: aria2RpcConstants.rpcServiceVersion,
method: (method.indexOf(aria2RpcConstants.rpcSystemServiceName + '.') != 0 ? getAria2MethodFullName(method) : method), method: (!isSystemMethod ? getAria2MethodFullName(method) : method),
id: context.uniqueId, id: context.uniqueId
params: context.params
}; };
if (finalParams.length > 0) {
context.requestBody.params = finalParams;
}
if (protocol == 'ws') { if (protocol == 'ws') {
return aria2WebSocketRpcService.request(context); return aria2WebSocketRpcService.request(context);
} else { } else {
@ -44,6 +65,33 @@
}); });
}; };
var buildRequestContext = function () {
var context = {};
if (arguments.length > 0) {
var invokeContext = arguments[0];
context.silent = invokeContext.silent === true;
context.callback = invokeContext.callback;
}
if (arguments.length > 1) {
var params = [];
for (var i = 1; i < arguments.length; i++) {
if (arguments[i] != null && !angular.isUndefined(arguments[i])) {
params.push(arguments[i]);
}
}
if (params.length > 0) {
context.params = params;
}
}
return context;
};
return { return {
getBasicTaskParams: function () { getBasicTaskParams: function () {
return [ return [
@ -76,16 +124,10 @@
// return invoke('addMetalink', context); // return invoke('addMetalink', context);
// }, // },
remove: function (context) { remove: function (context) {
return invoke('remove', { return invoke('remove', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
forceRemove: function (context) { forceRemove: function (context) {
return invoke('forceRemove', { return invoke('forceRemove', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
forceRemoveMulti: function (context) { forceRemoveMulti: function (context) {
var contexts = []; var contexts = [];
@ -99,21 +141,13 @@
return invokeMulti(this.forceRemove, contexts, 'gid', context.callback); return invokeMulti(this.forceRemove, contexts, 'gid', context.callback);
}, },
pause: function (context) { pause: function (context) {
return invoke('pause', { return invoke('pause', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
pauseAll: function (context) { pauseAll: function (context) {
return invoke('pauseAll', { return invoke('pauseAll', buildRequestContext(context));
callback: context.callback
});
}, },
forcePause: function (context) { forcePause: function (context) {
return invoke('forcePause', { return invoke('forcePause', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
forcePauseMulti: function (context) { forcePauseMulti: function (context) {
var contexts = []; var contexts = [];
@ -127,15 +161,10 @@
return invokeMulti(this.forcePause, contexts, 'gid', context.callback); return invokeMulti(this.forcePause, contexts, 'gid', context.callback);
}, },
forcePauseAll: function (context) { forcePauseAll: function (context) {
return invoke('forcePauseAll', { return invoke('forcePauseAll', buildRequestContext(context));
callback: context.callback
});
}, },
unpause: function (context) { unpause: function (context) {
return invoke('unpause', { return invoke('unpause', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
unpauseMulti: function (context) { unpauseMulti: function (context) {
var contexts = []; var contexts = [];
@ -149,167 +178,88 @@
return invokeMulti(this.unpause, contexts, 'gid', context.callback); return invokeMulti(this.unpause, contexts, 'gid', context.callback);
}, },
unpauseAll: function (context) { unpauseAll: function (context) {
return invoke('unpauseAll', { return invoke('unpauseAll', buildRequestContext(context));
callback: context.callback
});
}, },
tellStatus: function (context) { tellStatus: function (context) {
return invoke('tellStatus', { return invoke('tellStatus', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
getUris: function (context) { getUris: function (context) {
return invoke('getUris', { return invoke('getUris', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
getFiles: function (context) { getFiles: function (context) {
return invoke('getFiles', { return invoke('getFiles', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
getPeers: function (context) { getPeers: function (context) {
return invoke('getPeers', { return invoke('getPeers', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
getServers: function (context) { getServers: function (context) {
return invoke('getServers', { return invoke('getServers', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
tellActive: function (context) { tellActive: function (context) {
var requestContext = { return invoke('tellActive', buildRequestContext(context,
callback: context.callback angular.isUndefined(context.requestParams) ? null : context.requestParams
}; ));
if (context.requestParams) {
requestContext.params = [context.requestParams];
}
return invoke('tellActive', requestContext);
}, },
tellWaiting: function (context) { tellWaiting: function (context) {
var requestContext = { return invoke('tellWaiting', buildRequestContext(context,
params: [0, 1000], angular.isUndefined(context.offset) ? 0 : context.offset,
callback: context.callback angular.isUndefined(context.num) ? 1000 : context.num,
}; angular.isUndefined(context.requestParams) ? null : context.requestParams
));
if (!angular.isUndefined(context.offset)) {
requestContext.params[0] = context.offset;
}
if (!angular.isUndefined(context.num)) {
requestContext.params[1] = context.num;
}
if (context.requestParams) {
requestContext.params.push(context.requestParams);
}
return invoke('tellWaiting', requestContext);
}, },
tellStopped: function (context) { tellStopped: function (context) {
var requestContext = { return invoke('tellStopped', buildRequestContext(context,
params: [0, 1000], angular.isUndefined(context.offset) ? 0 : context.offset,
callback: context.callback angular.isUndefined(context.num) ? 1000 : context.num,
}; angular.isUndefined(context.requestParams) ? null : context.requestParams
));
if (!angular.isUndefined(context.offset)) {
requestContext.params[0] = context.offset;
}
if (!angular.isUndefined(context.num)) {
requestContext.params[1] = context.num;
}
if (context.requestParams) {
requestContext.params.push(context.requestParams);
}
return invoke('tellStopped', requestContext);
}, },
changePosition: function (context) { changePosition: function (context) {
return invoke('changePosition', { return invoke('changePosition', buildRequestContext(context, context.gid, context.pos, context.how));
params: [context.gid, context.pos, context.how],
callback: context.callback
});
}, },
// changeUri: function (context) { // changeUri: function (context) {
// return invoke('changeUri', context); // return invoke('changeUri', context);
// }, // },
getOption: function (context) { getOption: function (context) {
return invoke('getOption', { return invoke('getOption', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
changeOption: function (context) { changeOption: function (context) {
return invoke('changeOption', { return invoke('changeOption', buildRequestContext(context, context.gid, context.options));
params: [context.gid, context.options],
callback: context.callback
});
}, },
getGlobalOption: function (context) { getGlobalOption: function (context) {
return invoke('getGlobalOption', { return invoke('getGlobalOption', buildRequestContext(context));
callback: context.callback
});
}, },
changeGlobalOption: function (context) { changeGlobalOption: function (context) {
return invoke('changeGlobalOption', { return invoke('changeGlobalOption', buildRequestContext(context, context.options));
params: [context.options],
callback: context.callback
});
}, },
getGlobalStat: function (context) { getGlobalStat: function (context) {
return invoke('getGlobalStat', { return invoke('getGlobalStat', buildRequestContext(context));
callback: context.callback
});
}, },
purgeDownloadResult: function (context) { purgeDownloadResult: function (context) {
return invoke('purgeDownloadResult', { return invoke('purgeDownloadResult', buildRequestContext(context));
callback: context.callback
});
}, },
removeDownloadResult: function (context) { removeDownloadResult: function (context) {
return invoke('removeDownloadResult', { return invoke('removeDownloadResult', buildRequestContext(context, context.gid));
params: [context.gid],
callback: context.callback
});
}, },
getVersion: function (context) { getVersion: function (context) {
return invoke('getVersion', { return invoke('getVersion', buildRequestContext(context));
callback: context.callback
});
}, },
getSessionInfo: function (context) { getSessionInfo: function (context) {
return invoke('getSessionInfo', { return invoke('getSessionInfo', buildRequestContext(context));
callback: context.callback
});
}, },
shutdown: function (context) { shutdown: function (context) {
return invoke('shutdown', { return invoke('shutdown', buildRequestContext(context));
callback: context.callback
});
}, },
forceShutdown: function (context) { forceShutdown: function (context) {
return invoke('forceShutdown', { return invoke('forceShutdown', buildRequestContext(context));
callback: context.callback
});
}, },
saveSession: function (context) { saveSession: function (context) {
return invoke('saveSession', { return invoke('saveSession', buildRequestContext(context));
callback: context.callback
});
}, },
multicall: function (context) { multicall: function (context) {
var requestContext = { var requestContext = {
params: [], params: [],
silent: context.silent === true,
callback: context.callback callback: context.callback
}; };
@ -323,9 +273,7 @@
return invoke('system.multicall', requestContext); return invoke('system.multicall', requestContext);
}, },
listMethods: function (context) { listMethods: function (context) {
return invoke('system.listMethods', { return invoke('system.listMethods', buildRequestContext(context));
callback: context.callback
});
} }
}; };
}]); }]);

View file

@ -92,8 +92,9 @@
callback: callback callback: callback
}) })
}, },
getGlobalStat: function (callback) { getGlobalStat: function (callback, silent) {
return aria2RpcService.getGlobalStat({ return aria2RpcService.getGlobalStat({
silent: !!silent,
callback: function (result) { callback: function (result) {
if (!callback) { if (!callback) {
return; return;

View file

@ -95,7 +95,7 @@
}; };
return { return {
getTaskList: function (type, full, callback) { getTaskList: function (type, full, callback, silent) {
var invokeMethod = null; var invokeMethod = null;
if (type == 'downloading') { if (type == 'downloading') {
@ -110,6 +110,7 @@
return invokeMethod({ return invokeMethod({
requestParams: full ? aria2RpcService.getFullTaskParams() : aria2RpcService.getBasicTaskParams(), requestParams: full ? aria2RpcService.getFullTaskParams() : aria2RpcService.getBasicTaskParams(),
silent: !!silent,
callback: function (result) { callback: function (result) {
if (!callback) { if (!callback) {
return; return;
@ -119,9 +120,10 @@
} }
}); });
}, },
getTaskStatus: function (gid, callback) { getTaskStatus: function (gid, callback, silent) {
return aria2RpcService.tellStatus({ return aria2RpcService.tellStatus({
gid: gid, gid: gid,
silent: !!silent,
callback: function (result) { callback: function (result) {
if (!callback) { if (!callback) {
return; return;
@ -141,16 +143,17 @@
setTaskOption: function (gid, key, value, callback) { setTaskOption: function (gid, key, value, callback) {
var data = {}; var data = {};
data[key] = value; data[key] = value;
return aria2RpcService.changeOption({ return aria2RpcService.changeOption({
gid: gid, gid: gid,
options: data, options: data,
callback: callback callback: callback
}); });
}, },
getBtTaskPeers: function (gid, callback) { getBtTaskPeers: function (gid, callback, silent) {
return aria2RpcService.getPeers({ return aria2RpcService.getPeers({
gid: gid, gid: gid,
silent: !!silent,
callback: function (result) { callback: function (result) {
if (!callback) { if (!callback) {
return; return;

View file

@ -1,7 +1,7 @@
(function () { (function () {
'use strict'; 'use strict';
angular.module('ariaNg').factory('ariaNgSettingService', ['$location', '$translate', 'amMoment', 'localStorageService', 'ariaNgConstants', 'ariaNgDefaultOptions', 'ariaNgLanguages', function ($location, $translate, amMoment, localStorageService, ariaNgConstants, ariaNgDefaultOptions, ariaNgLanguages) { angular.module('ariaNg').factory('ariaNgSettingService', ['$location', '$base64', '$translate', 'amMoment', 'localStorageService', 'ariaNgConstants', 'ariaNgDefaultOptions', 'ariaNgLanguages', function ($location, $base64, $translate, amMoment, localStorageService, ariaNgConstants, ariaNgDefaultOptions, ariaNgLanguages) {
var getDefaultRpcHost = function () { var getDefaultRpcHost = function () {
return $location.$$host; return $location.$$host;
}; };
@ -47,11 +47,12 @@
options.rpcHost = getDefaultRpcHost(); options.rpcHost = getDefaultRpcHost();
} }
if (options.secret) {
options.secret = $base64.decode(options.secret);
}
return options; return options;
}, },
setAllOptions: function (options) {
setOptions(options);
},
applyLanguage: function (lang) { applyLanguage: function (lang) {
if (!ariaNgLanguages[lang]) { if (!ariaNgLanguages[lang]) {
return false; return false;
@ -93,6 +94,17 @@
setProtocol: function (value) { setProtocol: function (value) {
setOption('protocol', value); setOption('protocol', value);
}, },
getSecret: function () {
var value = getOption('secret');
return (value ? $base64.decode(value) : value);
},
setSecret: function (value) {
if (value) {
value = $base64.encode(value);
}
setOption('secret', value);
},
getGlobalStatRefreshInterval: function () { getGlobalStatRefreshInterval: function () {
return getOption('globalStatRefreshInterval'); return getOption('globalStatRefreshInterval');
}, },

View file

@ -46,6 +46,15 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row">
<div class="setting-key setting-key-without-desc col-sm-4">
<span translate>Aria2 RPC Secret Token</span>
<span class="asterisk">*</span>
</div>
<div class="setting-value col-sm-8">
<input class="form-control" type="password" ng-model="settings.secret" ng-change="settingService.setSecret(settings.secret)"/>
</div>
</div>
<div class="row"> <div class="row">
<div class="setting-key setting-key-without-desc col-sm-4"> <div class="setting-key setting-key-without-desc col-sm-4">
<span translate>Global Stat Refresh Interval</span> <span translate>Global Stat Refresh Interval</span>