aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--bower.json3
-rw-r--r--src/DillerDao.js4
-rw-r--r--web/static/app/app.css152
-rw-r--r--web/static/app/app.js56
-rw-r--r--web/static/app/templates/property.html12
-rw-r--r--web/templates/index.jade1
6 files changed, 218 insertions, 10 deletions
diff --git a/bower.json b/bower.json
index 0138bf1..d6219e6 100644
--- a/bower.json
+++ b/bower.json
@@ -16,6 +16,7 @@
"angular": "~1.4.7",
"angular-route": "~1.4.7",
"bootstrap": "4.0.0-alpha",
- "lodash": "~3.10.1"
+ "lodash": "~3.10.1",
+ "moment": "~2.10.6"
}
}
diff --git a/src/DillerDao.js b/src/DillerDao.js
index 98b6e6e..3a6601f 100644
--- a/src/DillerDao.js
+++ b/src/DillerDao.js
@@ -71,7 +71,7 @@ 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() {
- return tx.one('INSERT INTO value_by_hour(property, timestamp, count, max, min, avg) ' +
+ 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 ' +
'GROUP BY property, DATE_TRUNC(\'hour\', timestamp) ' +
@@ -82,7 +82,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() {
- return tx.one('INSERT INTO value_by_minute(property, timestamp, count, max, min, avg) ' +
+ 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 ' +
'WHERE property=$1 AND DATE_TRUNC(\'minute\', timestamp)=DATE_TRUNC(\'minute\', $2::TIMESTAMPTZ) AND value_numeric IS NOT NULL ' +
diff --git a/web/static/app/app.css b/web/static/app/app.css
index 090ac5e..f21c0dc 100644
--- a/web/static/app/app.css
+++ b/web/static/app/app.css
@@ -1,3 +1,153 @@
nav.navbar {
- margin-bottom: 2rem;
+ margin-bottom: 2rem;
}
+
+/*
+ * http://martinwolf.org/2015/01/08/pure-css-savingloading-dots-animation/
+ */
+.dl-dots span {
+ animation-name: dl-dots;
+ animation-duration: 1.4s;
+ animation-iteration-count: infinite;
+ animation-fill-mode: both;
+}
+
+.dl-dots span:nth-child(2) {
+ animation-delay: .2s;
+}
+
+.dl-dots span:nth-child(3) {
+ animation-delay: .4s;
+}
+
+@-webkit-keyframes dl-dots {
+ 0% {
+ opacity: .2;
+ }
+ 20% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: .2;
+ }
+}
+
+@-moz-keyframes dl-dots {
+ 0% {
+ opacity: .2;
+ }
+ 20% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: .2;
+ }
+}
+
+@-ms-keyframes dl-dots {
+ 0% {
+ opacity: .2;
+ }
+ 20% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: .2;
+ }
+}
+
+@keyframes dl-dots {
+ 0% {
+ opacity: .2;
+ }
+ 20% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: .2;
+ }
+}
+
+/*
+ * https://gist.github.com/jankorbel/2336513/cb2f68b59521438ac812ad72c1c93d8f656e49ad
+ */
+/*
+.dl-dots span {
+ background: transparent;
+ border-radius: 50%;
+ box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.3);
+ display: inline-block;
+ height: 0.6em;
+ width: 0.6em;
+
+ -webkit-animation: dl-dots 0.8s linear infinite;
+ -moz-animation: dl-dots 0.8s linear infinite;
+ -ms-animation: dl-dots 0.8s linear infinite;
+ animation: dl-dots 0.8s linear infinite;
+}
+
+.dl-dots span:nth-child(1) {
+ -webkit-animation-delay: 0.4s;
+ -moz-animation-delay: 0.4s;
+ -ms-animation-delay: 0.4s;
+ animation-delay: 0.4s;
+}
+
+.dl-dots span:nth-child(2) {
+ -webkit-animation-delay: 0.2s;
+ -moz-animation-delay: 0.2s;
+ -ms-animation-delay: 0.2s;
+ animation-delay: 0.2s;
+}
+
+!*
+ Define the animation for every efing vendor prefix
+*!
+@-webkit-keyframes dl-dots {
+ 0% {
+ background: transparent;
+ }
+ 50% {
+ background: #E4E4E4;
+ }
+ 100% {
+ background: transparent;
+ }
+}
+
+@-moz-keyframes dl-dots {
+ 0% {
+ background: transparent;
+ }
+ 50% {
+ background: #E4E4E4;
+ }
+ 100% {
+ background: transparent;
+ }
+}
+
+@-ms-keyframes dl-dots {
+ 0% {
+ background: transparent;
+ }
+ 50% {
+ background: #E4E4E4;
+ }
+ 100% {
+ background: transparent;
+ }
+}
+
+@keyframes dl-dots {
+ 0% {
+ background: transparent;
+ }
+ 50% {
+ background: #E4E4E4;
+ }
+ 100% {
+ background: transparent;
+ }
+}
+*/
diff --git a/web/static/app/app.js b/web/static/app/app.js
index 5274362..6fa1f71 100644
--- a/web/static/app/app.js
+++ b/web/static/app/app.js
@@ -11,15 +11,62 @@
ctrl.device = device.data.device;
}
- function PropertyController($route, device, values) {
+ function PropertyController($timeout, $route, DillerRpc, device, values) {
var ctrl = this;
ctrl.device = device.data.device;
ctrl.property = _.find(ctrl.device.properties, {id: $route.current.params.propertyId});
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);
+ })
+ };
+ }
+
+ function TimestampFilter() {
+ return function (value) {
+ if (!value) {
+ return;
+ }
+
+ return moment(value).startOf('second').fromNow();
+ }
+ }
+
+ function DlTimestampDirective() {
+ console.log('DlTimestampDirective', DlTimestampDirective);
+ return {
+ restrict: 'E',
+ scope: {
+ value: '='
+ },
+ replace: true,
+ template: '<span title="{{value|date:\'medium\'}}">{{value|timestamp}}</span>'
+ };
+ }
+
+ function DlDotsDirective() {
+ return {
+ restrict: 'E',
+ scope: {
+ value: '='
+ },
+ replace: true,
+ template: '<span class="dl-dots"><span>.</span><span>.</span><span>.</span></span>\n'
+ };
}
- function config($routeProvider, $locationProvider) {
+ function config($routeProvider) {
$routeProvider
.when('/', {
controller: FrontPageController,
@@ -49,8 +96,6 @@
.otherwise({
redirectTo: '/'
});
-
- //$locationProvider.html5Mode(true);
}
function DillerConfig() {
@@ -67,6 +112,9 @@
angular
.module('Diller', ['ngRoute'])
.config(config)
+ .filter('timestamp', TimestampFilter)
+ .directive('dlTimestamp', DlTimestampDirective)
+ .directive('dlDots', DlDotsDirective)
.service('DillerConfig', DillerConfig)
.service('DillerRpc', DillerRpc);
})();
diff --git a/web/static/app/templates/property.html b/web/static/app/templates/property.html
index 65a66e8..cd003ca 100644
--- a/web/static/app/templates/property.html
+++ b/web/static/app/templates/property.html
@@ -16,7 +16,15 @@
<li>Description: {{ctrl.property.description}}</li>
</ul>
- <h3>Latest Values</h3>
+ <h3>
+ Latest Values
+ <small ng-hide="ctrl.loading">
+ <a href ng-click="ctrl.refresh()">refresh</a>
+ </small>
+ <small ng-show="ctrl.loading">
+ Loading <dl-dots></dl-dots>
+ </small>
+ </h3>
<table class="table">
<thead>
@@ -27,7 +35,7 @@
</thead>
<tbody>
<tr ng-repeat="v in ctrl.values">
- <td>{{v.timestamp | date:'medium'}}</td>
+ <td><dl-timestamp value="v.timestamp"></dl-timestamp></td>
<td>{{v.value}}</td>
</tr>
</tbody>
diff --git a/web/templates/index.jade b/web/templates/index.jade
index 025354e..6dad0f5 100644
--- a/web/templates/index.jade
+++ b/web/templates/index.jade
@@ -14,6 +14,7 @@ html(lang='en')
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/lodash/lodash.js", type="application/javascript")
+ script(src="bower_components/moment/moment.js", type="application/javascript")
script(src="app/DillerRpc.js", type="application/javascript")
script(src="app/app.js", type="application/javascript")