Updated to include selectable domain statistics.
- added support for decent duration display - added support for byte count display (k/m/g/, etc) - added detail selector, you can watch n number of connections and refresh while keeping them open. - refresh button
This commit is contained in:
parent
72b4c4598f
commit
860580c7c8
|
@ -67,6 +67,7 @@
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||||
|
<script src="/admin/js/vendor/filter.js"></script>
|
||||||
<script src="/admin/js/app.js"></script>
|
<script src="/admin/js/app.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
console.log("app.sh startup")
|
console.log("app.sh startup")
|
||||||
|
|
||||||
var app = angular.module("rvpnApp", ["ngRoute"]);
|
var app = angular.module("rvpnApp", ["ngRoute", "angular-duration-format"]);
|
||||||
|
|
||||||
app.config(function($routeProvider, $locationProvider) {
|
app.config(function($routeProvider, $locationProvider) {
|
||||||
$routeProvider
|
$routeProvider
|
||||||
.when("/admin/index.html", {
|
.when("/admin/index.html", {
|
||||||
|
@ -18,27 +19,85 @@ app.config(function($routeProvider, $locationProvider) {
|
||||||
$locationProvider.html5Mode(true);
|
$locationProvider.html5Mode(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.controller('serverController', function ($scope, $http) {
|
app.filter('bytes', function() {
|
||||||
$scope.servers = [];
|
return function(bytes, precision) {
|
||||||
var api = '/api/com.daplie.rvpn/servers'
|
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
|
||||||
|
if (typeof precision === 'undefined') precision = 1;
|
||||||
$http.get(api).then(function(response) {
|
var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
|
||||||
updateView(response.data);
|
number = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updateView = function(data) {
|
app.filter('hfcduration', function() {
|
||||||
console.log(data);
|
return function(duration, precision) {
|
||||||
if (data.error == 'ok' ){
|
remain = duration
|
||||||
console.log("ok")
|
duration_day = 24*60*60
|
||||||
$scope.servers = data.result.servers;
|
duration_hour = 60*60
|
||||||
console.log(data.result)
|
duration_minute = 60
|
||||||
|
duration_str = ""
|
||||||
|
|
||||||
|
days = Math.floor(remain / duration_day)
|
||||||
|
if (days > 0) {
|
||||||
|
remain = remain - (days * duration_day)
|
||||||
|
duration_str = duration_str + days + 'd'
|
||||||
|
}
|
||||||
|
|
||||||
|
hours = Math.floor(remain / duration_hour)
|
||||||
|
if (hours > 0) {
|
||||||
|
remain = remain - (hours * duration_hour)
|
||||||
|
duration_str = duration_str + hours + 'h'
|
||||||
|
}
|
||||||
|
|
||||||
|
mins = Math.floor(remain / duration_minute)
|
||||||
|
if (mins > 0) {
|
||||||
|
remain = remain - (mins * duration_minute)
|
||||||
|
duration_str = duration_str + mins + 'm'
|
||||||
|
}
|
||||||
|
|
||||||
|
secs = Math.floor(remain)
|
||||||
|
duration_str = duration_str + secs + 's'
|
||||||
|
|
||||||
|
return (duration_str);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.controller('serverController', function ($scope, $http) {
|
||||||
|
$scope.servers = [];
|
||||||
|
$scope.servers_search = "";
|
||||||
|
$scope.servers_trigger_details = [];
|
||||||
|
|
||||||
|
var api = '/api/com.daplie.rvpn/servers'
|
||||||
|
|
||||||
|
$scope.updateView = function() {
|
||||||
|
$http.get(api).then(function(response) {
|
||||||
|
//console.log(response);
|
||||||
|
data = response.data;
|
||||||
|
if (data.error == 'ok' ){
|
||||||
|
$scope.servers = data.result.servers;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.triggerDetail = function(id) {
|
||||||
|
console.log("triggerDetail ", id, $scope.servers_trigger_details[id])
|
||||||
|
if ($scope.servers_trigger_details[id] == true) {
|
||||||
|
$scope.servers_trigger_details[id] = false;
|
||||||
|
} else {
|
||||||
|
$scope.servers_trigger_details[id] = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.checkDetail = function(id) {
|
||||||
|
console.log("checkDetail ", id, $scope.servers_trigger_details[id])
|
||||||
|
if ($scope.servers_trigger_details[id] == true) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.updateView()
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
// ### filter.js >>
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('angular-duration-format.filter', [ ])
|
||||||
|
.filter('duration', function() {
|
||||||
|
var DURATION_FORMATS_SPLIT = /((?:[^ydhms']+)|(?:'(?:[^']|'')*')|(?:y+|d+|h+|m+|s+))(.*)/;
|
||||||
|
var DURATION_FORMATS = {
|
||||||
|
y: { // years
|
||||||
|
// "longer" years are not supported
|
||||||
|
value: 365 * 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
yy: {
|
||||||
|
value: 'y',
|
||||||
|
pad: 2,
|
||||||
|
},
|
||||||
|
d: { // days
|
||||||
|
value: 24 * 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
dd: {
|
||||||
|
value: 'd',
|
||||||
|
pad: 2,
|
||||||
|
},
|
||||||
|
h: { // hours
|
||||||
|
value: 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
hh: { // padded hours
|
||||||
|
value: 'h',
|
||||||
|
pad: 2,
|
||||||
|
},
|
||||||
|
m: { // minutes
|
||||||
|
value: 60 * 1000,
|
||||||
|
},
|
||||||
|
mm: { // padded minutes
|
||||||
|
value: 'm',
|
||||||
|
pad: 2,
|
||||||
|
},
|
||||||
|
s: { // seconds
|
||||||
|
value: 1000,
|
||||||
|
},
|
||||||
|
ss: { // padded seconds
|
||||||
|
value: 's',
|
||||||
|
pad: 2,
|
||||||
|
},
|
||||||
|
sss: { // milliseconds
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
ssss: { // padded milliseconds
|
||||||
|
value: 'sss',
|
||||||
|
pad: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function _parseFormat(string) {
|
||||||
|
// @inspiration AngularJS date filter
|
||||||
|
var parts = [];
|
||||||
|
var format = string ? string.toString() : '';
|
||||||
|
|
||||||
|
while (format) {
|
||||||
|
var match = DURATION_FORMATS_SPLIT.exec(format);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
parts = parts.concat(match.slice(1));
|
||||||
|
|
||||||
|
format = parts.pop();
|
||||||
|
} else {
|
||||||
|
parts.push(format);
|
||||||
|
|
||||||
|
format = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatDuration(timestamp, format) {
|
||||||
|
var text = '';
|
||||||
|
var values = { };
|
||||||
|
|
||||||
|
format.filter(function(format) { // filter only value parts of format
|
||||||
|
return DURATION_FORMATS.hasOwnProperty(format);
|
||||||
|
}).map(function(format) { // get formats with values only
|
||||||
|
var config = DURATION_FORMATS[format];
|
||||||
|
|
||||||
|
if (config.hasOwnProperty('pad')) {
|
||||||
|
return config.value;
|
||||||
|
} else {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
}).filter(function(format, index, arr) { // remove duplicates
|
||||||
|
return (arr.indexOf(format) === index);
|
||||||
|
}).map(function(format) { // get format configurations with values
|
||||||
|
return angular.extend({
|
||||||
|
name: format,
|
||||||
|
}, DURATION_FORMATS[format]);
|
||||||
|
}).sort(function(a, b) { // sort formats descending by value
|
||||||
|
return b.value - a.value;
|
||||||
|
}).forEach(function(format) { // create values for format parts
|
||||||
|
var value = values[format.name] = Math.floor(timestamp / format.value);
|
||||||
|
|
||||||
|
timestamp = timestamp - (value * format.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
format.forEach(function(part) {
|
||||||
|
var format = DURATION_FORMATS[part];
|
||||||
|
|
||||||
|
if (format) {
|
||||||
|
var value = values[format.value];
|
||||||
|
|
||||||
|
text += (format.hasOwnProperty('pad') ? _padNumber(value, Math.max(format.pad, value.toString().length)) : values[part]);
|
||||||
|
} else {
|
||||||
|
text += part.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _padNumber(number, len) {
|
||||||
|
return ((new Array(len + 1)).join('0') + number).slice(-len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function(value, format) {
|
||||||
|
var parsedValue = parseFloat(value, 10);
|
||||||
|
var parsedFormat = _parseFormat(format);
|
||||||
|
|
||||||
|
if (isNaN(parsedValue) || (parsedFormat.length === 0)) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return _formatDuration(parsedValue, parsedFormat);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ### << filter.js
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ### main.js >>
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('angular-duration-format', [
|
||||||
|
'angular-duration-format.filter',
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
// ### << main.js
|
||||||
|
|
|
@ -9,27 +9,52 @@
|
||||||
<form class="form-inline pull-right">
|
<form class="form-inline pull-right">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="search">Search:</label>
|
<label for="search">Search:</label>
|
||||||
<input type="text" class="form-control" id="search">
|
<input type="text" class="form-control" id="search" data-ng-model="servers_search">
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" title="Refresh" class="btn btn-default" aria-label="Refresh">
|
||||||
|
<span class="glyphicon glyphicon-refresh" title="Refresh" aria-hidden="false" ng-click="updateView()"></span>
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<th width="10%">ID</th>
|
<th width="3%">ID</th>
|
||||||
<th width="20%">Name</th>
|
<th width="10%">Name</th>
|
||||||
<th>Address</th>
|
<th width="10%">Address</th>
|
||||||
<th>Xfer (in/out)</th>
|
<th width="10%">Xfer (in/out)</th>
|
||||||
<th>Time</th>
|
<th width="10%">Time</th>
|
||||||
|
<th width="10%">Idle</th>
|
||||||
|
<th width="1%"><center><span class="glyphicon glyphicon-option-vertical" aria-hidden="true"></span></center></th>
|
||||||
|
|
||||||
<tr ng-repeat="s in servers | orderBy:'server_id'">
|
|
||||||
|
<tr ng-repeat="s in servers | filter:servers_search | orderBy:'server_id'">
|
||||||
<td>{{ s.server_id }}</td>
|
<td>{{ s.server_id }}</td>
|
||||||
<td>{{ s.server_name }}</td>
|
<td>{{ s.server_name }}</td>
|
||||||
<td>{{ s.source_address }}</td>
|
<td>
|
||||||
<td>{{ s.bytes_in }}/{{ s.bytes_out}}</td>
|
{{ s.source_address }}
|
||||||
<td>{{ s.duration }}</td>
|
<div ng-hide="checkDetail(s.server_id)">
|
||||||
|
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
|
||||||
|
   {{ d.domain_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ s.bytes_in | bytes }}/{{ s.bytes_out | bytes }}
|
||||||
|
<div ng-hide="checkDetail(s.server_id)">
|
||||||
|
<div ng-repeat="d in s.domains | orderBy:'domain_name'">
|
||||||
|
{{ d.bytes_in | bytes }}/{{ d.bytes_out | bytes }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ s.duration | hfcduration }}</td>
|
||||||
|
<td>{{ s.idle | hfcduration }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="glyphicon glyphicon-zoom-in" title="Detail" aria-hidden="false" ng-click="triggerDetail(s.server_id)"></span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
2
main.go
2
main.go
|
@ -95,6 +95,6 @@ func main() {
|
||||||
go genericListeners.Run(ctx, argGenericBinding)
|
go genericListeners.Run(ctx, argGenericBinding)
|
||||||
|
|
||||||
//Run for 10 minutes and then shutdown cleanly
|
//Run for 10 minutes and then shutdown cleanly
|
||||||
time.Sleep(600 * time.Second)
|
time.Sleep(6000 * time.Second)
|
||||||
cancelContext()
|
cancelContext()
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ type ServerAPI struct {
|
||||||
ServerID int64 `json:"server_id"`
|
ServerID int64 `json:"server_id"`
|
||||||
Domains []*DomainAPI `json:"domains"`
|
Domains []*DomainAPI `json:"domains"`
|
||||||
Duration float64 `json:"duration"`
|
Duration float64 `json:"duration"`
|
||||||
|
Idle float64 `json:"idle"`
|
||||||
BytesIn int64 `json:"bytes_in"`
|
BytesIn int64 `json:"bytes_in"`
|
||||||
BytesOut int64 `json:"bytes_out"`
|
BytesOut int64 `json:"bytes_out"`
|
||||||
Source string `json:"source_address"`
|
Source string `json:"source_address"`
|
||||||
|
@ -23,6 +24,7 @@ func NewServerAPI(c *Connection) (s *ServerAPI) {
|
||||||
s.ServerID = c.ConnectionID()
|
s.ServerID = c.ConnectionID()
|
||||||
s.Domains = make([]*DomainAPI, 0)
|
s.Domains = make([]*DomainAPI, 0)
|
||||||
s.Duration = time.Since(c.ConnectTime()).Seconds()
|
s.Duration = time.Since(c.ConnectTime()).Seconds()
|
||||||
|
s.Idle = time.Since(c.LastUpdate()).Seconds()
|
||||||
s.BytesIn = c.BytesIn()
|
s.BytesIn = c.BytesIn()
|
||||||
s.BytesOut = c.BytesOut()
|
s.BytesOut = c.BytesOut()
|
||||||
s.Source = c.source
|
s.Source = c.source
|
||||||
|
|
|
@ -11,6 +11,7 @@ type ServersAPI struct {
|
||||||
ServerID int64 `json:"server_id"`
|
ServerID int64 `json:"server_id"`
|
||||||
Domains []*DomainAPI `json:"domains"`
|
Domains []*DomainAPI `json:"domains"`
|
||||||
Duration float64 `json:"duration"`
|
Duration float64 `json:"duration"`
|
||||||
|
Idle float64 `json:"idle"`
|
||||||
BytesIn int64 `json:"bytes_in"`
|
BytesIn int64 `json:"bytes_in"`
|
||||||
BytesOut int64 `json:"bytes_out"`
|
BytesOut int64 `json:"bytes_out"`
|
||||||
Source string `json:"source_address"`
|
Source string `json:"source_address"`
|
||||||
|
@ -23,6 +24,7 @@ func NewServersAPI(c *Connection) (s *ServersAPI) {
|
||||||
s.ServerID = c.ConnectionID()
|
s.ServerID = c.ConnectionID()
|
||||||
s.Domains = make([]*DomainAPI, 0)
|
s.Domains = make([]*DomainAPI, 0)
|
||||||
s.Duration = time.Since(c.ConnectTime()).Seconds()
|
s.Duration = time.Since(c.ConnectTime()).Seconds()
|
||||||
|
s.Idle = time.Since(c.LastUpdate()).Seconds()
|
||||||
s.BytesIn = c.BytesIn()
|
s.BytesIn = c.BytesIn()
|
||||||
s.BytesOut = c.BytesOut()
|
s.BytesOut = c.BytesOut()
|
||||||
s.Source = c.Source()
|
s.Source = c.Source()
|
||||||
|
|
|
@ -178,6 +178,11 @@ func (c *Connection) Update() {
|
||||||
c.lastUpdate = time.Now()
|
c.lastUpdate = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//LastUpdate -- retrieve last update
|
||||||
|
func (c *Connection) LastUpdate() time.Time {
|
||||||
|
return c.lastUpdate
|
||||||
|
}
|
||||||
|
|
||||||
//ConnectionID - Get
|
//ConnectionID - Get
|
||||||
func (c *Connection) ConnectionID() int64 {
|
func (c *Connection) ConnectionID() int64 {
|
||||||
return c.connectionID
|
return c.connectionID
|
||||||
|
|
Loading…
Reference in New Issue