diff --git a/admin/index.html b/admin/index.html index da1456f..24e0964 100644 --- a/admin/index.html +++ b/admin/index.html @@ -13,6 +13,18 @@ color: lightcoral; text-decoration: line-through; } + + .monitoring-active { + color: green; + } + + .monitoring-confirmation-pending { + color: lightcoral; + } + + .monitoring-disabled { + color: grey; + } @@ -20,7 +32,9 @@ + + + - diff --git a/admin/js/app.js b/admin/js/app.js new file mode 100644 index 0000000..ea9f552 --- /dev/null +++ b/admin/js/app.js @@ -0,0 +1,4 @@ +'use strict'; + +angular.module('ffffng', []); +angular.module('ffffngAdmin', ['ng-admin', 'ffffng']); diff --git a/admin/js/config.js b/admin/js/config.js index f285c32..4a1fd7f 100644 --- a/admin/js/config.js +++ b/admin/js/config.js @@ -1,12 +1,21 @@ 'use strict'; -var myApp = angular.module('ffffngAdmin', ['ng-admin']); - -myApp.config(['NgAdminConfigurationProvider', function(NgAdminConfigurationProvider) { +angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Constraints) { function formatMoment(unix) { return unix ? moment.unix(unix).fromNow() : 'N/A'; } + function nodeConstraint(field) { + var constraint = Constraints.node[field]; + var result = { + required: !constraint.optional + }; + if (constraint.type === 'string') { + result.pattern = constraint.regex; + } + return result; + } + var nga = NgAdminConfigurationProvider; var admin = nga.application('Knotenverwaltung - Admin-Panel'); @@ -14,6 +23,64 @@ myApp.config(['NgAdminConfigurationProvider', function(NgAdminConfigurationProvi .baseApiUrl('/internal/api/') .debug(true); + function nodeClasses(node) { + if (!node) { + return; + } + return; + } + + var nodes = nga.entity('nodes').label('Nodes').identifier(nga.field('token')); + nodes + .listView() + .title('Nodes') + .actions([]) + .batchActions([]) + .exportFields([]) + .fields([ + nga.field('token').cssClasses(nodeClasses), + nga.field('hostname').cssClasses(nodeClasses), + nga.field('monitoring').cssClasses(nodeClasses).template(function (node) { + if (!node.values.monitoring) { + return ''; + } + return node.values.monitoringConfirmed + ? '' + : ''; + }) + ]) + .listActions([ + 'edit', + 'delete' + ]) + ; + + nodes + .editionView() + .title('Edit node') + .actions(['list', 'delete']) + .addField(nga.field('token').editable(false)) + .addField(nga.field('hostname').label('Name').validation(nodeConstraint('hostname'))) + .addField(nga.field('key').label('Key').validation(nodeConstraint('key'))) + .addField(nga.field('mac').label('MAC').validation(nodeConstraint('mac'))) + .addField(nga.field('coords').label('GPS').validation(nodeConstraint('coords'))) + .addField(nga.field('nickname').validation(nodeConstraint('nickname'))) + .addField(nga.field('email').validation(nodeConstraint('email'))) + .addField(nga.field('monitoring', 'boolean').validation(nodeConstraint('monitoring'))) + .addField(nga.field('monitoringConfirmed').label('Monitoring confirmation').editable(false).map( + function (monitoringConfirmed, node) { + if (!node.monitoring) { + return 'N/A'; + } + + return monitoringConfirmed ? 'confirmed' : 'pending'; + } + )) + ; + + admin.addEntity(nodes); + function taskClasses(task) { if (!task) { return; @@ -40,13 +107,17 @@ myApp.config(['NgAdminConfigurationProvider', function(NgAdminConfigurationProvi ' ' + ' ' + '' - ) + ) ; admin.addEntity(tasks); admin.menu( nga.menu() + .addChild(nga + .menu(nodes) + .icon('') + ) .addChild(nga .menu(tasks) .icon('') @@ -62,4 +133,4 @@ myApp.config(['NgAdminConfigurationProvider', function(NgAdminConfigurationProvi ); nga.configure(admin); -}]); +}); diff --git a/admin/js/validation b/admin/js/validation new file mode 120000 index 0000000..57b9a2d --- /dev/null +++ b/admin/js/validation @@ -0,0 +1 @@ +../../shared/validation \ No newline at end of file diff --git a/admin/js/taskActionButton.js b/admin/js/views/taskActionButton.js similarity index 100% rename from admin/js/taskActionButton.js rename to admin/js/views/taskActionButton.js diff --git a/server/resources/nodeResource.js b/server/resources/nodeResource.js index 3d3b545..473c962 100644 --- a/server/resources/nodeResource.js +++ b/server/resources/nodeResource.js @@ -92,6 +92,18 @@ angular.module('ffffng').factory('NodeResource', function ( } return Resources.success(res, node); }); + }, + + getAll: function (req, res) { + // TODO: Paging + Sort + Filter + + return NodeService.getAllNodes(function (err, nodes) { + if (err) { + return Resources.error(res, err); + } + + return Resources.success(res, nodes); + }); } }; }); diff --git a/server/router.js b/server/router.js index 497f7db..254b93f 100644 --- a/server/router.js +++ b/server/router.js @@ -20,6 +20,11 @@ angular.module('ffffng').factory('Router', function ( app.put('/internal/api/tasks/run/:id', TaskResource.run); app.put('/internal/api/tasks/enable/:id', TaskResource.enable); app.put('/internal/api/tasks/disable/:id', TaskResource.disable); + + app.put('/internal/api/nodes/:token', NodeResource.update); + app.delete('/internal/api/nodes/:token', NodeResource.delete); + app.get('/internal/api/nodes', NodeResource.getAll); + app.get('/internal/api/nodes/:token', NodeResource.get); } }; }); diff --git a/server/services/nodeService.js b/server/services/nodeService.js index 3cc3555..e3b99f6 100644 --- a/server/services/nodeService.js +++ b/server/services/nodeService.js @@ -4,6 +4,7 @@ angular.module('ffffng') .service('NodeService', function ( config, _, + async, crypto, fs, glob, @@ -13,6 +14,8 @@ angular.module('ffffng') ErrorTypes, UrlBuilder ) { + var MAX_PARALLEL_NODES_PARSING = 10; + var linePrefixes = { hostname: '# Knotenname: ', nickname: '# Ansprechpartner: ', @@ -366,6 +369,24 @@ angular.module('ffffng') deleteNodeFile(token, callback); }, + getAllNodes: function (callback) { + var files = findNodeFiles({}); + + async.mapLimit( + files, + MAX_PARALLEL_NODES_PARSING, + parseNodeFile, + function (err, nodes) { + if (err) { + Logger.tag('nodes').error('Error getting all nodes:', error); + return callback({data: 'Internal error.', type: ErrorTypes.internalError}); + } + + return callback(null, nodes); + } + ); + }, + findNodeDataByMac: function (mac, callback) { return findNodeDataByFilePattern({ mac: mac }, callback); }, diff --git a/server/validation/validator.js b/server/validation/validator.js index ad814b6..0bc2e2f 100644 --- a/server/validation/validator.js +++ b/server/validation/validator.js @@ -6,6 +6,10 @@ angular.module('ffffng').factory('Validator', function (_) { return acceptUndefined || constraint.optional; } + if (constraint.type === 'boolean') { + return _.isBoolean(value); + } + if (!_.isString(value)) { return false; } diff --git a/shared/validation/constraints.js b/shared/validation/constraints.js index 3299073..ab16918 100644 --- a/shared/validation/constraints.js +++ b/shared/validation/constraints.js @@ -1,40 +1,50 @@ 'use strict'; -angular.module('ffffng').factory('Constraints', function () { - return { - id:{ - regex: /^[1-9][0-9]*/, +angular.module('ffffng').constant('Constraints', { + id:{ + type: 'string', + regex: /^[1-9][0-9]*/, + optional: false + }, + token:{ + type: 'string', + regex: /^[0-9a-f]{16}$/i, + optional: false + }, + node: { + hostname: { + type: 'string', + regex: /^[-a-z0-9_]{1,32}$/i, optional: false }, - token:{ - regex: /^[0-9a-f]{16}$/i, + key: { + type: 'string', + regex: /^([a-f0-9]{64})$/i, + optional: true + }, + email: { + type: 'string', + regex: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, optional: false }, - node: { - hostname: { - regex: /^[-a-z0-9_]{1,32}$/i, - optional: false - }, - key: { - regex: /^([a-f0-9]{64})$/i, - optional: true - }, - email: { - regex: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, - optional: false - }, - nickname: { - regex: /^[-a-z0-9_ äöüß]{1,64}$/i, - optional: false - }, - mac: { - regex: /^([a-f0-9]{12}|([a-f0-9]{2}:){5}[a-f0-9]{2})$/i, - optional: false - }, - coords: { - regex: /^(-?[0-9]{1,3}(\.[0-9]{1,15})? -?[0-9]{1,3}(\.[0-9]{1,15})?)$/, - optional: true - } + nickname: { + type: 'string', + regex: /^[-a-z0-9_ äöüß]{1,64}$/i, + optional: false + }, + mac: { + type: 'string', + regex: /^([a-f0-9]{12}|([a-f0-9]{2}:){5}[a-f0-9]{2})$/i, + optional: false + }, + coords: { + type: 'string', + regex: /^(-?[0-9]{1,3}(\.[0-9]{1,15})? -?[0-9]{1,3}(\.[0-9]{1,15})?)$/, + optional: true + }, + monitoring: { + type: 'boolean', + optional: false } - }; + } });