Extended node management in amdin panel.

This commit is contained in:
baldo 2016-06-07 10:50:15 +02:00
parent d5c69fa78f
commit 0f1a21c905
10 changed files with 152 additions and 32 deletions

View file

@ -23,7 +23,15 @@
} }
.monitoring-disabled { .monitoring-disabled {
color: grey; color: lightgrey;
}
.vpn-key-set, .coords-set {
color: green;
}
.vpn-key-unset, .coords-unset {
color: lightgrey;
} }
</style> </style>
</head> </head>

View file

@ -34,20 +34,31 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Cons
nodes nodes
.listView() .listView()
.title('Nodes') .title('Nodes')
.perPage(5)
.actions([]) .actions([])
.batchActions([]) .batchActions([])
.exportFields([]) .exportFields([])
.fields([ .fields([
nga.field('token').cssClasses(nodeClasses), nga.field('token').cssClasses(nodeClasses),
nga.field('mac').cssClasses(nodeClasses),
nga.field('hostname').cssClasses(nodeClasses), nga.field('hostname').cssClasses(nodeClasses),
nga.field('key').label('VPN').cssClasses(nodeClasses).template(function (node) {
return node.values.key
? '<i class="fa fa-lock vpn-key-set" aria-hidden="true" title="VPN key set"></i>'
: '<i class="fa fa-times vpn-key-unset" aria-hidden="true" title="no VPN key"></i>';
}),
nga.field('coords').label('GPS').cssClasses(nodeClasses).template(function (node) {
return node.values.coords
? '<i class="fa fa-map-marker coords-set" aria-hidden="true" title="coordinates set"></i>'
: '<i class="fa fa-times coords-unset" aria-hidden="true" title="no coordinates"></i>';
}),
nga.field('monitoring').cssClasses(nodeClasses).template(function (node) { nga.field('monitoring').cssClasses(nodeClasses).template(function (node) {
if (!node.values.monitoring) { if (!node.values.monitoring) {
return '<span class="glyphicon glyphicon-remove monitoring-disabled" title="disabled"></span>'; return '<i class="fa fa-times monitoring-disabled" title="disabled"></i>';
} }
return node.values.monitoringConfirmed return node.values.monitoringConfirmed
? '<span class="glyphicon glyphicon-ok monitoring-active" title="active"></span>' ? '<i class="fa fa-check monitoring-active" title="active"></i>'
: '<span class="glyphicon glyphicon-envelope monitoring-confirmation-pending" ' + : '<i class="fa fa-envelope monitoring-confirmation-pending" title="confirmation pending"></i>';
'title="confirmation pending"></span>';
}) })
]) ])
.listActions([ .listActions([
@ -105,8 +116,8 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Cons
]) ])
.listActions( .listActions(
'<fa-task-action-button action="run" task="entry" button="primary" label="run" size="sm"></fa-task-action-button> ' + '<fa-task-action-button action="run" task="entry" button="primary" label="run" size="sm"></fa-task-action-button> ' +
'<fa-task-action-button ng-if="!entry.values.enabled" button="success" action="enable" icon="off" task="entry" label="enable" size="sm"></fa-task-action-button> ' + '<fa-task-action-button ng-if="!entry.values.enabled" button="success" action="enable" icon="power-off" task="entry" label="enable" size="sm"></fa-task-action-button> ' +
'<fa-task-action-button ng-if="entry.values.enabled" button="warning" action="disable" icon="off" task="entry" label="disable" size="sm"></fa-task-action-button>' '<fa-task-action-button ng-if="entry.values.enabled" button="warning" action="disable" icon="power-off" task="entry" label="disable" size="sm"></fa-task-action-button>'
) )
; ;
@ -116,17 +127,17 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Cons
nga.menu() nga.menu()
.addChild(nga .addChild(nga
.menu(nodes) .menu(nodes)
.icon('<span class="glyphicon glyphicon-record"></span>') .icon('<i class="fa fa-dot-circle-o"></i>')
) )
.addChild(nga .addChild(nga
.menu(tasks) .menu(tasks)
.icon('<span class="glyphicon glyphicon-cog"></span>') .icon('<span class="fa fa-cog"></span>')
) )
.addChild(nga .addChild(nga
.menu() .menu()
.template( .template(
'<a href="/internal/logs" target="_blank">' + '<a href="/internal/logs" target="_blank">' +
'<span class="glyphicon glyphicon-list"></span> Logs' + '<span class="fa fa-list"></span> Logs' +
'</a>' '</a>'
) )
) )

View file

@ -36,7 +36,7 @@ angular.module('ffffngAdmin')
'template': 'template':
'<button class="btn btn-{{ button }}" ng-disabled="disabled" ng-class="size ? \'btn-\' + size : \'\'" ng-click="perform()">' + '<button class="btn btn-{{ button }}" ng-disabled="disabled" ng-class="size ? \'btn-\' + size : \'\'" ng-click="perform()">' +
'<span class="glyphicon glyphicon-{{ icon }}" aria-hidden="true"></span>&nbsp;<span class="hidden-xs">{{ label }}</span>' + '<span class="fa fa-{{ icon }}" aria-hidden="true"></span>&nbsp;<span class="hidden-xs">{{ label }}</span>' +
'</button>' '</button>'
}; };
}); });

View file

@ -13,6 +13,10 @@ angular.module('ffffng')
geolib, geolib,
OutsideOfCommunityDialog OutsideOfCommunityDialog
) { ) {
// backwards compatibility
$scope.node.monitoring = $scope.node.monitoring || false;
$scope.node.monitoringConfirmed = $scope.node.monitoringConfirmed || false;
var initialEmail = $scope.node.email; var initialEmail = $scope.node.email;
var initialMonitoring = $scope.node.monitoring; var initialMonitoring = $scope.node.monitoring;
var monitoringConfirmed = $scope.node.monitoringConfirmed; var monitoringConfirmed = $scope.node.monitoringConfirmed;

View file

@ -11,7 +11,7 @@ angular.module('ffffng').factory('NodeResource', function (
) { ) {
var nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring']; var nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
function getValidNodeData(reqData) { function getNormalizedNodeData(reqData) {
var node = {}; var node = {};
_.each(nodeFields, function (field) { _.each(nodeFields, function (field) {
var value = Strings.normalizeString(reqData[field]); var value = Strings.normalizeString(reqData[field]);
@ -30,7 +30,7 @@ angular.module('ffffng').factory('NodeResource', function (
create: function (req, res) { create: function (req, res) {
var data = Resources.getData(req); var data = Resources.getData(req);
var node = getValidNodeData(data); var node = getNormalizedNodeData(data);
if (!isValidNode(node)) { if (!isValidNode(node)) {
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
} }
@ -51,7 +51,7 @@ angular.module('ffffng').factory('NodeResource', function (
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
} }
var node = getValidNodeData(data); var node = getNormalizedNodeData(data);
if (!isValidNode(node)) { if (!isValidNode(node)) {
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
} }
@ -95,14 +95,22 @@ angular.module('ffffng').factory('NodeResource', function (
}, },
getAll: function (req, res) { getAll: function (req, res) {
// TODO: Paging + Sort + Filter Resources.getValidRestParams('list', req, function (err, restParams) {
return NodeService.getAllNodes(function (err, nodes) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
} }
return Resources.success(res, nodes); // TODO: Sort + Filter
return NodeService.getAllNodes(restParams._page, restParams._perPage, function (err, nodes, total) {
if (err) {
return Resources.error(res, err);
}
res.set('X-Total-Count', total);
return Resources.success(res, nodes);
});
}); });
} }
}; };

View file

@ -369,11 +369,13 @@ angular.module('ffffng')
deleteNodeFile(token, callback); deleteNodeFile(token, callback);
}, },
getAllNodes: function (callback) { getAllNodes: function (page, perPage, callback) {
var files = findNodeFiles({}); var files = _.sortBy(findNodeFiles({}));
var total = files.length;
var pageFiles = files.slice((page - 1) * perPage, page * perPage);
async.mapLimit( async.mapLimit(
files, pageFiles,
MAX_PARALLEL_NODES_PARSING, MAX_PARALLEL_NODES_PARSING,
parseNodeFile, parseNodeFile,
function (err, nodes) { function (err, nodes) {
@ -382,7 +384,7 @@ angular.module('ffffng')
return callback({data: 'Internal error.', type: ErrorTypes.internalError}); return callback({data: 'Internal error.', type: ErrorTypes.internalError});
} }
return callback(null, nodes); return callback(null, nodes, total);
} }
); );
}, },

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Resources', function (_) { angular.module('ffffng').factory('Resources', function (_, Constraints, Validator, ErrorTypes) {
function respond(res, httpCode, data) { function respond(res, httpCode, data) {
res.writeHead(httpCode, {'Content-Type': 'application/json'}); res.writeHead(httpCode, {'Content-Type': 'application/json'});
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
@ -11,6 +11,31 @@ angular.module('ffffng').factory('Resources', function (_) {
return _.extend({}, req.body, req.params, req.query); return _.extend({}, req.body, req.params, req.query);
}, },
getValidRestParams: function(type, req, callback) {
var constraints = Constraints.rest[type];
if (!_.isPlainObject(constraints)) {
Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type);
return callback({data: 'Internal error.', type: ErrorTypes.internalError});
}
var data = this.getData(req);
var restParams = {};
_.each(_.keys(constraints), function (key) {
var value = data[key];
restParams[key] = _.isUndefined(value) && !_.isUndefined(constraints[key].default)
? constraints[key].default
: value;
});
var areValidParams = Validator.forConstraints(constraints);
if (!areValidParams(restParams)) {
return callback({data: 'Invalid REST parameters.', type: ErrorTypes.badRequest});
}
callback(null, restParams);
},
success: function (res, data) { success: function (res, data) {
respond(res, 200, data); respond(res, 200, data);
}, },

View file

@ -17,6 +17,11 @@ angular.module('ffffng').factory('Strings', function (_) {
} }
return macParts.join(':'); return macParts.join(':');
},
parseInt: function (str) {
var parsed = _.parseInt(str, 10);
return parsed.toString() === str ? parsed : undefined;
} }
}; };
}); });

View file

@ -1,24 +1,64 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Validator', function (_) { angular.module('ffffng').factory('Validator', function (_, Strings, Logger) {
var isValid = function (constraint, acceptUndefined, value) { function isValidBoolean(value) {
if (value === undefined) { return _.isBoolean(value);
return acceptUndefined || constraint.optional; }
function isValidNumber(constraint, value) {
if (_.isString(value)) {
value = Strings.parseInt(value);
} }
if (constraint.type === 'boolean') { if (!_.isNumber(value)) {
return _.isBoolean(value); return false;
} }
if (_.isNaN(value) || !_.isFinite(value)) {
return false;
}
if (_.isNumber(constraint.min) && value < constraint.min) {
return false;
}
if (_.isNumber(constraint.max) && value > constraint.max) {
return false;
}
return true;
}
function isValidString(constraint, value) {
if (!_.isString(value)) { if (!_.isString(value)) {
return false; return false;
} }
var trimmed = value.trim(); var trimmed = value.trim();
return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex); return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex);
}; }
var areValid = function (constraints, acceptUndefined, values) { function isValid(constraint, acceptUndefined, value) {
if (value === undefined) {
return acceptUndefined || constraint.optional;
}
switch (constraint.type) {
case 'boolean':
return isValidBoolean(value);
case 'number':
return isValidNumber(constraint, value);
case 'string':
return isValidString(constraint, value);
}
Logger.tag('validation').error('No validation method for constraint type: {}', constraint.type);
return false;
}
function areValid(constraints, acceptUndefined, values) {
var fields = Object.keys(constraints); var fields = Object.keys(constraints);
for (var i = 0; i < fields.length; i ++) { for (var i = 0; i < fields.length; i ++) {
var field = fields[i]; var field = fields[i];
@ -27,7 +67,7 @@ angular.module('ffffng').factory('Validator', function (_) {
} }
} }
return true; return true;
}; }
return { return {
forConstraint: function (constraint, acceptUndefined) { forConstraint: function (constraint, acceptUndefined) {

View file

@ -46,5 +46,22 @@ angular.module('ffffng').constant('Constraints', {
type: 'boolean', type: 'boolean',
optional: false optional: false
} }
},
rest: {
list: {
_page: {
type: 'number',
min: 1,
optional: true,
default: 1
},
_perPage: {
type: 'number',
min: 1,
max: 50,
optional: true,
default: 20
}
}
} }
}); });