implement reactjs history

This commit is contained in:
Eric Hulburd
2016-03-04 13:59:34 -06:00
parent 5b218f6518
commit aa885f331c
27 changed files with 549 additions and 546 deletions

View File

@@ -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')
);
};

View File

@@ -1,4 +1,5 @@
import {browserHistory} from 'react-router';
import { createHistory, useQueries } from 'history'
import app from './../../app';
app(browserHistory);
app(useQueries(createHistory));

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,18 +0,0 @@
<div id="about">
<div class="panel panel-default">
<div class="panel-heading">About</div>
<div class="panel-body">
<p>This is a Spike bundle prototype using the following lirbaries:</p>
<ul>
<li>React</li>
<li>React Templates</li>
<li>React Router</li>
<li>LokiJs - persisting API calls to indexedDb</li>
<li>Webpack - hot mode developing and app bundling</li>
<li>Babel - ES6 transpiler</li>
</ul>
<p>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.</p>
<p>Select a house below to get started.</p>
</div>
</div>
</div>

View File

@@ -1,3 +0,0 @@
#about {
}

View File

@@ -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;

View File

@@ -1,5 +1,7 @@
<rt-require dependency="./graph/graph.component" as="EnergyGraph"/>
<rt-require dependency="./table/table.component" as="EnergyTable"/>
<div id="energy_view">
<div class="alert alert-warning" rt-if="this.props.location.state.loading_energy_data">
<div class="alert alert-warning" rt-if="this.loading_energy_data">
Retrieving energy data...
</div>
<div rt-if="this.props.view === 'graph'">
@@ -8,16 +10,28 @@
<button
data-param="graph_attr"
data-value="consumption"
rt-class="{active: this.props.params.graph_attr === 'consumption'}"
onClick="{this.setAttr}"
rt-class="{active: this.props.graph_attr === 'consumption'}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Consumption</button>
<button
data-param="graph_attr"
data-value="production"
rt-class="{active: this.props.params.graph_attr === 'production'}"
onClick="{this.setGraphAttr}"
rt-class="{active: this.props.graph_attr === 'production'}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Production</button>
</div>
</div>
{this.props.children}
<EnergyGraph
rt-if="this.props.view === 'graph'"
house="{this.props.house}"
state_manager="{this.props.state_manager}"
graph_attr="{this.props.graph_attr}"
year="{this.props.year}" ></EnergyGraph>
<EnergyTable
rt-if="this.props.view === 'table'"
state_manager="{this.props.state_manager}"
house="{this.props.house}"
graph_attr="{this.props.graph_attr}"
year="{this.props.year}" ></EnergyTable>
</div>

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
<table id="energy_table" rt-if="this.house" class="table">
<table id="energy_table" class="table">
<thead>
<tr>
<th></th>

View File

@@ -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;

View File

@@ -1,49 +0,0 @@
<div id="house">
<h4>Select dataset:</h4>
<div class="btn-group" role="group">
<button
data-param="dataset"
data-value="energy"
rt-class="{active: this.energySelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Daily Energy Statistics</button>
<button
data-param="dataset"
data-value="power"
rt-class="{active: this.powerSelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">15-minute Power Statistics</button>
</div>
<h4>View as:</h4>
<div class="btn-group" role="group">
<button
data-param="view"
data-value="graph"
rt-class="{active: this.graphSelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Graph</button>
<button
data-param="view"
data-value="table"
rt-class="{active: this.tableSelected()}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Table</button>
</div>
<div rt-if="this.house">
<h4>Select dates:</h4>
<div class="btn-group">
<button
rt-repeat="year in this.house.years"
data-param="year"
data-value="{year}"
key="data-year-{year}"
class="btn-info btn btn-sm"
rt-class="{active: year == this.house.state.year}"
onClick="{this.setParam.bind(this)}">{year}</button>
</div>
</div><br/>
{this.props.children}
</div>

View File

@@ -1 +0,0 @@
#house {}

View File

@@ -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;

View File

@@ -1,11 +1,97 @@
<rt-require dependency="./../energy/energy.component" as="EnergyComponent"/>
<rt-require dependency="./../power/power.component" as="PowerComponent"/>
<div id="layout">
<div class="alert alert-warning" rt-if="this.state.loading_house_data">Retrieving houses...</div>
<div rt-if="!this.house" id="about">
<div class="panel panel-default">
<div class="panel-heading">About</div>
<div class="panel-body">
<p>This is a Spike bundle prototype using the following libraries:</p>
<ul>
<li>React</li>
<li>React Templates</li>
<li>React Router</li>
<li>LokiJs - persisting API calls to indexedDb</li>
<li>Webpack - hot mode developing and app bundling</li>
<li>Babel - ES6 transpiler</li>
</ul>
<p>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.</p>
<p>Select a house below to get started.</p>
</div>
</div>
</div>
<div class="alert alert-warning" rt-if="this.state.loading_houses">Retrieving houses...</div>
<h4>Select household:</h4>
<select id="houses_select" rt-if="this.state.houses" class="form-control" onChange="{this.setHouse.bind(this)}" value="{this.props.params.house_id}">
<select id="houses_select" rt-if="this.state.houses && this.state_manager" class="form-control" onChange="{this.setHouse.bind(this)}" value="{this.house_id}">
<option rt-repeat="house in this.state.houses" value="{house.data.id}" key="{house.scoped_id}">{house.data.name}</option>
</select>
<button rt-if="this.house" onClick="{this.refreshData.bind(this)}" class="btn btn-xs btn-default">Refresh House Data</button>
{this.props.children}
<div rt-if="this.house">
<h4>Select dataset:</h4>
<div class="btn-group" role="group">
<button
data-param="dataset"
data-value="energy"
rt-class="{active: this.state.dataset === 'energy'}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Daily Energy Statistics</button>
<button
data-param="dataset"
data-value="power"
rt-class="{active: this.state.dataset === 'power'}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">15-minute Power Statistics</button>
</div>
<h4>View as:</h4>
<div class="btn-group" role="group">
<button
data-param="view"
data-value="graph"
rt-class="{active: this.state.view === 'graph'}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Graph</button>
<button
data-param="view"
data-value="table"
rt-class="{active: this.state.view === 'table'}"
onClick="{this.setParam.bind(this)}"
type="button" class="btn btn-primary">Table</button>
</div>
<div rt-if="this.house">
<h4>Select dates:</h4>
<div class="btn-group">
<button
rt-repeat="year in this.house.years"
data-param="year"
data-value="{year}"
key="data-year-{year}"
class="btn-info btn btn-sm"
rt-class="{active: year == this.state.year}"
onClick="{this.setParam.bind(this)}">{year}</button>
</div>
</div><br/>
<EnergyComponent
rt-if="this.state.dataset === 'energy'"
house="{this.state.house}"
loading_energy_data="{this.state.loading_energy_data}"
state_manager="{this.state_manager}"
view="{this.state.view}"
graph_attr="{this.state.graph_attr}"
year="{this.state.year}" />
<PowerComponent
rt-if="this.state.dataset === 'power'"
house="{this.state.house}"
loading_power_data="{this.state.loading_power_data}"
state_manager="{this.state_manager}"
view="{this.state.view}"
month="{this.state.month}"
year="{this.state.year}"
power_range="{this.state.power_range}" />
</div>
</div>

View File

@@ -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;

View File

@@ -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

View File

@@ -1,3 +1,5 @@
<rt-require dependency="./graph/graph.component" as="PowerGraph"/>
<rt-require dependency="./table/table.component" as="PowerTable"/>
<div id="power_view">
<div class="btn-group">
<button
@@ -10,9 +12,22 @@
rt-class="{active: month === this.house.state.month}"
onClick="{this.setParam.bind(this)}">{month}</button>
</div>
<div class="alert alert-warning" rt-if="this.props.location.state.loading_power_data">
<div class="alert alert-warning" rt-if="this.loading_power_data">
Retrieving power data...
</div>
<div id="power_date_setter"></div>
{this.props.children}
<PowerGraph
rt-if="this.props.view === 'graph'"
state_manager="{this.props.state_manager}"
house="{this.props.house}"
month="{this.props.month}"
year="{this.props.year}"
power_range="{this.props.power_range}" ></PowerGraph>
<PowerTable
rt-if="this.props.view === 'table'"
state_manager="{this.props.state_manager}"
house="{this.props.house}"
month="{this.props.month}"
year="{this.props.year}"
power_range="{this.props.power_range}" ></PowerTable>
</div>

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
<table id="power_table" class="table" rt-if="this.context.house">
<table id="power_table" class="table">
<thead>
<tr>
<th></th>
@@ -8,7 +8,7 @@
</tr>
</thead>
<tbody>
<tr rt-repeat="power_datum in this.context.house.power_data" key="{power_datum.scoped_id}">
<tr rt-repeat="power_datum in this.house.power_data" key="{power_datum.scoped_id}">
<td></td>
<td>{power_datum.time_to_s}</td>
<td>{power_datum.consumption_to_s}</td>

View File

@@ -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; });
}
}

View File

@@ -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;

View File

@@ -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){

45
npm-debug.log Normal file
View File

@@ -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.<anonymous> (/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.<anonymous> (/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 ]

View File

@@ -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",

12
shared/utils/object.js Normal file
View File

@@ -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;