Commit 29eb2c3d by Martin Kotula

Web dashboard project added

Display a list of sensor statuses

Load mock data from Json
parent 49470971
...@@ -259,3 +259,10 @@ paket-files/ ...@@ -259,3 +259,10 @@ paket-files/
# Python Tools for Visual Studio (PTVS) # Python Tools for Visual Studio (PTVS)
__pycache__/ __pycache__/
*.pyc *.pyc
.DS_Store
node_modules/
bower_modules/
# Don't track build output
dist/
{
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"rules": {
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": "warn",
"constructor-super": "warn",
"valid-typeof": "warn"
}
}
\ No newline at end of file
// Node modules
var fs = require('fs'), vm = require('vm'), merge = require('deeply'), chalk = require('chalk'), es = require('event-stream'), child = require('child_process');
// Gulp and plugins
var gulp = require('gulp'), rjs = require('gulp-requirejs-bundler'), concat = require('gulp-concat'), clean = require('gulp-clean'),
replace = require('gulp-replace'), uglify = require('gulp-uglify'), htmlreplace = require('gulp-html-replace'), webserver = require('gulp-webserver');
// Config
var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require.config.js') + '; require;');
requireJsOptimizerConfig = merge(requireJsRuntimeConfig, {
out: 'scripts.js',
baseUrl: './src',
name: 'app/startup',
paths: {
requireLib: '../node_modules/requirejs/require'
},
include: [
'requireLib',
'components/app/app',
'components/nav-bar/nav-bar',
'pages/home/home',
'pages/about/about'
],
insertRequire: ['app/startup'],
bundles: {
// If you want parts of the site to load on demand, remove them from the 'include' list
// above, and group them into bundles here.
// 'bundle-name': [ 'some/module', 'another/module' ],
// 'another-bundle-name': [ 'yet-another-module' ]
// 'about-page': [ 'pages/about/about' ]
}
});
// Discovers all AMD dependencies, concatenates together all required .js files, minifies them
gulp.task('js', function () {
return rjs(requireJsOptimizerConfig)
.pipe(uglify({ preserveComments: 'some' }))
.pipe(gulp.dest('./dist/'));
});
// Concatenates CSS files, rewrites relative paths to Bootstrap fonts
gulp.task('css', function () {
//Array of all CSS files needed
var appCss = gulp.src([
'./node_modules/bootstrap/dist/css/bootstrap.min.css',
'./src/css/*.css'
])
.pipe(replace(/url\((')?\.\.\/fonts\//g, 'url($1fonts/'));
var combinedCss = es.concat(appCss).pipe(concat('css.css'));
return es.concat(combinedCss)
.pipe(gulp.dest('./dist/'));
});
// Moves the bootstrap fonts to the dist-folder
gulp.task('fonts', function(){
return gulp.src('./node_modules/bootstrap/fonts/*', { base: './node_modules/bootstrap/components-bootstrap/' })
.pipe(gulp.dest('./dist/fonts'));
});
// Copies index.html, replacing <script> and <link> tags to reference production URLs
gulp.task('html', function() {
return gulp.src('./src/index.html')
.pipe(htmlreplace({
'css': 'css.css?' + Date.now(),
'js': 'scripts.js?' + Date.now()
}))
.pipe(gulp.dest('./dist/'));
});
// Removes all files from ./dist/
gulp.task('clean', function() {
return gulp.src('./dist/**/*', { read: false })
.pipe(clean());
});
gulp.task('default', ['html', 'js', 'css', 'fonts'], function(callback) {
callback();
console.log('\nPlaced optimized files in ' + chalk.magenta('dist/\n'));
});
// Sets up a webserver with live reload for development
gulp.task('webserver', function () {
gulp.src('')
.pipe(webserver({
livereload : true,
port : 8050,
directoryListing : true,
open : 'http://localhost:8050/src/index.html'
}));
});
// Runs the intern client, that runs through all unit tests
gulp.task('intern', function (done) {
var command = [
'./node_modules/intern/client.js',
'config=intern'
],
process = child.spawn('node', command, {
stdio : 'inherit'
});
process.on('close', function (code) {
if (code) {
done(new Error('Intern exited with code ' + code));
}
else {
done();
}
});
});
// Watches all source and test files and runs intern every time a file is saved
gulp.task('test', ['intern'], function () {
gulp.watch(['./src/**/*', './test/**/*'], ['intern']);
});
// Fires up the intern web-client in your browser. NB! BEWARE OF BROWSER CACHING
gulp.task('intern-web', function () {
gulp.src('')
.pipe(webserver({
livereload: true,
port: 9999,
directoryListing: true,
open: 'http://localhost:9999/node_modules/intern/client.html?config=intern'
}));
});
// Learn more about configuring this file at <https://theintern.github.io/intern/#configuration>.
// These default settings work OK for most people. The options that *must* be changed below are the packages, suites,
// excludeInstrumentation, and (if you want functional tests) functionalSuites
define({
//basePath: 'src',
// Default desired capabilities for all environments. Individual capabilities can be overridden by any of the
// specified browser environments in the `environments` array below as well. See
// <https://theintern.github.io/intern/#option-capabilities> for links to the different capabilities options for
// different services.
//
// Note that the `build` capability will be filled in with the current commit ID or build tag from the CI
// environment automatically
//capabilities: {
// 'browserstack.selenium_version': '2.45.0'
//},
// Browsers to run integration testing against. Options that will be permutated are browserName, version, platform,
// and platformVersion; any other capabilities options specified for an environment will be copied as-is. Note that
// browser and platform names, and version number formats, may differ between cloud testing systems.
//environments: [
// { browserName: 'internet explorer', version: '11', platform: 'WIN8' },
// { browserName: 'internet explorer', version: '10', platform: 'WIN8' },
// { browserName: 'internet explorer', version: '9', platform: 'WINDOWS' },
// { browserName: 'firefox', version: '37', platform: [ 'WINDOWS', 'MAC' ] },
// { browserName: 'chrome', version: '39', platform: [ 'WINDOWS', 'MAC' ] },
// { browserName: 'safari', version: '8', platform: 'MAC' }
//],
// Maximum number of simultaneous integration tests that should be executed on the remote WebDriver service
//maxConcurrency: 2,
// Name of the tunnel class to use for WebDriver tests.
// See <https://theintern.github.io/intern/#option-tunnel> for built-in options
//tunnel: 'BrowserStackTunnel',
// Set Requirejs to be the default amd loader
loaders: {
'host-node': 'requirejs',
'host-browser': 'node_modules/requirejs/require.js'
},
// Configuration options for the module loader; any AMD configuration options supported by the AMD loader in use
// can be used here.
// If you want to use a different loader than the default loader, see
// <https://theintern.github.io/intern/#option-useLoader> for more information.
loaderOptions: {
//baseUrl : "src",
paths: {
// Libraries
"home": "src/pages/home/home",
"text": "node_modules/requirejs-text/text",
"knockout": "../node_modules/knockout/build/output/knockout-latest"
}
// Packages that should be registered with the loader in each testing environment
//packages : [{ name : 'myPackage', location : '.' }]
},
// Unit test suite(s) to run in each browser
suites: ['test/pages/home' /* 'myPackage/tests/foo', 'myPackage/tests/bar' */],
// Functional test suite(s) to execute against each browser once unit tests are completed
functionalSuites: [/* 'myPackage/tests/functional' */],
// A regular expression matching URLs to files that should not be included in code coverage analysis. Set to `true`
// to completely disable code coverage.
excludeInstrumentation: /^(?:test|node_modules)\//
});
\ No newline at end of file
[
{ "name": "Sensor 1", "isOccupied": true, "timestamp": "2017-06-17T21:10:59.948Z"},
{ "name": "Sensor 2", "isOccupied": true, "timestamp": "2017-06-17T21:10:59.948Z"},
{ "name": "Sensor 1", "isOccupied": true, "timestamp": "2017-06-17T21:11:59.948Z"},
{ "name": "Sensor 2", "isOccupied": true, "timestamp": "2017-06-17T21:11:59.948Z"},
{ "name": "Sensor 1", "isOccupied": true, "timestamp": "2017-06-17T21:12:59.948Z"},
{ "name": "Sensor 1", "isOccupied": false, "timestamp": "2017-06-17T21:13:59.948Z"},
{ "name": "Sensor 3", "isOccupied": false, "timestamp": "2017-06-17T21:13:59.948Z"},
{ "name": "Sensor 3", "isOccupied": true, "timestamp": "2017-06-17T21:13:59.949Z"}
]
\ No newline at end of file
{
"name": "occupancysensordashboard",
"version": "0.0.0",
"devDependencies": {
"chalk": "~0.4.0",
"deeply": "~0.1.0",
"eslint": "^4.0.0",
"event-stream": "^3.3.4",
"gulp": "^3.9.1",
"gulp-clean": "^0.3.2",
"gulp-concat": "^2.6.0",
"gulp-html-replace": "^1.6.1",
"gulp-less": "^3.2.0",
"gulp-replace": "^0.5.4",
"gulp-requirejs-bundler": "^0.1.1",
"gulp-uglify": "^2.0.0",
"gulp-webserver": "^0.9.1",
"intern": "^3.4.2"
},
"dependencies": {
"bootstrap": "^3.3.7",
"crossroads": "^0.12.2",
"hasher": "^1.2.0",
"jquery": "^3.2.1",
"knockout": "^3.4.1",
"lodash": "^4.17.4",
"requirejs": "^2.3.2",
"requirejs-text": "^2.0.12"
}
}
# Installation
1. Install npm https://www.npmjs.com/
1. Run `npm install`
1. Run `gulp webserver`
define({
// Pages - knockout components that serve as pages
pages: [
// [Scaffolded pages will be inserted here. To retain this feature, don't remove this comment.]
{ name: 'home', path: 'pages/home/home' },
{ name: 'about', path: 'pages/about/about' }
],
// Components - knockout components intended as parts of pages, a.k.a. components
components: [
// [Scaffolded components will be inserted here. To retain this feature, don't remove this comment.]
{ name: 'nav-bar', path: 'components/nav-bar/nav-bar' },
{ name: 'app', path: 'components/app/app' },
{ name: 'sensor', path: 'components/sensor/sensor' }
]
});
// require.js looks for the following global when initializing
var require = {
baseUrl: "../src",
paths: {
// [Scaffolded bindings will be inserted here. To retain this feature, don't remove this comment.]
"crossroads": "../node_modules/crossroads/dist/crossroads.min",
"hasher": "../node_modules/hasher/dist/js/hasher.min",
"knockout": "../node_modules/knockout/build/output/knockout-latest",
"signals": "../node_modules/signals/dist/signals.min",
"text": "../node_modules/requirejs-text/text",
"lodash": "../node_modules/lodash/lodash",
"jquery": "../node_modules/jquery/dist/jquery.min"
}
};
define(["knockout", "crossroads", "hasher"], function(ko, crossroads, hasher) {
// This module configures crossroads.js, a routing library. If you prefer, you
// can use any other routing library (or none at all) as Knockout is designed to
// compose cleanly with external libraries.
//
// You *don't* have to follow the pattern established here (each route entry
// specifies a 'page', which is a Knockout component) - there's nothing built into
// Knockout that requires or even knows about this technique. It's just one of
// many possible ways of setting up client-side routes.
return new Router({
routes: [
{ url: '', params: { page: 'home' } },
{ url: 'about', params: { page: 'about' } }
]
});
function Router(config) {
var currentRoute = this.currentRoute = ko.observable({});
ko.utils.arrayForEach(config.routes, function(route) {
crossroads.addRoute(route.url, function(requestParams) {
currentRoute(ko.utils.extend(requestParams, route.params));
});
});
activateCrossroads();
}
function activateCrossroads() {
function parseHash(newHash, oldHash) { crossroads.parse(newHash); }
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
hasher.initialized.add(parseHash);
hasher.changed.add(parseHash);
hasher.init();
}
});
define(['knockout', './router', './components.config'], function (ko, router, components) {
// Register all page-components in knockout
components.pages.forEach(register);
// Register all regular components
components.components.forEach(register);
function register(component){
ko.components.register(component.name, { require : component.path});
};
// [Scaffolded component registrations will be inserted here. To retain this feature, don't remove this comment.]
// Start the application
ko.applyBindings({ route : router.currentRoute });
});
\ No newline at end of file
<nav-bar params="route: route"></nav-bar>
<div id="page" class="container" data-bind="component: { name: route().page, params: route }"></div>
define(['knockout', 'text!./app.html'], function(ko, template) {
function App(params) {
// This viewmodel doesn't do anything except pass through the 'route' parameter to the view.
this.route = params.route;
}
return { viewModel: App, template: template };
});
<!--
The navigation UI that is docked to the top of the window. Most of this markup simply
follows Bootstrap conventions. The only Knockout-specific parts are the data-bind
attributes on the <li> elements.
-->
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">OccupancySensorDashboard</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li data-bind="css: { active: route().page === 'home' }">
<a href="#">Home</a>
</li>
<li data-bind="css: { active: route().page === 'about' }">
<a href="#about">About</a>
</li>
</ul>
</div>
</div>
</div>
define(['knockout', 'text!./nav-bar.html'], function(ko, template) {
function NavBarViewModel(params) {
// This viewmodel doesn't do anything except pass through the 'route' parameter to the view.
// You could remove this viewmodel entirely, and define 'nav-bar' as a template-only component.
// But in most apps, you'll want some viewmodel logic to determine what navigation options appear.
this.route = params.route;
}
return { viewModel: NavBarViewModel, template: template };
});
<div class="sensor">
<h3 data-bind="text: name"></h3>
<span data-bind="visible: isOccupied()" class="glyphicon glyphicon-ban-circle" style="color:red" aria-hidden="true" title="Occupied"></span>
<span data-bind="visible: isOccupied() === undefined" class="glyphicon glyphicon-question-sign" style="color: grey" aria-hidden="true" title="Unkown"></span>
<span data-bind="visible: isOccupied() === false" class="glyphicon glyphicon-ok-circle" style="color: green" aria-hidden="true"></span>
<p>Timestamp: <span data-bind="text: timestamp"></span></p>
</div>
\ No newline at end of file
define(['knockout', 'text!./sensor.html'], function(ko, template) {
function Sensor(params) {
this.isOccupied = params.isOccupied;
this.name = params.name;
this.timestamp = params.timestamp;
}
return { viewModel: Sensor, template: template };
});
define(['knockout'], function(ko) {
function SensorViewModel(params) {
this.isOccupied = ko.observable(params.isOccupied);
this.name = params.name;
this.timestamp = ko.observable(params.timestamp);
}
return SensorViewModel;
});
\ No newline at end of file
#page {
margin-top: 80px;
}
.sensor .glyphicon{
font-size: 4em;
text-align: center;
}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>OccupancySensorDashboard</title>
<!-- build:css -->
<link href="../node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
<link href="css/styles.css" rel="stylesheet">
<!-- endbuild -->
<!-- build:js -->
<script src="app/require.config.js"></script>
<script data-main="app/startup" src="../node_modules/requirejs/require.js"></script>
<!-- endbuild -->
</head>
<body>
<app params="route: route"></app>
</body>
</html>
<h2>About</h2>
<p>This component has no viewmodel. It's just an HTML template.</p>
define(['text!./about.html'], function (aboutTemplate) {
// This module could have been a template-only component.
// For examples, see earlier commits or the original
// generator-ko project.
return { viewModel: function (){}, template: aboutTemplate };
});
\ No newline at end of file
<h2>Edge1 Occupancy dashboard</h2>
<div data-bind="visible: sensors().length == 0">
<h3>Waiting for sensor readings</h3>
</div>
<div class="row" data-bind="foreach: sensors">
<div class="col-xs-12 col-md-4">
<sensor params="name: name, isOccupied: isOccupied, timestamp: timestamp"></sensor>
</div>
</div>
<button type="button" class="btn btn-default btn-lg" data-bind="click: refresh, disable: isRefreshing">
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Refresh
</button>
<div data-bind="visible: isRefreshing">
Refreshing...
</div>
\ No newline at end of file
define([
"knockout",
"text!./home.html",
"components/sensor/sensorViewModel",
"queryHandlers/sensorReadingsQueryHandler",
"lodash"
], function (
ko,
homeTemplate,
SensorViewModel,
SensorReadingsQueryHandler,
_
) {
function HomeViewModel(route) {
this.sensorsDict = {};
this.sensors = ko.observableArray();
this.isRefreshing = ko.observable(false);
this.queryHandler = new SensorReadingsQueryHandler();
}
HomeViewModel.prototype.addSensor = function (key, sensorData) {
if (!this.sensorsDict[key]) {
var viewModel = new SensorViewModel(sensorData);
this.sensorsDict[key] = viewModel;
this.sensors.push(viewModel);
}
}
HomeViewModel.prototype.updateSensor = function (key, sensorData) {
var viewModel = this.sensorsDict[key];
if (viewModel) {
viewModel.isOccupied(sensorData.isOccupied);
viewModel.timestamp(sensorData.timestamp);
}
}
HomeViewModel.prototype.refresh = function () {
var that = this;
this.isRefreshing(true);
this.queryHandler.handle().then(function(data) {
var sensorsGroupedByName = _.groupBy(data, "name")
_.each(sensorsGroupedByName, function(sensorReadings, key){
var latestReading = _.orderBy(sensorReadings, "timestamp").pop();
if(that.sensorsDict[key]){
that.updateSensor(key, latestReading);
} else {
that.addSensor(key, latestReading);
}
})
that.isRefreshing(false);
})
}
return { viewModel: HomeViewModel, template: homeTemplate };
});
define(["jquery"], function($) {
function SensorReadingsQueryHandler() {
}
SensorReadingsQueryHandler.prototype.handle = function(){
return $.ajax('/mockData.json');
}
return SensorReadingsQueryHandler;
});
\ No newline at end of file
define(['home', 'intern!bdd', 'intern/chai!expect'], function (homePage, bdd, expect) {
var HomePageViewModel = homePage.viewModel;
bdd.describe('Home page view model', function () {
bdd.it('should supply a friendly message which changes when acted upon', function () {
var instance = new HomePageViewModel();
expect(instance.message()).to.contain('Welcome to ');
// See the message change
instance.doSomething();
expect(instance.message()).to.contain('You invoked doSomething()');
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment