From 7ca173de3de046501d79164da0c8c8871a03089b Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sat, 5 Mar 2016 16:47:32 +0100 Subject: web: o Adding an API method to get per-hour aggregate values. Doesn't use the by_hour table yet. o Adding a simple line graph component that can graph a single property's value. --- bower.json | 3 +- database.json | 2 +- package.json | 1 + src/DillerDao.js | 46 ++++++- src/DillerTx.js | 2 +- src/types.js | 1 + src/web/DillerWeb.js | 38 +++++- web/static/app/DillerRpc.js | 40 +++++- web/static/app/app.js | 179 ------------------------- web/static/app/diller/client.js | 72 ++++++++++ web/static/app/diller/global.js | 60 +++++++++ web/static/app/diller/line-chart.js | 62 +++++++++ web/static/app/diller/web.js | 236 +++++++++++++++++++++++++++++++++ web/static/app/templates/property.html | 30 +++-- web/templates/index.jade | 9 +- web/templates/wat.html | 2 +- 16 files changed, 573 insertions(+), 210 deletions(-) delete mode 100644 web/static/app/app.js create mode 100644 web/static/app/diller/client.js create mode 100644 web/static/app/diller/global.js create mode 100644 web/static/app/diller/line-chart.js create mode 100644 web/static/app/diller/web.js diff --git a/bower.json b/bower.json index 60b30ff..7a91f98 100644 --- a/bower.json +++ b/bower.json @@ -19,6 +19,7 @@ "angular-route": "~1.4.7", "bootstrap": "4.0.0-alpha", "angular-bootstrap": "~0.14.3", - "font-awesome": "fontawesome#~4.4.0" + "font-awesome": "fontawesome#~4.4.0", + "chartist": "^0.9.7" } } diff --git a/database.json b/database.json index b37dd20..0bdd83c 100644 --- a/database.json +++ b/database.json @@ -7,4 +7,4 @@ "database": "diller" }, "sql-file": true -} \ No newline at end of file +} diff --git a/package.json b/package.json index 2c082af..95e23ce 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "express": "^4.13.3", "jade": "^1.11.0", "lodash": "^3.10.1", + "moment": "^2.11.2", "mqtt": "^1.4.3", "pg": "^4.4.2", "pg-promise": "^2.0.12" diff --git a/src/DillerDao.js b/src/DillerDao.js index cf4c75b..7620541 100644 --- a/src/DillerDao.js +++ b/src/DillerDao.js @@ -4,7 +4,7 @@ var _ = require('lodash'); * @param tx {PgTx} * @class */ -function DillerDao(tx) { +function DillerDao(tx, as) { var deviceColumns = ['id', 'created_timestamp', 'key', 'name', 'description']; var deviceStatusColumns = ['device', 'online', 'timestamp', 'host']; @@ -176,8 +176,47 @@ function DillerDao(tx) { // ------------------------------------------------------------------------------------------------------------------- function valuesByPropertyId(propertyId, limit) { - limit = limit || 10; - return tx.manyOrNone('SELECT timestamp, coalesce(value_numeric::text, value_text) AS value FROM value WHERE property=$1 ORDER BY timestamp DESC LIMIT $2', [propertyId, limit]); + var sql = 'SELECT timestamp, coalesce(value_numeric::text, value_text) AS value FROM value WHERE property=$1'; + var args = [propertyId]; + + sql += ' ORDER BY timestamp DESC'; + + if (limit) { + args.push(limit); + sql += ' LIMIT $' + args.length; + } + + return tx.manyOrNone(sql, args); + } + + function aggregateValuesByPropertyId(propertyId, level, from, to) { + var sql, args = [level, as.date(from), as.date(to), propertyId]; + + if (level == 'hour') { + // TODO: use correct table instead of querying raw table + } else { + throw 'Unsupported level: ' + level; + } + + sql = 'with g as (select * from generate_series($2::timestamp, $3::timestamp, (\'1 \' || $1)::interval) as times(ts)),\n' + + 'v as (select\n' + + ' date_trunc($1, timestamp) as ts,\n' + + ' count(timestamp)::real as count,\n' + + ' min(value_numeric)::real as min,\n' + + ' max(value_numeric)::real as max,\n' + + ' avg(value_numeric)::real as avg\n' + + 'FROM value\n' + + 'WHERE timestamp >= $2::timestamp\n' + + ' AND timestamp < $3::timestamp\n' + + ' AND property=$4\n' + + ' AND value_numeric is not null\n' + + ' GROUP BY date_trunc($1, timestamp)\n' + + ')\n' + + 'select g.ts as timestamp, v.count, v.min, v.max, v.avg\n' + + 'from g left outer join v on g.ts = v.ts\n' + + 'order by 1'; + + return tx.manyOrNone(sql, args); } function insertValue(propertyId, timestamp, value) { @@ -234,6 +273,7 @@ function DillerDao(tx) { updateProperty: updateProperty, valuesByPropertyId: valuesByPropertyId, + aggregateValuesByPropertyId: aggregateValuesByPropertyId, insertValue: insertValue, updateHourAggregatesForProperty: updateHourAggregatesForProperty, updateMinuteAggregatesForProperty: updateMinuteAggregatesForProperty diff --git a/src/DillerTx.js b/src/DillerTx.js index 0f6b2ea..f1caa09 100644 --- a/src/DillerTx.js +++ b/src/DillerTx.js @@ -46,7 +46,7 @@ function _DillerTx(config) { var con = pgp(config.postgresqlConfig); return con.tx(function (pg) { - var dao = new _DillerDao(con); + var dao = new _DillerDao(con, pgp.as); var diller = new _Diller(config, con, dao); return action(pg, dao, diller) }); diff --git a/src/types.js b/src/types.js index 8c6300c..9856036 100644 --- a/src/types.js +++ b/src/types.js @@ -74,6 +74,7 @@ var HttpReq = {}; HttpReq.prototype.body = {}; HttpReq.prototype.headers = {}; HttpReq.prototype.params = {}; +HttpReq.prototype.query = {}; var HttpRes = {}; /** diff --git a/src/web/DillerWeb.js b/src/web/DillerWeb.js index 396c2f8..b42e47e 100644 --- a/src/web/DillerWeb.js +++ b/src/web/DillerWeb.js @@ -1,8 +1,11 @@ var express = require('express'); +var moment = require('moment'); var bodyParser = require('body-parser'); var _ = require('lodash'); var di = require('di'); +var isoFormat = 'YYYY-MM-DDTHH:mm:ss'; + /** * @param {DillerConfig} config * @param {DillerMqttClient} mqttClient @@ -105,14 +108,33 @@ function DillerWeb(config, mqttClient, tx) { * @param {HttpRes} res */ function getValues(req, res) { + var from = req.query.from && moment(req.query.from, isoFormat); + var to = req.query.to && moment(req.query.to, isoFormat); + var limit = req.query.limit || 10; + tx(function (tx, dao) { - var propertyId = req.params.propertyId; - return dao.valuesByPropertyId(propertyId, 10); + var propertyId = Number(req.params.propertyId); + + if (from || to) { + from = (from || moment()).startOf('hour'); + to = (to || moment()).startOf('hour'); + + if (typeof propertyId !== 'number' || !from.isValid() || !to.isValid()) { + log.info('getValues: Invalid parameters: propertyId', propertyId, 'from', from.toISOString(), 'to', to.toISOString()); + return Promise.reject('Invalid parameters: ' + typeof propertyId); + } else { + log.info('getValues: propertyId', propertyId, 'from', from.toISOString(), 'to', to.toISOString()); + + return dao.aggregateValuesByPropertyId(propertyId, 'hour', from.toDate(), to.toDate()); + } + } else { + return dao.valuesByPropertyId(propertyId, limit); + } }).then(function (values) { res.json({values: values}); }, function (err) { log.warn('fail', err); - res.status(500).json({message: 'fail'}); + res.status(500).json({message: typeof err === 'string' ? err : 'fail'}); }); } @@ -254,7 +276,7 @@ function DillerWeb(config, mqttClient, tx) { s += _.map(calls, function (call) { - var s = ' function ' + call.name + '(' + call.keys.join(', ') + ') {\n' + + var s = ' function ' + call.name + 'Req(' + call.keys.join(', ') + ') {\n' + ' var req = {};\n' + ' req.method = \'' + call.method + '\';\n' + ' req.url = baseUrl + \'/api' + call.path + '\';\n'; @@ -268,6 +290,12 @@ function DillerWeb(config, mqttClient, tx) { } s += + ' return req;\n' + + ' }\n' + + '\n'; + + s += ' function ' + call.name + '(' + call.keys.join(', ') + ') {\n' + + ' var req = ' + call.name + 'Req(' + call.keys.join(', ') + ');\n' + ' return $http(req);\n' + ' }\n'; @@ -276,7 +304,7 @@ function DillerWeb(config, mqttClient, tx) { s += '\n'; s += ' return {\n'; s += _.map(calls, function (call) { - return ' ' + call.name + ': ' + call.name + return ' ' + call.name + 'Req: ' + call.name + 'Req,\n ' + call.name + ': ' + call.name }).join(',\n'); s += '\n'; s += ' };\n'; diff --git a/web/static/app/DillerRpc.js b/web/static/app/DillerRpc.js index f865dc5..eddbbf9 100644 --- a/web/static/app/DillerRpc.js +++ b/web/static/app/DillerRpc.js @@ -1,52 +1,82 @@ function DillerRpc($http, DillerConfig) { var baseUrl = DillerConfig.baseUrl; - function getDevices() { + function getDevicesReq() { var req = {}; req.method = 'get'; req.url = baseUrl + '/api/device'; + return req; + } + + function getDevices() { + var req = getDevicesReq(); return $http(req); } - function getDevice(deviceId) { + function getDeviceReq(deviceId) { var req = {}; req.method = 'get'; req.url = baseUrl + '/api/device/:deviceId'; req.url = req.url.replace(/:deviceId/, deviceId); + return req; + } + + function getDevice(deviceId) { + var req = getDeviceReq(deviceId); return $http(req); } - function patchDevice(deviceId, payload) { + function patchDeviceReq(deviceId, payload) { var req = {}; req.method = 'patch'; req.url = baseUrl + '/api/device/:deviceId'; req.url = req.url.replace(/:deviceId/, deviceId); req.data = payload; + return req; + } + + function patchDevice(deviceId, payload) { + var req = patchDeviceReq(deviceId, payload); return $http(req); } - function getValues(propertyId) { + function getValuesReq(propertyId) { var req = {}; req.method = 'get'; req.url = baseUrl + '/api/property/:propertyId/values'; req.url = req.url.replace(/:propertyId/, propertyId); + return req; + } + + function getValues(propertyId) { + var req = getValuesReq(propertyId); return $http(req); } - function patchProperty(propertyId, payload) { + function patchPropertyReq(propertyId, payload) { var req = {}; req.method = 'patch'; req.url = baseUrl + '/api/property/:propertyId/values'; req.url = req.url.replace(/:propertyId/, propertyId); req.data = payload; + return req; + } + + function patchProperty(propertyId, payload) { + var req = patchPropertyReq(propertyId, payload); return $http(req); } return { + getDevicesReq: getDevicesReq, getDevices: getDevices, + getDeviceReq: getDeviceReq, getDevice: getDevice, + patchDeviceReq: patchDeviceReq, patchDevice: patchDevice, + getValuesReq: getValuesReq, getValues: getValues, + patchPropertyReq: patchPropertyReq, patchProperty: patchProperty }; } diff --git a/web/static/app/app.js b/web/static/app/app.js deleted file mode 100644 index 22c7f83..0000000 --- a/web/static/app/app.js +++ /dev/null @@ -1,179 +0,0 @@ -(function () { - function FrontPageController(devices) { - var ctrl = this; - - ctrl.devices = devices.data.devices; - } - - function DeviceController($uibModal, device, DillerRpc) { - var ctrl = this; - - ctrl.device = device.data.device; - - ctrl.propertyChunks = _(ctrl.device.properties).sortByAll(['name', 'key']).chunk(3).value(); - - ctrl.editDeviceAttribute = function (attributeName) { - var outer = ctrl; - $uibModal.open({ - controller: function ($uibModalInstance) { - var ctrl = this; - - ctrl.title = 'Edit device ' + attributeName; - ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); - ctrl.value = outer.device[attributeName]; - - ctrl.update = function () { - DillerRpc.patchDevice(outer.device.id, {attribute: attributeName, value: ctrl.value}) - .then(function (res) { - outer.device = res.data.device; - $uibModalInstance.close({}); - }, function (res) { - ctrl.error = res.data.message; - }); - }; - }, - controllerAs: 'ctrl', - bindToController: true, - templateUrl: 'app/templates/edit-attribute.modal.html' - }); - } - } - - function PropertyController($timeout, $route, $uibModal, DillerRpc, device, values) { - var ctrl = this; - - function updateData(device) { - ctrl.device = device.data.device; - ctrl.property = _.find(ctrl.device.properties, {id: $route.current.params.propertyId}); - } - updateData(device); - ctrl.values = values.data.values; - - var refreshPromise; - ctrl.refresh = function () { - $timeout.cancel(refreshPromise); - refreshPromise = $timeout(function () { - ctrl.loading = true; - }, 200); - - DillerRpc.getValues($route.current.params.propertyId).then(function (res) { - ctrl.values = res.data.values; - ctrl.loading = false; - $timeout.cancel(refreshPromise); - }) - }; - - ctrl.editPropertyAttribute = function (attributeName) { - var outer = ctrl; - $uibModal.open({ - controller: function ($uibModalInstance) { - var ctrl = this; - - ctrl.title = 'Edit property ' + attributeName; - ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); - ctrl.value = outer.property[attributeName]; - - ctrl.update = function () { - DillerRpc.patchProperty(outer.property.id, {attribute: attributeName, value: ctrl.value}) - .then(function (res) { - updateData(res); - $uibModalInstance.close({}); - }, function (res) { - ctrl.error = res.data.message; - }); - }; - }, - controllerAs: 'ctrl', - bindToController: true, - templateUrl: 'app/templates/edit-attribute.modal.html' - }); - } - } - - function TimestampFilter() { - return function (value) { - if (!value) { - return; - } - - return moment(value).startOf('second').fromNow(); - } - } - - function DlTimestampDirective() { - return { - restrict: 'E', - scope: { - value: '=' - }, - replace: true, - template: '{{value|timestamp}}' - }; - } - - function DlDotsDirective() { - return { - restrict: 'E', - scope: { - value: '=' - }, - replace: true, - template: '...\n' - }; - } - - function config($routeProvider) { - $routeProvider - .when('/', { - controller: FrontPageController, - controllerAs: 'ctrl', - templateUrl: 'app/templates/front-page.html', - resolve: { - devices: DillerRpcResolve.getDevices - } - }) - .when('/device/:deviceId', { - controller: DeviceController, - controllerAs: 'ctrl', - templateUrl: 'app/templates/device.html', - resolve: { - device: DillerRpcResolve.getDevice - } - }) - .when('/device/:deviceId/property/:propertyId', { - controller: PropertyController, - controllerAs: 'ctrl', - templateUrl: 'app/templates/property.html', - resolve: { - device: DillerRpcResolve.getDevice, - values: DillerRpcResolve.getValues - } - }) - .otherwise({ - redirectTo: '/' - }); - } - - function run($log) { - window.console = $log; - } - - function DillerConfig() { - var head = document.getElementsByTagName('head')[0]; - var base = head.getElementsByTagName('base')[0]; - var baseUrl = base.href.replace(/\/$/, ''); - return { - baseUrl: baseUrl - }; - } - - angular - .module('Diller', ['ngRoute', 'ui.bootstrap']) - .config(config) - .run(run) - .filter('timestamp', TimestampFilter) - .directive('dlTimestamp', DlTimestampDirective) - .directive('dlDots', DlDotsDirective) - .service('DillerConfig', DillerConfig) - .service('DillerRpc', DillerRpc); -})(); diff --git a/web/static/app/diller/client.js b/web/static/app/diller/client.js new file mode 100644 index 0000000..6641ffb --- /dev/null +++ b/web/static/app/diller/client.js @@ -0,0 +1,72 @@ +(function () { + function extractData(res) { + return res && res.data; + } + + function Property($http, DillerRpc, propertyId) { + function getInterval(interval) { + + // moment().subtract(24, 'hour') + var req = DillerRpc.getValuesReq(propertyId); + req.params = { + from: interval.getFrom().toISOString() + }; + return $http(req).then(extractData); + } + + /** @lends Property.prototype */ + return { + getInterval: getInterval + }; + } + + function Device($http, DillerRpc, deviceId) { + var properties = {}; + + /** + * @param propertyId + * @returns {Property} + */ + function getProperty(propertyId) { + var p = properties[propertyId]; + + if (!p) { + p = new Property($http, DillerRpc, propertyId); + properties[propertyId] = p; + } + + return p; + } + + /** @lends Device.prototype */ + return { + getProperty: getProperty + }; + } + + function DillerClient($timeout, $http, DillerRpc) { + + var devices = {}; + + function getDevice(deviceId) { + var d = devices[deviceId]; + + if (!d) { + d = new Device($http, DillerRpc, deviceId); + devices[deviceId] = d; + } + + return d; + } + + /** @lends DillerClient.prototype */ + return { + getDevice: getDevice + }; + } + + angular + .module('diller.client', []) + .service('DillerClient', DillerClient) + .service('DillerRpc', window.DillerRpc); +})(); diff --git a/web/static/app/diller/global.js b/web/static/app/diller/global.js new file mode 100644 index 0000000..0da8da7 --- /dev/null +++ b/web/static/app/diller/global.js @@ -0,0 +1,60 @@ +(function () { + var Diller = window.Diller = window.Diller || {}; + + function toDate(unknown) { + if (!unknown) { + return undefined; + } else if (moment.isMoment(unknown)) { + return unknown; + } else if (typeof unknown === 'string') { + unknown = unknown.trim(); + + if (unknown == '') { + return moment(); + } + + return moment(unknown, 'YYYY-MM-DDTHH:mm.sssZ'); + } else if (typeof unknown === 'string' || moment.isDate(unknown)) { + return moment(unknown); + } else { + return undefined; + } + } + + Diller.Interval = function (from, to) { + var f = toDate(from), + t = toDate(to); + + if (f.isAfter(t)) { + var tmp = f; + f = t; + t = tmp; + } + + return { + getFrom: function () { + return f || moment(); + }, + getTo: function () { + return t || moment(); + }, + toString: function () { + return 'yo' + } + }; + }; + + Diller.Interval.create = function (value) { + if (value instanceof Diller.Interval) { + return value; + } + + return new Diller.Interval.hours(24); + }; + + Diller.Interval.hours = function (hours) { + var to = moment(); + var from = to.subtract(hours, 'hours'); + return new Diller.Interval(from, to); + }; +})(); diff --git a/web/static/app/diller/line-chart.js b/web/static/app/diller/line-chart.js new file mode 100644 index 0000000..69b6f77 --- /dev/null +++ b/web/static/app/diller/line-chart.js @@ -0,0 +1,62 @@ +(function () { + + var isoFormat = 'YYYY-MM-DDTHH:mm:ss'; + + function DlLineChartDirective($timeout, DillerClient) { + var id_seq = 0; + + return { + restrict: 'E', + scope: { + device: '=', + property: '=', + value: '=', + interval: '=' + }, + replace: true, + template: '
', + link: function (scope, element, attrs) { + var elementId = element.attr('id'); + if (!elementId) { + elementId = 'dl-line-chart-' + id_seq++; + element.attr('id', elementId); + } + + var deviceId = scope.device; + var propertyId = scope.property; + var interval = Diller.Interval.create(scope.interval); + var property = DillerClient.getDevice(deviceId).getProperty(propertyId); + + var options = { + axisX: { + showLabel: true, + showGrid: true, + labelInterpolationFnc: function (value, index) { + return index % 4 === 0 ? value.format('HH:mm') : null; + } + } + }; + + property.getInterval(interval).then(function (data) { + var avgs = _.pluck(data.values, 'avg'); + var timestamps = _.map(data.values, function (row) { + return moment(row.timestamp, isoFormat); + }); + + var chartData = { + labels: timestamps, + series: [ + avgs + ] + }; + + var chart = new Chartist.Line('#' + elementId, chartData, options); + }); + } + }; + } + + angular + .module('diller.line-chart', ['diller.client']) + .directive('dlLineChart', DlLineChartDirective); +})(); diff --git a/web/static/app/diller/web.js b/web/static/app/diller/web.js new file mode 100644 index 0000000..4b173ad --- /dev/null +++ b/web/static/app/diller/web.js @@ -0,0 +1,236 @@ +(function () { + var isoFormat = 'YYYY-MM-DDTHH:mm:ss'; + + function FrontPageController(devices) { + var ctrl = this; + + ctrl.devices = devices.data.devices; + } + + function DeviceController($uibModal, device, DillerRpc) { + var ctrl = this; + + ctrl.device = device.data.device; + + ctrl.propertyChunks = _(ctrl.device.properties).sortByAll(['name', 'key']).chunk(3).value(); + + ctrl.editDeviceAttribute = function (attributeName) { + var outer = ctrl; + $uibModal.open({ + controller: function ($uibModalInstance) { + var ctrl = this; + + ctrl.title = 'Edit device ' + attributeName; + ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); + ctrl.value = outer.device[attributeName]; + + ctrl.update = function () { + DillerRpc.patchDevice(outer.device.id, {attribute: attributeName, value: ctrl.value}) + .then(function (res) { + outer.device = res.data.device; + $uibModalInstance.close({}); + }, function (res) { + ctrl.error = res.data.message; + }); + }; + }, + controllerAs: 'ctrl', + bindToController: true, + templateUrl: 'app/templates/edit-attribute.modal.html' + }); + } + } + + function PropertyController($timeout, $route, $uibModal, DillerRpc, device, values) { + var ctrl = this; + + function updateData(device) { + ctrl.device = device.data.device; + ctrl.property = _.find(ctrl.device.properties, {id: $route.current.params.propertyId}); + } + updateData(device); + ctrl.values = values.data.values; + + var refreshPromise; + ctrl.refresh = function () { + $timeout.cancel(refreshPromise); + refreshPromise = $timeout(function () { + ctrl.loading = true; + }, 200); + + DillerRpc.getValues($route.current.params.propertyId).then(function (res) { + ctrl.values = res.data.values; + ctrl.loading = false; + $timeout.cancel(refreshPromise); + }) + }; + + ctrl.editPropertyAttribute = function (attributeName) { + var outer = ctrl; + $uibModal.open({ + controller: function ($uibModalInstance) { + var ctrl = this; + + ctrl.title = 'Edit property ' + attributeName; + ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); + ctrl.value = outer.property[attributeName]; + + ctrl.update = function () { + DillerRpc.patchProperty(outer.property.id, {attribute: attributeName, value: ctrl.value}) + .then(function (res) { + updateData(res); + $uibModalInstance.close({}); + }, function (res) { + ctrl.error = res.data.message; + }); + }; + }, + controllerAs: 'ctrl', + bindToController: true, + templateUrl: 'app/templates/edit-attribute.modal.html' + }); + }; + + ctrl.interval = Diller.Interval.hours(5); + + // ctrl.values24h = values24h.data.values; + + function chartist() { + var avgs = _.pluck(ctrl.values24h, 'avg'); + var timestamps = _.map(ctrl.values24h, function(row) { + var m = moment(row.timestamp, isoFormat); + return m.format('HH:mm'); + }); + + var data = { + labels: timestamps, + series: [ + avgs + ] + }; + + var options = { + axisX: { + showLabel: false, + showGrid: false + } + }; + +// options.lineSmooth = Chartist.Interpolation.cardinal({ +// fillHoles: true, +// }) + + options.axisX = { + showLabel: true, + showGrid: true, + labelInterpolationFnc: function(value, index) { + return index % 4 === 0 ? value : null; + } + }; + + var chartData = { + series: [[]], + labels: [] + }; + + // var chart = new Chartist.Line('#values-chart', chartData, options); + // console.log('chart', chart); +/* + $timeout(function() { + chartData = data; + // console.log(data); + var chart = new Chartist.Line('#values-chart', chartData, options); + console.log('chart', chart); + }); +*/ + } + + // chartist(); + } + + function TimestampFilter() { + return function (value) { + if (!value) { + return; + } + + return moment(value).startOf('second').fromNow(); + } + } + + function DlTimestampDirective() { + return { + restrict: 'E', + scope: { + value: '=' + }, + replace: true, + template: '{{value|timestamp}}' + }; + } + + function DlDotsDirective() { + return { + restrict: 'E', + scope: { + value: '=' + }, + replace: true, + template: '...\n' + }; + } + + function config($routeProvider) { + $routeProvider + .when('/', { + controller: FrontPageController, + controllerAs: 'ctrl', + templateUrl: 'app/templates/front-page.html', + resolve: { + devices: DillerRpcResolve.getDevices + } + }) + .when('/device/:deviceId', { + controller: DeviceController, + controllerAs: 'ctrl', + templateUrl: 'app/templates/device.html', + resolve: { + device: DillerRpcResolve.getDevice + } + }) + .when('/device/:deviceId/property/:propertyId', { + controller: PropertyController, + controllerAs: 'ctrl', + templateUrl: 'app/templates/property.html', + resolve: { + device: DillerRpcResolve.getDevice, + values: DillerRpcResolve.getValues + } + }) + .otherwise({ + redirectTo: '/' + }); + } + + function run($log) { + window.console = $log; + } + + function DillerConfig() { + var head = document.getElementsByTagName('head')[0]; + var base = head.getElementsByTagName('base')[0]; + var baseUrl = base.href.replace(/\/$/, ''); + return { + baseUrl: baseUrl + }; + } + + angular + .module('diller.web', ['ngRoute', 'ui.bootstrap', 'diller.line-chart']) + .config(config) + .run(run) + .filter('timestamp', TimestampFilter) + .directive('dlTimestamp', DlTimestampDirective) + .directive('dlDots', DlDotsDirective) + .service('DillerConfig', DillerConfig); +})(); diff --git a/web/static/app/templates/property.html b/web/static/app/templates/property.html index 969734b..3f3424f 100644 --- a/web/static/app/templates/property.html +++ b/web/static/app/templates/property.html @@ -20,20 +20,25 @@

-
-
Key
-
- {{ctrl.property.key}} -   -
+
+
+
Key
+
+ {{ctrl.property.key}} +   +
-
Created
-
- {{ctrl.property.created_timestamp | date:'medium'}} -   -
-
+
Created
+
+ {{ctrl.property.created_timestamp | date:'medium'}} +   +
+
+
+ + + diff --git a/web/templates/index.jade b/web/templates/index.jade index 1b2adbc..84c31dc 100644 --- a/web/templates/index.jade +++ b/web/templates/index.jade @@ -17,14 +17,19 @@ html(lang='en') 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") + script(src="bower_components/chartist/dist/chartist.js", type="application/javascript") script(src="app/DillerRpc.js", type="application/javascript") - script(src="app/app.js", type="application/javascript") + script(src="app/diller/global.js", type="application/javascript") + script(src="app/diller/client.js", type="application/javascript") + script(src="app/diller/web.js", type="application/javascript") + script(src="app/diller/line-chart.js", type="application/javascript") link(href="app/app.css", rel="stylesheet") link(rel="stylesheet", href="bower_components/font-awesome/css/font-awesome.min.css") + link(rel="stylesheet", href="bower_components/chartist/dist/chartist.css") - body(ng-app="Diller") + body(ng-app="diller.web") .container nav.navbar.navbar-dark.bg-inverse a.navbar-brand(href='#/') Diller diff --git a/web/templates/wat.html b/web/templates/wat.html index fd504a7..0e77077 100644 --- a/web/templates/wat.html +++ b/web/templates/wat.html @@ -17,7 +17,7 @@ - + Loading Diller ... -- cgit v1.2.3