diff options
-rw-r--r-- | .editorconfig | 9 | ||||
-rw-r--r-- | bower.json | 6 | ||||
-rw-r--r-- | gulpfile.js | 11 | ||||
-rw-r--r-- | src/Diller.js | 23 | ||||
-rw-r--r-- | src/DillerDao.js | 33 | ||||
-rw-r--r-- | src/web/DillerWeb.js | 76 | ||||
-rw-r--r-- | web/static/app/DillerRpc.js | 13 | ||||
-rw-r--r-- | web/static/app/app.js | 29 | ||||
-rw-r--r-- | web/static/app/templates/device-edit-attribute.modal.html | 13 | ||||
-rw-r--r-- | web/static/app/templates/device.html | 8 | ||||
-rw-r--r-- | web/templates/index.jade | 4 |
11 files changed, 199 insertions, 26 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..118d2d6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{js,py,jade,css}] +indent_size = 2 @@ -13,10 +13,12 @@ "tests" ], "dependencies": { + "lodash": "~3.10.1", + "moment": "~2.10.6", "angular": "~1.4.7", "angular-route": "~1.4.7", "bootstrap": "4.0.0-alpha", - "lodash": "~3.10.1", - "moment": "~2.10.6" + "angular-bootstrap": "~0.14.3", + "font-awesome": "fontawesome#~4.4.0" } } diff --git a/gulpfile.js b/gulpfile.js index 724a0b1..1e98a86 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -46,9 +46,7 @@ gulp.task('diller-mqtt', function () { ], env: {NODE_ENV: 'development'}, tasks: ['diller-mqtt-reload'], - stdout: false, - //readable: false, - a: '' + stdout: false }).on('readable', function () { return readable(state, this); }); @@ -58,6 +56,8 @@ gulp.task('diller-web-reload', function () { }); gulp.task('diller-web', ['bower'], function () { + var state = {}; + nodemon({ script: 'diller-web.js', delay: 500, @@ -69,7 +69,10 @@ gulp.task('diller-web', ['bower'], function () { 'src/mqtt/' ], env: {NODE_ENV: 'development'}, - tasks: ['diller-web-reload'] + tasks: ['diller-web-reload'], + stdout: false + }).on('readable', function () { + return readable(state, this); }); }); diff --git a/src/Diller.js b/src/Diller.js index e73bf46..d52e3f1 100644 --- a/src/Diller.js +++ b/src/Diller.js @@ -1,5 +1,11 @@ var di = require('di'); +/** + * @param config DillerConfig + * @param db DillerDb + * @returns {{onMessage: onMessage, updateDeviceName: updateDeviceName}} + * @constructor + */ function Diller(config, db) { var log = config.log(); @@ -51,6 +57,20 @@ function Diller(config, db) { }); } + function updateDeviceName(deviceId, name) { + log.info('Updating device name', {deviceId: deviceId, name: name}); + return db() + .tx(function (tx) { + var dao = new DillerDao(tx); + + return dao.updateDevice(deviceId, {name: name}) + .then(function (res) { + log.info('Device name updated', {deviceId: deviceId, name: name}); + return res; + }); + }) + } + function onMessage(topic, message, payload) { var parts = topic.split(/\//); @@ -118,7 +138,8 @@ function Diller(config, db) { } return { - onMessage: onMessage + onMessage: onMessage, + updateDeviceName: updateDeviceName } } diff --git a/src/DillerDao.js b/src/DillerDao.js index cae80d7..2133dfc 100644 --- a/src/DillerDao.js +++ b/src/DillerDao.js @@ -1,3 +1,5 @@ +var _ = require('lodash'); + function DillerDao(tx) { var deviceColumns = 'id, key, created_timestamp'; @@ -24,6 +26,30 @@ function DillerDao(tx) { return tx.one("INSERT INTO device(id, key, created_timestamp) VALUES(DEFAULT, $1, CURRENT_TIMESTAMP) RETURNING " + deviceColumns, key); } + function updateDevice(id, attributes) { + + var values = [id]; + var i = 2; + var fields = _.map(attributes, function (value, name) { + console.log('name', name, 'value', value, 'i', i); + if (name == 'name') { + values.push(value); + return 'name = $' + i++; + } + }); + + if (fields.length == 0) { + return; // TODO: return an empty promise; + } + + fields = _.collect(fields); + + var x = 'UPDATE device SET ' + fields.join(', ') + ' WHERE id = $1'; + console.log('x', x); + console.log('values', values); + return tx.none(x, values); + } + // ------------------------------------------------------------------------------------------------------------------- // Device Property // ------------------------------------------------------------------------------------------------------------------- @@ -70,10 +96,10 @@ function DillerDao(tx) { function updateHourAggregatesForProperty(propertyId, timestamp) { return tx.none('DELETE FROM value_by_hour WHERE property=$1 AND timestamp=DATE_TRUNC(\'hour\', $2::TIMESTAMPTZ)', [propertyId, timestamp]) - .then(function() { + .then(function () { return tx.oneOrNone('INSERT INTO value_by_hour(property, timestamp, count, max, min, avg) ' + 'SELECT property, DATE_TRUNC(\'hour\', timestamp) AS timestamp, COUNT(value_numeric) AS count, MAX(value_numeric) AS max, MIN(value_numeric) AS min, AVG(value_numeric) AS avg ' + - 'FROM value WHERE property=$1 AND DATE_TRUNC(\'hour\', timestamp)=DATE_TRUNC(\'hour\', $2::TIMESTAMPTZ) AND value_numeric IS NOT NULL ' + + 'FROM value WHERE property=$1 AND DATE_TRUNC(\'hour\', timestamp)=DATE_TRUNC(\'hour\', $2::TIMESTAMPTZ) AND value_numeric IS NOT NULL ' + 'GROUP BY property, DATE_TRUNC(\'hour\', timestamp) ' + 'RETURNING *;', [propertyId, timestamp]); }); @@ -81,7 +107,7 @@ function DillerDao(tx) { function updateMinuteAggregatesForProperty(propertyId, timestamp) { return tx.none('DELETE FROM value_by_minute WHERE property=$1 AND timestamp=DATE_TRUNC(\'minute\', $2::TIMESTAMPTZ)', [propertyId, timestamp]) - .then(function() { + .then(function () { return tx.oneOrNone('INSERT INTO value_by_minute(property, timestamp, count, max, min, avg) ' + 'SELECT property, DATE_TRUNC(\'minute\', timestamp) AS timestamp, COUNT(value_numeric) AS count, MAX(value_numeric) AS max, MIN(value_numeric) AS min, AVG(value_numeric) AS avg ' + 'FROM value ' + @@ -96,6 +122,7 @@ function DillerDao(tx) { deviceById: deviceById, deviceByKey: deviceByKey, insertDevice: insertDevice, + updateDevice: updateDevice, devicePropertyById: devicePropertyById, devicePropertyByDeviceIdAndKey: devicePropertyByDeviceIdAndKey, diff --git a/src/web/DillerWeb.js b/src/web/DillerWeb.js index c261af7..95bcbb6 100644 --- a/src/web/DillerWeb.js +++ b/src/web/DillerWeb.js @@ -14,16 +14,26 @@ function DillerWeb(diller, db, config) { var calls = []; var app; + function genericErrorHandler(res) { + return function (data) { + log.warn('fail', data); + return res.status(500).json({message: 'fail', data: data}) + } + } + function getDevices(req, res) { db().tx(function (pg) { var dao = new DillerDao(pg); return dao.devices(); }).then(function (devices) { res.json({devices: devices}); - }, function (err) { - log.warn('fail', err); - res.status(500).json({message: 'fail'}); - }); + }, genericErrorHandler(res)); + } + + function deviceResponse(data) { + var device = data[0]; + device.properties = data[1]; + return {device: device}; } function getDevice(req, res) { @@ -36,13 +46,36 @@ function DillerWeb(diller, db, config) { dao.devicePropertiesByDeviceId(deviceId)] ); }).then(function (data) { + res.json(deviceResponse(data)); + }, genericErrorHandler(res)); + } + + function patchDevice(req, res) { + db().tx(function (tx) { + var deviceId = req.params.deviceId; + + var body = req.body; + + if (body.attribute == 'name') { + diller.updateDeviceName(deviceId, body.value) + .then(function () { + var dao = new DillerDao(tx); + return tx.batch([ + dao.deviceById(deviceId), + dao.devicePropertiesByDeviceId(deviceId)] + ); + }) + .then(function (data) { + res.json(deviceResponse(data)); + }, genericErrorHandler(res)); + } else { + res.status(400).json({message: 'Required keys: "attribute" and "value".'}); + } + }).then(function (data) { var device = data[0]; device.properties = data[1]; res.json({device: device}); - }, function (err) { - log.warn('fail', err); - res.status(500).json({message: 'fail'}); - }); + }, genericErrorHandler(res)); } function getValues(req, res) { @@ -76,19 +109,34 @@ function DillerWeb(diller, db, config) { api[method](path, callback); var layer = _.last(api.stack); + var methodWithPayloads = { + put: true, + post: true, + patch: true + }; + + var keys = _.map(layer.keys, function (key) { + return key.name; + }); + + var hasPayload = methodWithPayloads[method]; + if (hasPayload) { + keys.push('payload'); + } + calls.push({ name: name, method: method, path: path, layer: layer, - keys: _.map(layer.keys, function (key) { - return key.name; - }) + hasPayload: hasPayload, + keys: keys }); } addApi('getDevices', 'get', '/device', getDevices); addApi('getDevice', 'get', '/device/:deviceId', getDevice); + addApi('patchDevice', 'patch', '/device/:deviceId', patchDevice); addApi('getValues', 'get', '/property/:propertyId/values', getValues); var templates = express.Router(); @@ -116,8 +164,6 @@ function DillerWeb(diller, db, config) { s += _.map(calls, function (call) { - //console.error(call); - //console.error('call.layer', call.layer); var s = ' function ' + call.name + '(' + call.keys.join(', ') + ') {\n' + ' var req = {};\n' + ' req.method = \'' + call.method + '\';\n' + @@ -127,6 +173,10 @@ function DillerWeb(diller, db, config) { return ' req.url = req.url.replace(/:' + key.name + '/, ' + key.name + ');\n' }).join('\n'); + if (call.hasPayload) { + s += ' req.data = payload;\n'; + } + s += ' return $http(req);\n' + ' }\n'; diff --git a/web/static/app/DillerRpc.js b/web/static/app/DillerRpc.js index de90aee..b1939f4 100644 --- a/web/static/app/DillerRpc.js +++ b/web/static/app/DillerRpc.js @@ -16,6 +16,15 @@ function DillerRpc($http, DillerConfig) { return $http(req); } + function patchDevice(deviceId, payload) { + var req = {}; + req.method = 'patch'; + req.url = baseUrl + '/api/device/:deviceId'; + req.url = req.url.replace(/:deviceId/, deviceId); + req.data = payload; + return $http(req); + } + function getValues(propertyId) { var req = {}; req.method = 'get'; @@ -27,6 +36,7 @@ function DillerRpc($http, DillerConfig) { return { getDevices: getDevices, getDevice: getDevice, + patchDevice: patchDevice, getValues: getValues }; } @@ -38,6 +48,9 @@ DillerRpcResolve.getDevices = function(DillerRpc) { DillerRpcResolve.getDevice = function(DillerRpc, $route) { return DillerRpc.getDevice($route.current.params.deviceId); }; +DillerRpcResolve.patchDevice = function(DillerRpc, $route) { + return DillerRpc.patchDevice($route.current.params.deviceId, $route.current.params.payload); +}; DillerRpcResolve.getValues = function(DillerRpc, $route) { return DillerRpc.getValues($route.current.params.propertyId); }; diff --git a/web/static/app/app.js b/web/static/app/app.js index 6fa1f71..134afc8 100644 --- a/web/static/app/app.js +++ b/web/static/app/app.js @@ -5,10 +5,35 @@ ctrl.devices = devices.data.devices; } - function DeviceController(device) { + function DeviceController($uibModal, device, DillerRpc) { var ctrl = this; ctrl.device = device.data.device; + + ctrl.editDeviceAttribute = function (attributeName) { + var outer = ctrl; + $uibModal.open({ + controller: function ($uibModalInstance) { + var ctrl = this; + + ctrl.attributeName = attributeName; + ctrl.value = outer.device[attributeName]; + + ctrl.value = 'yoyo'; + + ctrl.update = function () { + + DillerRpc.patchDevice(outer.device.id, {attribute: attributeName, value: ctrl.value}) + .then(function (res) { + $uibModalInstance.close({}); + }); + }; + }, + controllerAs: 'ctrl', + bindToController: true, + templateUrl: 'app/templates/device-edit-attribute.modal.html' + }); + } } function PropertyController($timeout, $route, DillerRpc, device, values) { @@ -110,7 +135,7 @@ } angular - .module('Diller', ['ngRoute']) + .module('Diller', ['ngRoute', 'ui.bootstrap']) .config(config) .filter('timestamp', TimestampFilter) .directive('dlTimestamp', DlTimestampDirective) diff --git a/web/static/app/templates/device-edit-attribute.modal.html b/web/static/app/templates/device-edit-attribute.modal.html new file mode 100644 index 0000000..e471e5f --- /dev/null +++ b/web/static/app/templates/device-edit-attribute.modal.html @@ -0,0 +1,13 @@ +<div class="modal-header"> + <button type="button" class="close" ng-click="$dismiss()"> + <span>×</span> + </button> + <h4 class="modal-title">Edit device {{attributeName}}</h4> +</div> +<div class="modal-body"> + <p>One fine body…</p> +</div> +<div class="modal-footer"> + <button type="button" class="btn btn-secondary" ng-click="$dismiss()">Cancel</button> + <button type="button" class="btn btn-primary" ng-click="ctrl.update()">Update</button> +</div> diff --git a/web/static/app/templates/device.html b/web/static/app/templates/device.html index b8936f3..d126f1d 100644 --- a/web/static/app/templates/device.html +++ b/web/static/app/templates/device.html @@ -59,7 +59,13 @@ <dd class="col-sm-9">{{ctrl.device.created_timestamp | date}}</dd> <dt class="col-sm-3">Name</dt> - <dd class="col-sm-9"> {{ctrl.device.name}}</dd> + <dd class="col-sm-9"> + {{ctrl.device.name}} + + <a ng-click="ctrl.editDeviceAttribute('name')" class="pull-right"> + <i class="fa fa-edit"/> + </a> + </dd> <dt class="col-sm-3">Description</dt> <dd class="col-sm-9"> {{ctrl.device.description}}</dd> diff --git a/web/templates/index.jade b/web/templates/index.jade index 6dad0f5..1b2adbc 100644 --- a/web/templates/index.jade +++ b/web/templates/index.jade @@ -13,6 +13,8 @@ html(lang='en') link(rel="stylesheet", href="bower_components/bootstrap/dist/css/bootstrap.css") script(src="bower_components/angular/angular.js", type="application/javascript") script(src="bower_components/angular-route/angular-route.js", type="application/javascript") + script(src="bower_components/angular-bootstrap/ui-bootstrap.js", type="application/javascript") + script(src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js", type="application/javascript") script(src="bower_components/lodash/lodash.js", type="application/javascript") script(src="bower_components/moment/moment.js", type="application/javascript") @@ -20,6 +22,8 @@ html(lang='en') script(src="app/app.js", type="application/javascript") link(href="app/app.css", rel="stylesheet") + link(rel="stylesheet", href="bower_components/font-awesome/css/font-awesome.min.css") + body(ng-app="Diller") .container nav.navbar.navbar-dark.bg-inverse |