From aa885f331cd9acfa09e8d154befa2bdf0b3c6f08 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Fri, 4 Mar 2016 13:59:34 -0600 Subject: [PATCH] implement reactjs history --- client/app.js | 7 +- client/config/development/app.js | 5 +- client/config/development/templates.js | 4 - client/dashboard/about/about.component.js | 13 -- client/dashboard/about/about.rt | 18 -- client/dashboard/about/about.scss | 3 - client/dashboard/energy/energy.component.js | 26 +-- client/dashboard/energy/energy.rt | 26 ++- .../dashboard/energy/graph/graph.component.js | 26 ++- .../dashboard/energy/table/table.component.js | 14 +- client/dashboard/energy/table/table.rt | 2 +- client/dashboard/house/house.component.js | 66 ------ client/dashboard/house/house.rt | 49 ----- client/dashboard/house/house.scss | 1 - client/dashboard/layout/layout.component.js | 63 +++--- client/dashboard/layout/layout.rt | 92 ++++++++- .../dashboard/power/graph/graph.component.js | 27 +-- client/dashboard/power/power.component.js | 72 +++---- client/dashboard/power/power.rt | 19 +- .../dashboard/power/table/table.component.js | 14 +- client/dashboard/power/table/table.rt | 4 +- client/dashboard/routes.js | 195 ------------------ client/dashboard/state_manager.js | 174 ++++++++++++++++ client/models/house.js | 115 ++++++----- npm-debug.log | 45 ++++ package.json | 3 +- shared/utils/object.js | 12 ++ 27 files changed, 549 insertions(+), 546 deletions(-) delete mode 100644 client/dashboard/about/about.component.js delete mode 100644 client/dashboard/about/about.rt delete mode 100644 client/dashboard/about/about.scss delete mode 100644 client/dashboard/house/house.component.js delete mode 100644 client/dashboard/house/house.rt delete mode 100644 client/dashboard/house/house.scss delete mode 100644 client/dashboard/routes.js create mode 100644 client/dashboard/state_manager.js create mode 100644 npm-debug.log create mode 100644 shared/utils/object.js diff --git a/client/app.js b/client/app.js index 9d6c715..733196a 100644 --- a/client/app.js +++ b/client/app.js @@ -2,13 +2,12 @@ import 'babel-polyfill'; import 'bootstrap/dist/js/bootstrap.min'; import React from 'react'; import ReactDOM from 'react-dom'; -import {Router} from 'react-router'; -import {ROUTES} from './dashboard/routes'; +import Layout from './dashboard/layout/layout.component'; -export default function(history){ +export default function(createHistory){ ReactDOM.render( - React.createElement(Router, {routes: ROUTES, history: history}), + React.createElement(Layout, {createHistory: createHistory}), document.getElementById('root') ); }; diff --git a/client/config/development/app.js b/client/config/development/app.js index e86c9ec..7bd62dc 100644 --- a/client/config/development/app.js +++ b/client/config/development/app.js @@ -1,4 +1,5 @@ -import {browserHistory} from 'react-router'; +import { createHistory, useQueries } from 'history' + import app from './../../app'; -app(browserHistory); +app(useQueries(createHistory)); diff --git a/client/config/development/templates.js b/client/config/development/templates.js index 22203d5..4f6ce97 100644 --- a/client/config/development/templates.js +++ b/client/config/development/templates.js @@ -3,8 +3,6 @@ import fs from 'fs'; -import aboutRt from './../../dashboard/about/about.rt'; -import houseRt from './../../dashboard/house/house.rt'; import layoutRt from './../../dashboard/layout/layout.rt'; import energyRt from './../../dashboard/energy/energy.rt'; import energyGraphRt from './../../dashboard/energy/graph/graph.rt'; @@ -14,8 +12,6 @@ import powerGraphRt from './../../dashboard/power/graph/graph.rt'; import powerTableRt from './../../dashboard/power/table/table.rt'; const TEMPLATES = { - about: aboutRt, - house: houseRt, layout: layoutRt, energy: energyRt, energy_graph: energyGraphRt, diff --git a/client/dashboard/about/about.component.js b/client/dashboard/about/about.component.js deleted file mode 100644 index 3802dfe..0000000 --- a/client/dashboard/about/about.component.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import Templates from 'config/templates'; - -class AboutComponent extends React.Component { - - render() { - var aboutRt = Templates.forComponent('about'); - return aboutRt.call(this); - } - -} - -export default AboutComponent; diff --git a/client/dashboard/about/about.rt b/client/dashboard/about/about.rt deleted file mode 100644 index fe4a2cb..0000000 --- a/client/dashboard/about/about.rt +++ /dev/null @@ -1,18 +0,0 @@ -
-
-
About
-
-

This is a Spike bundle prototype using the following lirbaries:

-
    -
  • React
  • -
  • React Templates
  • -
  • React Router
  • -
  • LokiJs - persisting API calls to indexedDb
  • -
  • Webpack - hot mode developing and app bundling
  • -
  • Babel - ES6 transpiler
  • -
-

The demo app consists of a dataset of 10 houses and 10 years of randomly generated power consumption and production at 15 minute intervals. You can toggle between different houses and time periods to compare and contrast the dataset.

-

Select a house below to get started.

-
-
-
diff --git a/client/dashboard/about/about.scss b/client/dashboard/about/about.scss deleted file mode 100644 index fbbd802..0000000 --- a/client/dashboard/about/about.scss +++ /dev/null @@ -1,3 +0,0 @@ -#about { - -} diff --git a/client/dashboard/energy/energy.component.js b/client/dashboard/energy/energy.component.js index a7d1df3..36d31d0 100644 --- a/client/dashboard/energy/energy.component.js +++ b/client/dashboard/energy/energy.component.js @@ -1,30 +1,33 @@ import React from 'react'; import Templates from 'config/templates'; -import {RouteHelper} from './../routes'; class EnergyComponent extends React.Component { constructor(props){ super(props); - var energy = this; } - componentDidMount(){ - var energy = this; + get state_manager(){ + return this.props.state_manager; } - componentDidUpdate(prev_props, prev_state, prev_context){ + get loading_energy_data(){ + return this.props.loading_energy_data; + } + + syncFromStateManager(fnStateSet){ var energy = this; + energy.setState(energy.state_manager.state, fnStateSet); } setParam(event){ var energy = this, param = event.target.dataset.param, value = event.target.dataset.value, - update = {}, route_helper; + update = {}; update[param] = value; - route_helper = new RouteHelper(energy.props, update); - if (route_helper.routeUpdated()) route_helper.updateRoute(); + if (value == energy.state_manager.state[param]) return false; + energy.state_manager.setParams(update, energy); } render() { @@ -32,9 +35,6 @@ class EnergyComponent extends React.Component { return energyRt.call(this); } } +EnergyComponent.NAME = 'EnergyComponent' -EnergyComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; - -export default EnergyComponent; +module.exports = EnergyComponent; diff --git a/client/dashboard/energy/energy.rt b/client/dashboard/energy/energy.rt index 659567b..cced39f 100644 --- a/client/dashboard/energy/energy.rt +++ b/client/dashboard/energy/energy.rt @@ -1,5 +1,7 @@ + +
-
+
Retrieving energy data...
@@ -8,16 +10,28 @@
- {this.props.children} + + +
diff --git a/client/dashboard/energy/graph/graph.component.js b/client/dashboard/energy/graph/graph.component.js index 929da07..9c3ce72 100644 --- a/client/dashboard/energy/graph/graph.component.js +++ b/client/dashboard/energy/graph/graph.component.js @@ -8,27 +8,29 @@ class GraphComponent extends React.Component { componentDidMount(){ var energy_graph = this; - if (energy_graph.house) energy_graph.updateGraph(); + energy_graph.updateGraph(); } get house(){ - return this.props.location.state && this.props.location.state.house; + return this.props.house; } - componentDidUpdate(prev_props, prev_state, prev_context){ - var energy_graph = this; - if (energy_graph.shouldUpdateGraph(prev_props)) { energy_graph.updateGraph(); } + get state_manager(){ + return this.props.state_manager; } - shouldUpdateGraph(prev_props){ + componentDidUpdate(prev_props, prev_state){ var energy_graph = this; - return energy_graph.house && !prev_props.location.state.house || - prev_props.location.state.house.id != energy_graph.house.id; + if (prev_props.house != energy_graph.props.house || + prev_props.year != energy_graph.props.year || + prev_props.graph_attr != energy_graph.props.graph_attr){ + energy_graph.updateGraph(); + } } updateGraph(){ var energy_graph = this, - graph_attr = energy_graph.props.params.graph_attr; + graph_attr = energy_graph.props.graph_attr; if (energy_graph.graph === undefined){ energy_graph.graph = new CalendarGridChart({ @@ -68,8 +70,4 @@ class GraphComponent extends React.Component { } -GraphComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; - -export default GraphComponent; +module.exports = GraphComponent; diff --git a/client/dashboard/energy/table/table.component.js b/client/dashboard/energy/table/table.component.js index d45f8a9..ed6b20c 100644 --- a/client/dashboard/energy/table/table.component.js +++ b/client/dashboard/energy/table/table.component.js @@ -5,6 +5,14 @@ import House from './../../../models/house'; class TableComponent extends React.Component { + get state_manager(){ + return this.props.state_manager; + } + + get house(){ + return this.state_manager.state.house; + } + render() { var tableRt = Templates.forComponent('energy_table'); return tableRt.call(this); @@ -12,8 +20,4 @@ class TableComponent extends React.Component { } -TableComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; - -export default TableComponent; +module.exports = TableComponent; diff --git a/client/dashboard/energy/table/table.rt b/client/dashboard/energy/table/table.rt index 6d042e5..78538b1 100644 --- a/client/dashboard/energy/table/table.rt +++ b/client/dashboard/energy/table/table.rt @@ -1,4 +1,4 @@ - +
diff --git a/client/dashboard/house/house.component.js b/client/dashboard/house/house.component.js deleted file mode 100644 index 034f141..0000000 --- a/client/dashboard/house/house.component.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import Templates from 'config/templates'; -import House from './../../models/house'; -import {RouteHelper} from './../routes'; -import EnergyComponent from './../energy/energy.component'; -import PowerComponent from './../power/power.component'; - -class HouseComponent extends React.Component { - - constructor(props){ - super(props); - this.renders = 0; - this.updates = 0; - } - - get house(){ - return this.props.location.state && this.props.location.state.house; - } - - setParam(event){ - var house_component = this, - param = event.target.dataset.param, - value = event.target.dataset.value, - update = {}, route_helper; - update[param] = value; - route_helper = new RouteHelper(house_component.props, update); - if (route_helper.routeUpdated()) route_helper.updateRoute(); - } - - componentDidUpdate(){ - this.updates += 1; - console.log(this.updates, ') HouseComponent#componentDidUpdate'); - } - - graphSelected(){ - var house_component = this; - return RouteHelper.graphSelected(house_component.props.routes); - } - - tableSelected(){ - var house_component = this; - return RouteHelper.tableSelected(house_component.props.routes); - } - - energySelected(){ - var house_component = this; - return RouteHelper.energySelected(house_component.props.routes); - } - - powerSelected(){ - var house_component = this; - return RouteHelper.powerSelected(house_component.props.routes); - } - - render() { - var houseRt = Templates.forComponent('house'); - return houseRt.call(this); - } - -}; - -HouseComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; - -export default HouseComponent; diff --git a/client/dashboard/house/house.rt b/client/dashboard/house/house.rt deleted file mode 100644 index d83635f..0000000 --- a/client/dashboard/house/house.rt +++ /dev/null @@ -1,49 +0,0 @@ -
-

Select dataset:

-
- - -
- -

View as:

-
- - -
- -
-

Select dates:

-
- -
-

- - {this.props.children} -
diff --git a/client/dashboard/house/house.scss b/client/dashboard/house/house.scss deleted file mode 100644 index bf6684c..0000000 --- a/client/dashboard/house/house.scss +++ /dev/null @@ -1 +0,0 @@ -#house {} diff --git a/client/dashboard/layout/layout.component.js b/client/dashboard/layout/layout.component.js index a7ccaba..32799bb 100644 --- a/client/dashboard/layout/layout.component.js +++ b/client/dashboard/layout/layout.component.js @@ -1,62 +1,67 @@ import React from 'react'; +import { createHistory } from 'history'; + +import ObjectUtil from './../../../shared/utils/object'; import Templates from 'config/templates'; import House from './../../models/house'; import PowerDatum from './../../models/power_datum'; -import {RouteHelper} from './../routes'; +import StateManager from './../state_manager'; class LayoutComponent extends React.Component { constructor(props, context){ super(props, context); - this.renders = 0; - this.state = { + var layout = this; + layout.state = { + loading_houses: true, houses: null, house: null, - loading_house_data: true - }; - this.updates = 0 + dataset: null, + year: null, + view: null + } } get house(){ - return this.props.location.state && this.props.location.state.house; + return this.state_manager && this.state_manager.state.house; } componentDidMount() { var layout = this; House.ensureHouses().then((houses)=>{ var house = null; - if (layout.props.params.house_id != undefined){ - house = houses.find((h)=>{ return h.data.id == layout.props.params.house_id; }); - } layout.setState({ - houses: houses, - loading_house_data: false + houses: houses, + loading_houses: false }, ()=>{ - if (house){ - var route_helper = new RouteHelper(layout.context.router, layout.props, {house: house}); - route_helper.updateRoute(); - } + layout.state_manager = new StateManager(layout.props.createHistory, houses); + layout.state_manager.history.listen((location)=>{ + layout.state_manager.updateStateFromUrl(location, layout); + }); }); }); } - componentDidUpdate(){ + syncFromStateManager(fnStateSet){ var layout = this; - - this.updates += 1; - console.log(this.updates, ') LayoutComponent#componentDidUpdate'); + layout.setState(layout.state_manager.state, fnStateSet); } setHouse(event){ var layout = this, house_id = event.target.value; - if (!layout.house || layout.house.id != house_id){ - House.ensureHouses().then((houses)=>{ - var new_house = houses.find((h)=>{ return h.data.id == house_id }), - route_helper = new RouteHelper(layout.context.router, layout.props, {house: new_house}); - route_helper.updateRoute(); - }); - } + if (layout.state_manager.state.house_id == house_id) return false; + layout.state_manager.setParams({house_id: house_id}, layout); + } + + setParam(event){ + var layout = this, + param = event.target.dataset.param, + value = event.target.dataset.value, + update = {}; + update[param] = value; + if (value == layout.state_manager.state[param]) return false; + layout.state_manager.setParams(update, layout); } refreshData(){ @@ -78,8 +83,4 @@ class LayoutComponent extends React.Component { } } -LayoutComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; - export default LayoutComponent; diff --git a/client/dashboard/layout/layout.rt b/client/dashboard/layout/layout.rt index 1c81e6d..956db9d 100644 --- a/client/dashboard/layout/layout.rt +++ b/client/dashboard/layout/layout.rt @@ -1,11 +1,97 @@ + +
-
Retrieving houses...
+
+
+
About
+
+

This is a Spike bundle prototype using the following libraries:

+
    +
  • React
  • +
  • React Templates
  • +
  • React Router
  • +
  • LokiJs - persisting API calls to indexedDb
  • +
  • Webpack - hot mode developing and app bundling
  • +
  • Babel - ES6 transpiler
  • +
+

The demo app consists of a dataset of 10 houses and 10 years of randomly generated power consumption and production at 15 minute intervals. You can toggle between different houses and time periods to compare and contrast the dataset.

+

Select a house below to get started.

+
+
+
+ +
Retrieving houses...

Select household:

- - {this.props.children} +
+

Select dataset:

+
+ + +
+ +

View as:

+
+ + +
+ +
+

Select dates:

+
+ +
+

+ + + +
+
diff --git a/client/dashboard/power/graph/graph.component.js b/client/dashboard/power/graph/graph.component.js index 8a4e016..25ae19b 100644 --- a/client/dashboard/power/graph/graph.component.js +++ b/client/dashboard/power/graph/graph.component.js @@ -8,30 +8,27 @@ class GraphComponent extends React.Component { componentDidMount(){ var power_graph = this; - power_graph.graph_title = ' '; - if (power_graph.house) power_graph.updateGraph(); + power_graph.updateGraph(); } get house(){ - return this.props.location.state && this.props.location.state.house; + return this.state_manager.state.house; } - componentDidUpdate(prev_props, prev_state, prev_context){ + get state_manager(){ + return this.props.state_manager; + } + + componentDidUpdate(prev_props, prev_state){ var power_graph = this; - if (power_graph.shouldUpdateGraph(prev_props)) { + if (prev_props.house != power_graph.props.house || prev_props.power_range != power_graph.props.power_range){ power_graph.updateGraph(); } } - shouldUpdateGraph(prev_props){ - var power_graph = this; - return (power_graph.house && !prev_props.location.state.house || - prev_props.location.state.house.id != power_graph.props.location.state.house.id); - } - updateGraph(){ var power_graph = this, - house = power_graph.context.house; + house = power_graph.house; if (power_graph.graph === undefined){ power_graph.graph = new SplineStackChart({ container: '#power_graph', @@ -87,8 +84,4 @@ class GraphComponent extends React.Component { } -GraphComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; - -export default GraphComponent; +module.exports = GraphComponent; diff --git a/client/dashboard/power/power.component.js b/client/dashboard/power/power.component.js index faf1c0b..c4792a9 100644 --- a/client/dashboard/power/power.component.js +++ b/client/dashboard/power/power.component.js @@ -5,55 +5,54 @@ import _ from 'lodash'; import Templates from 'config/templates'; import House from './../../models/house'; import DateRangeSlider from './../../d3/sliders/date_range'; -import {RouteHelper} from './../routes'; class PowerComponent extends React.Component { constructor(props){ super(props); - var power = this; - power.updates = 0; + this.state = { + loading_power_data: false, + house: null, + power_range: null + }; } get house(){ - console.log('PowerComponent#get house', this.props.location.state && this.props.location.state.house) - return this.props.location.state && this.props.location.state.house; + return this.state_manager && this.state_manager.state && this.state_manager.state.house; + } + + get state_manager(){ + return this.props.state_manager; + } + + get loading_power_data(){ + return this.props.loading_power_data || this.state.loading_power_data; } componentDidMount(){ - var power = this, - house = power.props.location.state.house; - - console.log(this.updates, ') PowerComponent#componentDidMount') - console.log(this.house) - power.renders = 0; - if (!house) return false; + var power = this; power.initDateRange(); } - componentDidUpdate(prev_props, prev_state, prev_context){ - this.updates += 1 - console.log(this.updates, ') PowerComponent#componentDidUpdate') - console.log(this.house) + componentDidUpdate(prev_props, prev_state){ var power = this, - route_helper = new RouteHelper(power.props); - if (!route_helper.house) return false; - if (power.shouldInitDateRange(prev_props)) { + state_manager = power.state_manager; + if (prev_props.month != power.props.month || + prev_props.year != power.props.year || + prev_props.house != power.props.house){ power.initDateRange(); + state_manager.powerDataRendered(); } } - shouldInitDateRange(prev_props){ - var power = this, - route_helper = new RouteHelper(power.props); - return !prev_props.location.state.house || - prev_props.location.state.house.data.id != power.context.house.data.id || - !route_helper.house.matchesPowerRange(prev_props.params, prev_props.location.query['dates[]'] || []); + syncFromStateManager(fnStateSet){ + var power = this; + power.setState(power.state_manager.state, fnStateSet); } initDateRange(){ var power = this, - house = power.context.house; + house = power.house; if (power.date_range_slider === undefined){ power.date_range_slider = new DateRangeSlider({ container: '#power_date_setter', @@ -73,10 +72,8 @@ class PowerComponent extends React.Component { power.date_range_slider.onRangeUpdated = (min, max)=>{ if (power.date_range_update) clearTimeout(power.date_range_update); power.date_range_update = setTimeout(()=>{ - var power_range = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)], - route_helper = new RouteHelper(house, power.props, {power_range: power_range}); - - route_helper.updateRoute(); + var power_range = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)]; + power.state_manager.setParams({power_range: power_range}, power); }, 500); }; power.date_range_slider.drawData({ @@ -89,26 +86,21 @@ class PowerComponent extends React.Component { setParam(event){ var power = this, - house = power.context.house, param = event.target.dataset.param, value = event.target.dataset.value, update = {}, route_helper; update[param] = value; - route_helper = new RouteHelper(house, power.props, update); - if (route_helper.routeUpdated()){ - route_helper.updateHouseState(); - power.context.router.push(route_helper.newRoute()); - } + if (value == power.state_manager.state[param]) return false; + power.state_manager.setParams(update, power); } render() { var powerRt = Templates.forComponent('power'); return powerRt.call(this); } + } -PowerComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; +PowerComponent.NAME = 'PowerComponent' -export default PowerComponent; +module.exports = PowerComponent diff --git a/client/dashboard/power/power.rt b/client/dashboard/power/power.rt index ebd6513..281d102 100644 --- a/client/dashboard/power/power.rt +++ b/client/dashboard/power/power.rt @@ -1,3 +1,5 @@ + +
-
+
Retrieving power data...
- {this.props.children} + +
diff --git a/client/dashboard/power/table/table.component.js b/client/dashboard/power/table/table.component.js index 10d81e7..9942df9 100644 --- a/client/dashboard/power/table/table.component.js +++ b/client/dashboard/power/table/table.component.js @@ -5,14 +5,18 @@ import House from './../../../models/house'; class TableComponent extends React.Component { + get state_manager(){ + return this.props.state_manager; + } + + get house(){ + return this.state_manager.state.house; + } + render() { var powerTableRt = Templates.forComponent('power_table'); return powerTableRt.call(this); } } -TableComponent.contextTypes = { - router: React.PropTypes.object.isRequired -}; - -export default TableComponent; +module.exports = TableComponent; diff --git a/client/dashboard/power/table/table.rt b/client/dashboard/power/table/table.rt index 0482182..700f5b3 100644 --- a/client/dashboard/power/table/table.rt +++ b/client/dashboard/power/table/table.rt @@ -1,4 +1,4 @@ -
+
@@ -8,7 +8,7 @@ - + diff --git a/client/dashboard/routes.js b/client/dashboard/routes.js deleted file mode 100644 index f9fcf8b..0000000 --- a/client/dashboard/routes.js +++ /dev/null @@ -1,195 +0,0 @@ -import House from './house/house.component' -import Power from './power/power.component'; -import PowerGraph from './power/graph/graph.component'; -import PowerTable from './power/table/table.component'; -import Energy from './energy/energy.component'; -import EnergyGraph from './energy/graph/graph.component'; -import EnergyTable from './energy/table/table.component'; -import About from './about/about.component'; -import Layout from './layout/layout.component'; -import ArrayUtil from './../../shared/utils/array'; - -const POWER_ROUTES = { - path: 'power', - component: Power, - childRoutes: [ - {path: ':month/:year', component: PowerGraph}, - {path: ':month/:year/graph', component: PowerGraph}, - {path: ':month/:year/table', component: PowerTable} - ] -}; - -const ENERGY_ROUTES = { - path: 'energy', - component: Energy, - childRoutes: [ - {path: ':year/:graph_attr', component: EnergyGraph}, - {path: ':year/:graph_attr/graph', component: EnergyGraph}, - {path: ':year/:graph_attr/table', component: EnergyTable} - ] -}; - -export const ROUTES = [{ - path: '/', - component: Layout, - indexRoute: { component: About }, - childRoutes: [{ - path: 'houses/:house_id', - component: House, - childRoutes: [ENERGY_ROUTES, POWER_ROUTES] - }] -}]; - -export class RouteHelper { - - constructor(router, props, update){ - update = update || {}; - var route_helper = this; - route_helper.props = props; - route_helper.router = router; - route_helper.update = update || {}; - } - - get house(){ - var route_helper = this; - return route_helper.update.house || route_helper.props.location.state && route_helper.props.location.state.house; - } - - get view(){ - var route_helper = this; - return route_helper.update.view || (route_helper.tableSelected() ? 'table' : 'graph'); - } - - get graph_attr(){ - var route_helper = this; - return route_helper.update.graph_attr || route_helper.props.params.graph_attr || 'consumption'; - } - - get dataset(){ - var route_helper = this; - return route_helper.update.dataset || (route_helper.energySelected() ? 'energy' : 'power'); - } - - get power_range(){ - var route_helper = this, - range = route_helper.update.power_range || route_helper.props.location.query.dates; - if (range) { - range[0] = +range[0]; - range[1] = +range[1]; - } - return range; - } - - get date_params(){ - var route_helper = this; - return { - month: route_helper.update.month || route_helper.house.month, - year: route_helper.update.year || route_helper.house.year }; - } - - get new_state(){ - var route_helper = this; - return Object.keys(route_helper.update).reduce((state, key)=>{ - if (['house'].indexOf(key) >= 0) state[key] = route_helper.update[key]; - return state; - }, {}); - } - - - // compare house state to updates or params. - routeUpdated(){ - var route_helper = this, - house = route_helper.house; - return (route_helper.energySelected() && !house.matchesYearState(route_helper.date_params)) || - (route_helper.powerSelected() && !house.matchesMonthState(route_helper.date_params) || !house.matchesPowerRange(route_helper.power_range)) && - (!route_helper.update.view || route_helper.update.view !== route_helper.view); - } - - // This will update the house state acccording to passed update parameters. - updateHouseState(){ - var route_helper = this, - house = route_helper.house; - house.setMonthState(route_helper.date_params, route_helper.update.power_range); - if (route_helper.energySelected()){ - route_helper.router.push({state: {loading_energy_data: true}}) - return house.setEnergyData().then(()=>{ return {loading_energy_data: false} }); - } else if (route_helper.powerSelected()) { - route_helper.router.push({state: {loading_power_data: true}}) - return house.setPowerData().then(()=>{ return {loading_power_data: false} }); - } else return Promise.resolve({}); - } - - updateRoute(){ - var route_helper = this; - return route_helper.updateHouseState() - .then((data_state)=>{ - route_helper.router.push({ - pathname: route_helper.newRoute(), - query: route_helper.newQuery(), - state: Object.assign(data_state, route_helper.new_state) - }); - }); - } - - // should be run AFTER updateHouseState is called. - newRoute(){ - var route_helper = this, - house = route_helper.house; - if (route_helper.dataset === 'energy'){ - return `/houses/${house.data.id}/energy/${house.state.year}/${route_helper.graph_attr}/${route_helper.view}`; - } else { - return `/houses/${house.data.id}/power/${house.state.month}/${house.state.year}/${route_helper.view}`; - } - } - - newQuery(){ - var route_helper = this; - if (route_helper.dataset === 'power') return {dates: route_helper.house.state.power_range}; - else return {}; - } - - graphSelected(){ - return RouteHelper.graphSelected(this.props.routes); - } - - static graphSelected(routes){ - if (RouteHelper.energySelected(routes)){ - return ArrayUtil.any(routes, (route)=>{ return route.component === EnergyGraph; }); - } else if (RouteHelper.powerSelected(routes)){ - return ArrayUtil.any(routes, (route)=>{ return route.component === PowerGraph; }); - } - return false; - } - - tableSelected(){ - return RouteHelper.tableSelected(this.props.routes); - } - - static tableSelected(routes){ - if (RouteHelper.energySelected(routes)){ - return ArrayUtil.any(routes, (route)=>{ return route.component === EnergyTable; }); - } else if (RouteHelper.powerSelected(routes)){ - return ArrayUtil.any(routes, (route)=>{ return route.component === PowerTable; }); - } - return false; - } - - energySelected(){ - return RouteHelper.energySelected(this.props.routes); - } - - static energySelected(routes){ - return ArrayUtil.any(routes, (route)=>{ return route.component === Energy; }); - } - - powerSelected(){ - return RouteHelper.powerSelected(this.props.routes); - } - - static powerSelected(routes){ - return ArrayUtil.any(routes, (route)=>{ return route.component === Power; }); - } - - -} - diff --git a/client/dashboard/state_manager.js b/client/dashboard/state_manager.js new file mode 100644 index 0000000..56daa01 --- /dev/null +++ b/client/dashboard/state_manager.js @@ -0,0 +1,174 @@ +import query_string from 'query-string'; + +import ObjectUtil from './../../shared/utils/object'; +import ArrayUtil from './../../shared/utils/array'; + +const ROUTES = [ + { + path: /houses\/(\d+)\/?$/, + parameters: {1: 'house_id'} + }, { + path: /houses\/(\d+)\/(energy)\/(\d+)\/([^\/]+)\/([^\/]+)\/?$/, + parameters: { 1: 'house_id', 2: 'dataset', 3: 'year', 4: 'graph_attr', 5: 'view' } + }, { + path: /houses\/(\d+)\/(power)\/([^\/]+)\/(\d+)\/([^\/]+)\/?$/, + parameters: { 1: 'house_id', 2: 'dataset', 3: 'month', 4: 'year', 5: 'view' } + } +]; + +class StateManager { + + constructor(createHistory, houses){ + var state_manager = this; + + state_manager.houses = houses; + + state_manager.state = { + loading_energy_data: false, + loading_power_data: false, + graph_attr: 'consumption', + view: 'graph', + dataset: 'power', + house_id: null, + house: null, + month: null, + year: null, + power_range: null }; + + state_manager.history = createHistory(); + state_manager.update_in_progress = false; + } + + get date_params(){ + return ObjectUtil.filterKeys(this.state, ['year', 'month', 'power_range']); + } + + // This will update the house state acccording to passed update parameters. + updateHouseFromState(component){ + var state_manager = this, + house = state_manager.state.house, + promise; + if (!house) { + promise = Promise.resolve(); + } else if (state_manager.state.dataset === 'energy' && (!house.energy_data || !house.matchesEnergyState(state_manager.state))){ + house.setMonthState(state_manager.state); + promise = state_manager.setHouseEnergyFromState(component); + } else if (state_manager.state.dataset === 'power' && !house.power_data || !house.matchesPowerState(state_manager.state)){ + house.setMonthState(state_manager.state); + promise = state_manager.setHousePowerFromState(component); + } else { + promise = new Promise((fnResolve, fnReject)=>{ + component.syncFromStateManager(fnResolve); + }); + } + return promise.then(()=>{ state_manager.update_in_progress = false; }) + } + + setHouseEnergyFromState(component){ + var state_manager = this; + state_manager.power_data_updated = true; + return new Promise((fnResolve, fnReject)=>{ + component.setState({ + loading_energy_data: true + }, ()=>{ + state_manager.state.house.setEnergyData() + .then(()=>{ + component.syncFromStateManager(fnResolve); + }); + }); + }); + } + + powerDataRendered(){ + var state_manager = this; + state_manager.power_data_updated = false; + } + + setHousePowerFromState(component){ + var state_manager = this, + house = state_manager.state.house; + return new Promise((fnResolve, fnReject)=>{ + component.setState({ + loading_power_data: true + }, ()=>{ + house.setPowerData() + .then(()=>{ + component.syncFromStateManager(fnResolve); + }); + }); + }); + } + + /* + * Change Params -> Change Url + */ + + setParams(params){ + var state_manager = this, + url; + if (state_manager.update_in_progress) return false; + state_manager.update_in_progress = true; + params = Object.assign({}, state_manager.state, params); + if (!params.house_id){ + url = '/'; + } else { + var house = state_manager.houses.find((h)=>{ return h.data.id == params.house_id; }) + + house.verifyMonthState(params); + if (params.dataset === 'energy'){ + url = `/houses/${params.house_id}/energy/${params.year}/${params.graph_attr}/${params.view}`; + } else if (params.dataset === 'power'){ + house.verifyPowerRange(params); + url = `/houses/${params.house_id}/power/${params.month}/${params.year}/${params.view}?${query_string.stringify({dates: params.power_range})}`; + } else { + url = `/houses/${house.house_id}`; + } + } + state_manager.history.push(url); + } + + /* + * Url Changed -> Change State + */ + + updateStateFromUrl(location, component){ + var state_manager = this; + return new Promise((fnResolve, fnReject)=>{ + var params = state_manager.parseUrl(location.pathname), + house = null; + if (params.dataset === 'power' && location.query.dates) { + params.power_range = [+location.query.dates[0], +location.query.dates[1]]; + } + if (params.house_id || params.house_id != state_manager.state.house_id){ + house = state_manager.houses.find((h)=>{ return h.data.id == params.house_id; }); + } + state_manager.state.house = house; + Object.assign(state_manager.state, params); + if (state_manager.state.house_id) { + state_manager.updateHouseFromState(component); + } else { + component.syncFromStateManager(()=>{ + state_manager.update_in_progress = false; + fnResolve(); + }); + } + }); + } + + parseUrl(url, query){ + for (var route of ROUTES){ + var match = url.match(route.path); + if (match){ + var parsed = {}; + for (var index in route.parameters){ + parsed[route.parameters[index]] = match[index]; + } + return parsed; + } + } + return {}; + } + +} + +export default StateManager; diff --git a/client/models/house.js b/client/models/house.js index 06ac46a..b059f15 100644 --- a/client/models/house.js +++ b/client/models/house.js @@ -53,11 +53,11 @@ class House { else return {}; } - availableMonths(){ + availableMonths(year){ var house = this, - all_months = moment.monthsShort(), - year = house.state.year.toString(); - if ((year) === house.data_from_moment.format('YYYY')){ + all_months = moment.monthsShort(); + year = year || house.state.year.toString(); + if (year === house.data_from_moment.format('YYYY')){ return all_months.slice(house.data_from_moment.month(), 12); } else if (year === house.data_until_moment.format('YYYY')){ return all_months.slice(0, house.data_until_moment.month() + 1); @@ -66,76 +66,89 @@ class House { } } - setMonthState(params, power_ranges){ - var house = this, - all_months = moment.monthsShort(); + // this will mutate params and set house.state. + setMonthState(params){ + var house = this; + house.verifyMonthState(params); + house.state.month = params.month; + house.state.year = params.year; - if (house.state.month !== params.month || house.state.year != params.year){ - var new_year = +params.year; - if (new_year >= house.data_from_moment.year() && new_year <= house.data_until_moment.year()){ - house.state.year = params.year; - } else if (!house.state.year){ - house.state.year = house.years[house.years.length - 1]; - } - var available_months = house.availableMonths(); - if (available_months.indexOf(params.month) >= 0){ - house.state.month = params.month; - } else if (!house.state.month || available_months.indexOf(house.state.month) < 0){ - house.state.month = available_months[available_months.length - 1]; - } - } - - var month_i = all_months.indexOf(house.state.month), + var month_i = moment.monthsShort().indexOf(house.state.month), new_month_moment = moment.tz({year: house.state.year, month: month_i, day: 1}, house.data.timezone).startOf('month'); if (!house.state.current_month_moment || new_month_moment.unix() !== house.state.current_month_moment.unix()){ house.state.current_month_moment = new_month_moment; + var end_of_month = new_month_moment.clone().endOf('month') + house.state.end_of_current_data_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month } - house.setDataRanges(power_ranges); + + house.verifyPowerRange(params); + house.state.power_range = params.power_range; + var energy_max = Math.min(house.state.end_of_current_data_moment.clone().endOf('year').unix(), house.data.data_until); + house.state.energy_range = [house.state.end_of_current_data_moment.clone().startOf('year').unix(), energy_max]; } - setDataRanges(power_ranges){ - var house = this, - end_of_month = house.state.current_month_moment.clone().endOf('month'), - end_of_current_data_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month, - energy_max = Math.min(end_of_current_data_moment.clone().endOf('year').unix(), house.data.data_until); - house.state.energy_range = [end_of_current_data_moment.clone().startOf('year').unix(), energy_max]; - house.state.power_range = house.state.power_range || []; - house.state.end_of_current_data_moment = end_of_current_data_moment; + // This will mutate params. + verifyMonthState(params){ + var house = this; - var current_data_range = [house.state.current_month_moment.unix(), end_of_current_data_moment.unix()], - power_min = house.state.power_range[0], - power_max = house.state.power_range[1]; - if (power_ranges){ - if (DateRange.inRange(power_ranges[1], current_data_range)){ - power_max = power_ranges[1]; + params.month = params.month || house.state.month; + params.year = params.year || house.state.year; + if (house.state.month !== params.month || house.state.year != params.year){ + var new_year = +params.year; + if (new_year < house.data_from_moment.year() && new_year > house.data_until_moment.year()){ + if (house.state.year) params.year = house.state.year; + else params.year = house.years[house.years.length - 1]; } - if (DateRange.inRange(power_ranges[0], current_data_range) && power_ranges[0] < power_max){ - power_min = power_ranges[0]; + + var available_months = house.availableMonths(params.year); + if (available_months.indexOf(params.month) < 0){ + if (house.state.month) params.month = house.state.month; + else params.month = available_months[available_months.length - 1]; + } + } + } + + // This will mutate params + verifyPowerRange(params){ + var house = this, + month_i = moment.monthsShort().indexOf(params.month), + month_moment = moment.tz({year: params.year, month: month_i, day: 1}, house.data.timezone).startOf('month'), + end_of_month = month_moment.clone().endOf('month'), + end_of_current_data_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month; + + params.power_range = params.power_range || []; + + var current_data_range = [month_moment.unix(), end_of_current_data_moment.unix()], + power_min = params.power_range[0], + power_max = params.power_range[1]; + if (params.power_range.length > 0){ + if (DateRange.inRange(params.power_range[1], current_data_range)){ + power_max = params.power_range[1]; + } + if (DateRange.inRange(params.power_range[0], current_data_range) && params.power_range[0] < power_max){ + power_min = params.power_range[0]; } } if (!power_max || !DateRange.inRange(power_max, current_data_range)){ power_max = end_of_current_data_moment.unix(); } - if (!power_min || !DateRange.inRange(power_min, current_data_range) || - power_max - power_min > 3600 * 24 * 4){ + if (!power_min || + !DateRange.inRange(power_min, current_data_range) || + power_max - power_min > 3600 * 24 * 4){ power_min = power_max - 3600 * 24 * 4; } - house.state.power_range = [power_min, power_max]; + params.power_range = [power_min, power_max]; } - matchesYearState(params){ + matchesEnergyState(params){ var house = this; return params.year == house.state.year; } - matchesMonthState(params){ + matchesPowerState(params){ var house = this; - return params.month == house.state.month && params.year == house.state.year; - } - - matchesPowerRange(params, dates){ - var house = this; - return house.matchesMonthState(params) && house.state.power_range[0] == dates[0] && house.state.power_range[1] == dates[1]; + return params.month === house.state.month && params.year == house.state.year && + house.state.power_range[0] == params.power_range[0] && house.state.power_range[1] == params.power_range[1]; } offset_diff(unix){ diff --git a/npm-debug.log b/npm-debug.log new file mode 100644 index 0000000..22fe718 --- /dev/null +++ b/npm-debug.log @@ -0,0 +1,45 @@ +0 info it worked if it ends with ok +1 verbose cli [ '/usr/local/bin/node', '/usr/local/bin/npm', 'start' ] +2 info using npm@3.5.3 +3 info using node@v5.4.1 +4 verbose run-script [ 'prestart', 'start', 'poststart' ] +5 info lifecycle spike_proto@0.0.0~prestart: spike_proto@0.0.0 +6 silly lifecycle spike_proto@0.0.0~prestart: no script for prestart, continuing +7 info lifecycle spike_proto@0.0.0~start: spike_proto@0.0.0 +8 verbose lifecycle spike_proto@0.0.0~start: unsafe-perm in lifecycle true +9 verbose lifecycle spike_proto@0.0.0~start: PATH: /usr/local/lib/node_modules/npm/bin/node-gyp-bin:/home/eric/Code/spike2/node_modules/.bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@oroeco_dev/bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@global/bin:/home/eric/.rvm/rubies/ruby-1.9.3-p484/bin:/home/eric/.rvm/bin:/home/eric/bin:/usr/local/heroku/bin:/home/eric/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/opt/lampp/bin +10 verbose lifecycle spike_proto@0.0.0~start: CWD: /home/eric/Code/spike2 +11 silly lifecycle spike_proto@0.0.0~start: Args: [ '-c', 'babel-node ./server/app.express.js' ] +12 silly lifecycle spike_proto@0.0.0~start: Returned: code: 1 signal: null +13 info lifecycle spike_proto@0.0.0~start: Failed to exec start script +14 verbose stack Error: spike_proto@0.0.0 start: `babel-node ./server/app.express.js` +14 verbose stack Exit status 1 +14 verbose stack at EventEmitter. (/usr/local/lib/node_modules/npm/lib/utils/lifecycle.js:232:16) +14 verbose stack at emitTwo (events.js:87:13) +14 verbose stack at EventEmitter.emit (events.js:172:7) +14 verbose stack at ChildProcess. (/usr/local/lib/node_modules/npm/lib/utils/spawn.js:24:14) +14 verbose stack at emitTwo (events.js:87:13) +14 verbose stack at ChildProcess.emit (events.js:172:7) +14 verbose stack at maybeClose (internal/child_process.js:821:16) +14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:211:5) +15 verbose pkgid spike_proto@0.0.0 +16 verbose cwd /home/eric/Code/spike2 +17 error Linux 3.19.0-51-generic +18 error argv "/usr/local/bin/node" "/usr/local/bin/npm" "start" +19 error node v5.4.1 +20 error npm v3.5.3 +21 error code ELIFECYCLE +22 error spike_proto@0.0.0 start: `babel-node ./server/app.express.js` +22 error Exit status 1 +23 error Failed at the spike_proto@0.0.0 start script 'babel-node ./server/app.express.js'. +23 error Make sure you have the latest version of node.js and npm installed. +23 error If you do, this is most likely a problem with the spike_proto package, +23 error not with npm itself. +23 error Tell the author that this fails on your system: +23 error babel-node ./server/app.express.js +23 error You can get information on how to open an issue for this project with: +23 error npm bugs spike_proto +23 error Or if that isn't available, you can get their info via: +23 error npm owner ls spike_proto +23 error There is likely additional logging output above. +24 verbose exit [ 1, true ] diff --git a/package.json b/package.json index 2f45bff..6bf1113 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "express": "4.13.3", "react": "0.14.3", "react-dom": "0.14.3", - "react-router": "2.0.0", + "query-string": "^3.0.0", + "history": "^2.0.0", "webpack": "1.12.9", "webpack-dev-server": "1.14.0", "extract-text-webpack-plugin": "1.0.1", diff --git a/shared/utils/object.js b/shared/utils/object.js new file mode 100644 index 0000000..f40feee --- /dev/null +++ b/shared/utils/object.js @@ -0,0 +1,12 @@ +class ObjectUtil { + + static filterKeys(obj, keys){ + return Object.keys(obj).reduce((filtered, key)=>{ + if (keys.indexOf(key) >= 0) filtered[key] = obj[key] + return filtered; + }, {}); + } + +} + +export default ObjectUtil;
{power_datum.time_to_s} {power_datum.consumption_to_s}