Extended node management in amdin panel.
This commit is contained in:
parent
d5c69fa78f
commit
0f1a21c905
|
@ -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>
|
||||||
|
|
|
@ -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>'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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> <span class="hidden-xs">{{ label }}</span>' +
|
'<span class="fa fa-{{ icon }}" aria-hidden="true"></span> <span class="hidden-xs">{{ label }}</span>' +
|
||||||
'</button>'
|
'</button>'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue