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:
Henry Camacho 2017-03-18 14:28:54 -05:00
parent 72b4c4598f
commit 860580c7c8
8 changed files with 267 additions and 25 deletions

View File

@ -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>

View File

@ -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()
}); });

148
html/admin/js/vendor/filter.js vendored Normal file
View File

@ -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

View File

@ -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'">
&nbsp&nbsp&nbsp{{ 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>

View File

@ -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()
} }

View File

@ -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

View File

@ -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()

View File

@ -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