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 @@ -
This is a Spike bundle prototype using the following lirbaries:
-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/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 @@ - |
|---|
| @@ -8,7 +8,7 @@ | ||
|---|---|---|
| {power_datum.time_to_s} | {power_datum.consumption_to_s} | 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.