commit
a23bef70db
29 changed files with 2053 additions and 1096 deletions
3
.babelrc
3
.babelrc
|
|
@ -2,4 +2,7 @@
|
|||
"presets": [
|
||||
"env", "react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties"
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ module.exports = {
|
|||
"no-floating-decimal": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-invalid-this": "error",
|
||||
"no-iterator": "error",
|
||||
"no-loop-func": "error",
|
||||
"no-multi-spaces": "warn",
|
||||
|
|
@ -126,6 +125,11 @@ module.exports = {
|
|||
"space-before-blocks": "error",
|
||||
"quotes": ["error", "double"],
|
||||
|
||||
// ES6
|
||||
"arrow-spacing": "error",
|
||||
"arrow-parens": "warn",
|
||||
"no-confusing-arrow": ["error", {"allowParens": true}],
|
||||
|
||||
// react
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,5 @@ types/types.js
|
|||
|
||||
[options]
|
||||
esproposal.export_star_as=enable
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[lints]
|
||||
all=warn
|
||||
module.system.node.resolve_dirname=node_modules
|
||||
module.system.node.resolve_dirname=src
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,2 @@
|
|||
node_modules/
|
||||
public/
|
||||
dist/
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 7
|
||||
- 6
|
||||
- 8
|
||||
- 9
|
||||
script:
|
||||
- yarn travis
|
||||
cache:
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ const config : Config = {
|
|||
command: "/service/onkyo/command",
|
||||
defaultValue: "",
|
||||
values: { mpd: "NPR01", kohina: "NPR02", somafm_dronezone: "NPR03", somafm_thetrip: "NPR04",
|
||||
querfunk: "NPR05", somafm_defconradio: "NPR06", somafm_secretagent: "NPR07", somafm_lush: "NPR08"}
|
||||
querfunk: "NPR05", somafm_defconradio: "NPR06", somafm_secretagent: "NPR07", somafm_lush: "NPR08",
|
||||
somafm_beatblender: "NPR09"}
|
||||
},
|
||||
rundumleuchte: {
|
||||
state: "/service/openhab/out/pca301_rundumleuchte/state",
|
||||
|
|
@ -121,9 +122,9 @@ const config : Config = {
|
|||
controls: {
|
||||
led_stahltrager: {
|
||||
name: "LED Stahlträger",
|
||||
position: [380, 300],
|
||||
icon: "white-balance-iridescent mdi-rotate-90",
|
||||
iconColor: state => state.led_stahltraeger == "on" ? utils.rainbow : "#000000",
|
||||
position: [380, 590],
|
||||
icon: "white-balance-iridescent",
|
||||
iconColor: ({led_stahltraeger}) => led_stahltraeger == "on" ? utils.rainbow : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -137,7 +138,7 @@ const config : Config = {
|
|||
name: "Snackbar",
|
||||
position: [510, 500],
|
||||
icon: "fridge",
|
||||
iconColor: state => state.snackbar == "on" ? "#E20074" : "#000000",
|
||||
iconColor: ({snackbar}) => snackbar == "on" ? "#E20074" : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -150,8 +151,8 @@ const config : Config = {
|
|||
twinkle: {
|
||||
name: "Twinkle",
|
||||
position: [530, 560],
|
||||
icon: "led-off mdi-flip-v",
|
||||
iconColor: state => state.twinkle == "on" ? utils.rainbow : "#000000",
|
||||
icon: ({twinkle}) => twinkle == "on" ? "led-on flip-v" : "led-off flip-v",
|
||||
iconColor: ({twinkle}) => twinkle == "on" ? utils.rainbow : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -165,7 +166,7 @@ const config : Config = {
|
|||
name: "Ventilator",
|
||||
position: [520, 450],
|
||||
icon: "fan",
|
||||
iconColor: state => state.fan == "on" ? "#00FF00" : "#000000",
|
||||
iconColor: ({fan}) => fan == "on" ? "#00FF00" : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -191,7 +192,7 @@ const config : Config = {
|
|||
name: "Videospiele",
|
||||
position: [100, 100],
|
||||
icon: "gamepad-variant",
|
||||
iconColor: state => state.videogames == "on" ? "#00FF00" : "#000000",
|
||||
iconColor: ({videogames}) => videogames == "on" ? "#00FF00" : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -205,7 +206,7 @@ const config : Config = {
|
|||
name: "Rechner und Drucker",
|
||||
position: [297, 90],
|
||||
icon: "desktop-classic",
|
||||
iconColor: state => state.olymp_pc == "on" ? "#00FF00" : "#000000",
|
||||
iconColor: ({olymp_pc}) => olymp_pc == "on" ? "#00FF00" : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -219,7 +220,7 @@ const config : Config = {
|
|||
name: "Fliegenbratgerät",
|
||||
position: [450, 590],
|
||||
icon: "fire",
|
||||
iconColor: state => state.flyfry == "on" ? "#6666FF" : "#000000",
|
||||
iconColor: ({flyfry}) => flyfry == "on" ? "#6666FF" : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -233,15 +234,15 @@ const config : Config = {
|
|||
name: "Artnet",
|
||||
position: [535,480],
|
||||
icon: "spotlight",
|
||||
iconColor: state =>
|
||||
iconColor: ({artnet}) =>
|
||||
({
|
||||
off: "#000000",
|
||||
yellow: "#CCCC00",
|
||||
yellow: "#F0DF10",
|
||||
red: "#FF0000",
|
||||
purple: "#FF00FF",
|
||||
green: "#00FF00",
|
||||
cycle: utils.rainbow
|
||||
})[state.artnet],
|
||||
})[artnet],
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -270,8 +271,8 @@ const config : Config = {
|
|||
onkyo: {
|
||||
name: "Onkyo",
|
||||
position: [350, 650],
|
||||
iconColor: state =>
|
||||
state.onkyo_connection != "connected" ? "#888888" : (state.onkyo_power == "on" ? "#00FF00" : "#000000"),
|
||||
iconColor: ({onkyo_connection, onkyo_power}) =>
|
||||
onkyo_connection != "connected" ? "#888888" : (onkyo_power == "on" ? "#00FF00" : "#000000"),
|
||||
icon: "volume-high",
|
||||
ui: [
|
||||
{
|
||||
|
|
@ -279,7 +280,7 @@ const config : Config = {
|
|||
text: "Power",
|
||||
icon: "power",
|
||||
topic: "onkyo_power",
|
||||
enableCondition: (a, b, state) => state.onkyo_connection == "connected"
|
||||
enableCondition: (a, b, state) => state.onkyo_connection.internal == "connected"
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
|
|
@ -292,14 +293,14 @@ const config : Config = {
|
|||
min: 0,
|
||||
max: 50,
|
||||
icon: "volume-high",
|
||||
enableCondition: (a, b, state) => state.onkyo_connection == "connected"
|
||||
enableCondition: (a, b, state) => state.onkyo_connection.internal == "connected"
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Mute",
|
||||
topic: "onkyo_mute",
|
||||
icon: "volume-off",
|
||||
enableCondition: (a, b, state) => state.onkyo_connection == "connected"
|
||||
enableCondition: (a, b, state) => state.onkyo_connection.internal == "connected"
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
|
|
@ -316,7 +317,7 @@ const config : Config = {
|
|||
pult: "Pult"
|
||||
},
|
||||
icon: "usb",
|
||||
enableCondition: (a, b, state) => state.onkyo_connection == "connected"
|
||||
enableCondition: (a, b, state) => state.onkyo_connection.internal == "connected"
|
||||
},
|
||||
{
|
||||
type: "dropDown",
|
||||
|
|
@ -330,10 +331,11 @@ const config : Config = {
|
|||
querfunk: "Querfunk",
|
||||
somafm_defconradio: "Defcon Radio (SomaFM)",
|
||||
somafm_secretagent: "Secret Agent (SomaFM)",
|
||||
somafm_lush: "Lush (SomaFM)"
|
||||
somafm_lush: "Lush (SomaFM)",
|
||||
somafm_beatblender: "Beat Blender (Soma FM)"
|
||||
},
|
||||
icon: "radio",
|
||||
enableCondition: (a, b, state) => state.onkyo_connection == "connected" && state.onkyo_inputs == "netzwerk"
|
||||
enableCondition: (a, b, state) => state.onkyo_connection.internal == "connected" && state.onkyo_inputs.internal == "netzwerk"
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
|
|
@ -350,7 +352,7 @@ const config : Config = {
|
|||
name: "Rundumleuchte",
|
||||
position: [310,275],
|
||||
icon: "alarm-light",
|
||||
iconColor: state => state.rundumleuchte == "on" ? "#CCCC00" : "#000000",
|
||||
iconColor: ({rundumleuchte}) => rundumleuchte == "on" ? "#F0DF10" : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -364,7 +366,7 @@ const config : Config = {
|
|||
name: "Tür",
|
||||
position: [455,350],
|
||||
icon: "swap-vertical",
|
||||
iconColor: state => state.door_status == "on" ? "#00FF00" : "#FF0000",
|
||||
iconColor: ({door_status}) => door_status == "on" ? "#00FF00" : "#FF0000",
|
||||
ui: [
|
||||
{
|
||||
type: "link",
|
||||
|
|
@ -377,7 +379,7 @@ const config : Config = {
|
|||
name: "Infoscreen",
|
||||
position: [255, 495],
|
||||
icon: "developer-board",
|
||||
iconColor: state => state.infoscreen == "on" ? "#4444FF" : "#000000",
|
||||
iconColor: ({infoscreen}) => infoscreen == "on" ? "#4444FF" : "#000000",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
|
|
@ -399,18 +401,30 @@ const config : Config = {
|
|||
baseLayer: true,
|
||||
name: "RaumZeitLabor",
|
||||
defaultVisibility: "visible",
|
||||
opacity: 0.7
|
||||
opacity: 0.7,
|
||||
bounds: {
|
||||
topLeft: [0, 0],
|
||||
bottomRight: [1000, 700]
|
||||
}
|
||||
},
|
||||
{
|
||||
image: require("../img/layers/rzl/details.svg"),
|
||||
name: "Details",
|
||||
defaultVisibility: "visible",
|
||||
opacity: 0.4
|
||||
opacity: 0.4,
|
||||
bounds: {
|
||||
topLeft: [0, 0],
|
||||
bottomRight: [1000, 700]
|
||||
}
|
||||
},
|
||||
{
|
||||
image: require("../img/layers/rzl/labels.svg"),
|
||||
name: "Labels",
|
||||
defaultVisibility: "visible"
|
||||
defaultVisibility: "visible",
|
||||
bounds: {
|
||||
topLeft: [0, 0],
|
||||
bottomRight: [1000, 700]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,29 @@
|
|||
// @flow
|
||||
|
||||
export const rainbow = "rgba(200,120,120,0.5);"
|
||||
+ "background: linear-gradient(40deg, #FF0000 0%, #00FF00 50%, #0000FF 70%, #FFFF00 100%);"
|
||||
+ "background-clip: text; -webkit-background-clip: text;";
|
||||
+ "--before-background: linear-gradient(40deg, #FF0000 0%, #00FF00 50%, #0000FF 70%, #FFFF00 100%);";
|
||||
|
||||
export const esper_topics = (chip_id: string) => ({
|
||||
[ `esper_${chip_id}_version` ]: {
|
||||
state: `/service/esper/${chip_id}/info`,
|
||||
command: "",
|
||||
defaultValue: "UNKNOWN",
|
||||
values: {},
|
||||
parseState: msg => JSON.parse(msg.toString()).version.esper
|
||||
}
|
||||
});
|
||||
|
||||
export const esper_statistics = (chip_id: string,
|
||||
prev_ui: Array<ControlUI> = []) => (
|
||||
prev_ui.concat([
|
||||
{
|
||||
type: "section",
|
||||
text: "Funkdose"
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Version",
|
||||
topic: `esper_${chip_id}_version`
|
||||
}
|
||||
])
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@ body .leaflet-div-icon {
|
|||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-marker-icon .mdi {
|
||||
line-height: 1;
|
||||
}
|
||||
#drawer_uiComponents .mdi {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.mdi:before {
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
background-image: var(--before-background, transparent);
|
||||
}
|
||||
|
|
|
|||
27
package.json
27
package.json
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "spacemap",
|
||||
"name": "mqtt-control-map",
|
||||
"version": "1.0.0",
|
||||
"author": "uwap <me+spacemap.package.json@uwap.name>",
|
||||
"author": "uwap <me+mqttmap.package.json@uwap.name>",
|
||||
"description": "control devices via mqtt on a beautiful map of your space",
|
||||
"scripts": {
|
||||
"build": "webpack --bail --config webpack.dev.js",
|
||||
|
|
@ -13,16 +13,16 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"leaflet": "^1.2.0",
|
||||
"material-ui": "next",
|
||||
"leaflet": "^1.3.1",
|
||||
"lodash": "^4.17.4",
|
||||
"material-ui": "npm:material-ui@next",
|
||||
"material-ui-old": "npm:material-ui@latest",
|
||||
"mdi": "^2.0.46",
|
||||
"mqtt": "^2.11.0",
|
||||
"ramda": "^0.24.1",
|
||||
"react": "^15.6.1",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-leaflet": "^1.5.0",
|
||||
"react-tap-event-plugin": "^2.0.1",
|
||||
"mqtt": "^2.14.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-leaflet": "^1.8.0",
|
||||
"react-tap-event-plugin": "^3.0.0",
|
||||
"redux": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -30,22 +30,23 @@
|
|||
"babel-core": "^6.25.0",
|
||||
"babel-eslint": "^8.0.1",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"clean-webpack-plugin": "^0.1.17",
|
||||
"css-loader": "^0.28.7",
|
||||
"css-loader": "^0.28.9",
|
||||
"eslint": "^4.10.0",
|
||||
"eslint-plugin-flowtype": "^2.39.1",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"flow": "^0.2.3",
|
||||
"flow-bin": "^0.50.0",
|
||||
"flow-bin": "^0.63.1",
|
||||
"flow-typed": "^2.2.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"husky": "^0.14.3",
|
||||
"style-loader": "^0.19.0",
|
||||
"webpack": "^3.1.0",
|
||||
"webpack-dev-server": "^2.9.4",
|
||||
"webpack-dev-server": "^2.11.0",
|
||||
"webpack-merge": "^4.1.1",
|
||||
"webpack-shell-plugin": "^0.5.0"
|
||||
},
|
||||
|
|
|
|||
149
src/UiItems.js
149
src/UiItems.js
|
|
@ -1,149 +0,0 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import Switch from "material-ui/Switch";
|
||||
import Select from "material-ui/Select";
|
||||
import { MenuItem } from "material-ui/Menu";
|
||||
import Slider from "material-ui-old/Slider";
|
||||
import MuiThemeProvider from "material-ui-old/styles/MuiThemeProvider";
|
||||
import Config from "./config";
|
||||
import Input, { InputLabel } from "material-ui/Input";
|
||||
import { FormControl } from "material-ui/Form";
|
||||
import R from "ramda";
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
ListSubheader
|
||||
} from "material-ui/List";
|
||||
import Button from "material-ui/Button";
|
||||
|
||||
const enabled = (props: ControlUI, state: State) => {
|
||||
if (props.enableCondition == null) {
|
||||
return true;
|
||||
} else {
|
||||
const val = state.values[props.topic];
|
||||
return props.enableCondition(
|
||||
val.internal == null ? val.actual : val.internal, val.actual,
|
||||
R.map(x => x.internal == null ? x.actual
|
||||
: x.internal, state.values == null ? {} : state.values));
|
||||
}
|
||||
};
|
||||
|
||||
const getValue = (topic: string, val: string) =>
|
||||
Config.topics[topic].values[val];
|
||||
|
||||
const renderIcon = (icon: string) => {
|
||||
if (icon != null) {
|
||||
return (
|
||||
<ListItemIcon>
|
||||
<i className={`mdi mdi-${icon} mdi-24px`}></i>
|
||||
</ListItemIcon>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const onSwitch = (topic: string, props: ControlUI, state: State) =>
|
||||
(x, toggled: boolean) => {
|
||||
if (state.mqtt != null) {
|
||||
state.mqtt.publish(Config.topics[topic].command,
|
||||
toggled ? getValue(topic, R.propOr("on", "on", props))
|
||||
: getValue(topic, R.propOr("off", "off", props)));
|
||||
}
|
||||
};
|
||||
|
||||
export const isToggled = (state: State, props: ControlUI) => {
|
||||
const val = state.values[props.topic];
|
||||
if (props.toggled != null) {
|
||||
return props.toggled(val.internal == null ? val.actual : val.internal,
|
||||
val.actual);
|
||||
} else {
|
||||
return val.internal === R.propOr("on", "on", props);
|
||||
}
|
||||
};
|
||||
|
||||
export const toggle = (state: State, props: ControlUI) => {
|
||||
return (
|
||||
<ListItem>
|
||||
{renderIcon(props.icon)}
|
||||
<ListItemText primary={props.text} />
|
||||
<ListItemSecondaryAction>
|
||||
<Switch label={props.text}
|
||||
checked={isToggled(state, props)}
|
||||
onChange={onSwitch(props.topic, props, state)}
|
||||
disabled={!(enabled(props, state))} />
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
const onDropDownChange = (topic: string, props: ControlUI, state: State) =>
|
||||
(event) => {
|
||||
if (state.mqtt != null) {
|
||||
state.mqtt.publish(Config.topics[topic].command, event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const dropDownItem = (topic: string) => (text: string, key: string) => (
|
||||
<MenuItem value={Config.topics[topic].values[key]} key={key}>{text}</MenuItem>
|
||||
);
|
||||
|
||||
export const dropDown = (state: State, props: ControlUI) => {
|
||||
const id = `${props.topic}.${Object.keys(props.options)
|
||||
.reduce((v, r) => v + "." + r)}`;
|
||||
return (
|
||||
<ListItem>
|
||||
{renderIcon(props.icon)}
|
||||
<FormControl>
|
||||
<InputLabel htmlFor={id}>{props.text}</InputLabel>
|
||||
<Select value={state.values[props.topic].actual}
|
||||
onChange={onDropDownChange(props.topic, props, state)}
|
||||
disabled={!(enabled(props, state))}
|
||||
input={<Input id={id} />}
|
||||
>
|
||||
{R.values(R.mapObjIndexed(dropDownItem(props.topic), props.options))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
const onSliderChange = (state: State, props: ControlUI) =>
|
||||
(event, value) => {
|
||||
if (state.mqtt != null) {
|
||||
state.mqtt.publish(Config.topics[props.topic].command, value.toString());
|
||||
}
|
||||
};
|
||||
|
||||
export const slider = (state: State, props: ControlUI) => (
|
||||
<ListItem>
|
||||
{renderIcon(props.icon)}
|
||||
<ListItemText primary={props.text} />
|
||||
<ListItemSecondaryAction>
|
||||
<MuiThemeProvider>
|
||||
<Slider value={state.values[props.topic].actual}
|
||||
min={props.min == null ? 0 : props.min}
|
||||
max={props.max == null ? 1 : props.max}
|
||||
step={props.step == null ? 1 : props.step}
|
||||
onChange={onSliderChange(state, props)}
|
||||
style={{width: 100}}
|
||||
/></MuiThemeProvider>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
export const section = (state: State, props: ControlUI) => (
|
||||
<ListSubheader>{props.text}</ListSubheader>
|
||||
);
|
||||
|
||||
export const link = (state: State, props: ControlUI) => (
|
||||
<ListItem>
|
||||
<Button raised
|
||||
onClick={() => window.open(props.link, "_blank")}
|
||||
color="primary"
|
||||
>
|
||||
{props.text}
|
||||
</Button>
|
||||
</ListItem>
|
||||
);
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import AppBar from "material-ui/AppBar";
|
||||
import Toolbar from "material-ui/Toolbar";
|
||||
import { CircularProgress } from "material-ui/Progress";
|
||||
import IconButton from "material-ui/IconButton";
|
||||
import Typography from "material-ui/Typography";
|
||||
|
||||
const TopBarLayerSelector = (_props: Object) => (
|
||||
<IconButton>
|
||||
<i className="mdi mdi-layers"></i>
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
const TopBarIndicatorMenu = (props: Object) => (
|
||||
<IconButton>
|
||||
{props.mqtt.connected ?
|
||||
(<i style={{fontSize: 48}} className="mdi mdi-map"></i>) :
|
||||
(<i style={{fontSize: 48}} className="mdi mdi-lan-disconnect"></i>)}
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
|
||||
const TopBarIndicator = (props: Object) => {
|
||||
if (props.mqtt == null || props.mqtt.reconnecting) {
|
||||
return (<CircularProgress size={48}
|
||||
style={{color: "rgba(0, 0, 0, 0.54)"}} />);
|
||||
} else {
|
||||
return (<TopBarIndicatorMenu {...props} />);
|
||||
}
|
||||
};
|
||||
|
||||
const TopBar = (props: Object) => (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<TopBarIndicator {...props} />
|
||||
<Typography type="title">{props.title}</Typography>
|
||||
{false && <TopBarLayerSelector {...props} />}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
||||
export default TopBar;
|
||||
138
src/components/App.js
Normal file
138
src/components/App.js
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
import MuiThemeProvider from "material-ui/styles/MuiThemeProvider";
|
||||
import createMuiTheme from "material-ui/styles/createMuiTheme";
|
||||
import withStyles from "material-ui/styles/withStyles";
|
||||
import * as Colors from "material-ui/colors";
|
||||
|
||||
import SideBar from "components/SideBar";
|
||||
import ControlMap from "components/ControlMap";
|
||||
import TopBar from "components/TopBar";
|
||||
import UiItemList from "components/UiItemList";
|
||||
|
||||
import keyOf from "utils/keyOf";
|
||||
import { controlGetIcon } from "utils/parseIconName";
|
||||
|
||||
import connectMqtt from "../connectMqtt";
|
||||
|
||||
export type AppProps = {
|
||||
config: Config
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
selectedControl: ?Control,
|
||||
drawerOpened: boolean,
|
||||
mqttState: State,
|
||||
mqttSend: (topic: string, value: any) => void,
|
||||
mqttConnected: boolean,
|
||||
};
|
||||
|
||||
class App extends React.Component<AppProps & Classes, AppState> {
|
||||
constructor(props: AppProps & Classes) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedControl: null,
|
||||
drawerOpened: false,
|
||||
mqttState: _.mapValues(props.config.topics, (topic) => ({
|
||||
actual: topic.defaultValue,
|
||||
internal: keyOf(topic.values, topic.defaultValue)
|
||||
})),
|
||||
mqttSend: connectMqtt(props.config.space.mqtt, {
|
||||
onMessage: this.receiveMessage.bind(this),
|
||||
onConnect: () => this.setState({ mqttConnected: true }),
|
||||
onReconnect: () => this.setState({ mqttConnected: false }),
|
||||
onDisconnect: () => this.setState({ mqttConnected: false }),
|
||||
subscribe: _.map(props.config.topics, (x) => x.state)
|
||||
}),
|
||||
mqttConnected: false
|
||||
};
|
||||
}
|
||||
|
||||
static styles(_theme: Object) {
|
||||
return {
|
||||
drawerPaper: {
|
||||
width: 320
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get theme() {
|
||||
return createMuiTheme({
|
||||
palette: {
|
||||
primary: Colors[this.props.config.space.color]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
receiveMessage(rawTopic: string, message: Object) {
|
||||
const topic = _.findKey(
|
||||
this.props.config.topics,
|
||||
(v) => v.state === rawTopic
|
||||
);
|
||||
if (topic == null) {
|
||||
return;
|
||||
}
|
||||
const parseValue = this.props.config.topics[topic].parseState;
|
||||
const value = parseValue == null ? message.toString() : parseValue(message);
|
||||
this.setState({mqttState: _.merge(this.state.mqttState,
|
||||
{ [topic]: {
|
||||
actual: value,
|
||||
internal: keyOf(this.props.config.topics[topic].values, value) || value
|
||||
}})});
|
||||
}
|
||||
|
||||
changeControl(control: ?Control = null) {
|
||||
this.setState({selectedControl: control, drawerOpened: control != null});
|
||||
}
|
||||
|
||||
closeDrawer() {
|
||||
this.setState({drawerOpened: false});
|
||||
}
|
||||
|
||||
changeState(topic: string, value: any) {
|
||||
const rawTopic = this.props.config.topics[topic].command;
|
||||
if (rawTopic == null) {
|
||||
return;
|
||||
}
|
||||
this.state.mqttSend(
|
||||
rawTopic,
|
||||
String(this.props.config.topics[topic].values[value] || value)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<MuiThemeProvider theme={this.theme}>
|
||||
<div>
|
||||
<TopBar title={`${this.props.config.space.name} Map`}
|
||||
connected={this.state.mqttConnected} />
|
||||
<SideBar open={this.state.drawerOpened}
|
||||
control={this.state.selectedControl}
|
||||
onCloseRequest={this.closeDrawer.bind(this)}
|
||||
icon={this.state.selectedControl == null ? null :
|
||||
controlGetIcon(this.state.selectedControl,
|
||||
this.state.mqttState)}
|
||||
>
|
||||
{this.state.selectedControl == null
|
||||
|| <UiItemList state={this.state.mqttState}
|
||||
controls={this.state.selectedControl.ui}
|
||||
onChangeState={this.changeState.bind(this)}
|
||||
/>}
|
||||
</SideBar>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
<ControlMap width={1000} height={700} zoom={0}
|
||||
layers={this.props.config.layers}
|
||||
controls={this.props.config.controls}
|
||||
onChangeControl={this.changeControl.bind(this)}
|
||||
state={this.state.mqttState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(App.styles)(App);
|
||||
109
src/components/ControlMap.js
Normal file
109
src/components/ControlMap.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import { Map, ImageOverlay, Marker, LayersControl } from "react-leaflet";
|
||||
import Leaflet from "leaflet";
|
||||
import _ from "lodash";
|
||||
import parseIconName, { controlGetIcon } from "utils/parseIconName";
|
||||
|
||||
export type Point = [number, number];
|
||||
|
||||
const convertPoint = ([y, x]: Point): Point => [-x, y];
|
||||
|
||||
export type ControlMapProps = {
|
||||
width: number,
|
||||
height: number,
|
||||
zoom: number,
|
||||
layers: Array<Layer>,
|
||||
controls: Controls,
|
||||
onChangeControl: (control: Control) => void,
|
||||
state: State
|
||||
};
|
||||
|
||||
export default class ControlMap extends React.Component<ControlMapProps> {
|
||||
constructor(props: ControlMapProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
get center(): Point {
|
||||
return convertPoint([
|
||||
this.props.width / 2,
|
||||
this.props.height / 2
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Map center={this.center}
|
||||
zoom={this.props.zoom}
|
||||
crs={Leaflet.CRS.Simple}>
|
||||
{this.renderMarkers()}
|
||||
{this.renderLayers()}
|
||||
</Map>
|
||||
);
|
||||
}
|
||||
|
||||
renderMarkers() {
|
||||
return _.map(this.props.controls, this.renderMarker.bind(this));
|
||||
}
|
||||
|
||||
createLeafletIcon(control: Control) {
|
||||
const icon = controlGetIcon(control, this.props.state);
|
||||
const iconClass = parseIconName(`${icon} 36px`);
|
||||
return Leaflet.divIcon({
|
||||
iconSize: Leaflet.point(36, 36),
|
||||
iconAnchor: Leaflet.point(18, 18),
|
||||
html: `<i class="${iconClass}"
|
||||
style="line-height: 1; color: ${this.iconColor(control)}"></i>`
|
||||
});
|
||||
}
|
||||
|
||||
iconColor(control: Control): string {
|
||||
const ints = _.mapValues(this.props.state, (x) => x.internal || x.actual);
|
||||
const acts = _.mapValues(this.props.state, (x) => x.actual);
|
||||
if (control.iconColor != null) {
|
||||
return control.iconColor(ints, acts, this.props.state);
|
||||
}
|
||||
return "#000";
|
||||
}
|
||||
|
||||
renderMarker(control: Control, key: string) {
|
||||
return (
|
||||
<Marker position={convertPoint(control.position)}
|
||||
key={key}
|
||||
icon={this.createLeafletIcon(control)}
|
||||
onClick={() => this.props.onChangeControl(control)}
|
||||
>
|
||||
</Marker>
|
||||
);
|
||||
}
|
||||
|
||||
renderLayers() {
|
||||
return (
|
||||
<LayersControl position="topright">
|
||||
{this.props.layers.map(this.renderLayer)}
|
||||
</LayersControl>
|
||||
);
|
||||
}
|
||||
|
||||
renderLayer(layer: Layer) {
|
||||
const LayersControlType =
|
||||
layer.baseLayer ? LayersControl.BaseLayer : LayersControl.Overlay;
|
||||
return (
|
||||
<LayersControlType
|
||||
key={layer.name}
|
||||
name={layer.name}
|
||||
checked={layer.defaultVisibility === "visible"}
|
||||
removeLayer={(_layer) => {}}
|
||||
removeLayerControl={(_layer) => {}}
|
||||
addOverlay={(_layer, _name, _checked) => {}}
|
||||
addBaseLayer={(_layer, _name, _checked) => {}}>
|
||||
<ImageOverlay url={layer.image}
|
||||
bounds={[
|
||||
convertPoint(layer.bounds.topLeft),
|
||||
convertPoint(layer.bounds.bottomRight)
|
||||
]}
|
||||
opacity={layer.opacity || 1} />
|
||||
</LayersControlType>
|
||||
);
|
||||
}
|
||||
}
|
||||
72
src/components/SideBar.js
Normal file
72
src/components/SideBar.js
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// @flow
|
||||
import * as React from "react";
|
||||
|
||||
import withStyles from "material-ui/styles/withStyles";
|
||||
import Drawer from "material-ui/Drawer";
|
||||
import Typography from "material-ui/Typography";
|
||||
import IconButton from "material-ui/IconButton";
|
||||
import AppBar from "material-ui/AppBar";
|
||||
import Toolbar from "material-ui/Toolbar";
|
||||
import List from "material-ui/List";
|
||||
import { renderIcon } from "utils/parseIconName";
|
||||
|
||||
export type SideBarProps = {
|
||||
control: ?Control,
|
||||
open: boolean,
|
||||
onCloseRequest: () => void,
|
||||
icon?: ?string,
|
||||
children?: React.Node
|
||||
};
|
||||
|
||||
export type SideBarState = {
|
||||
};
|
||||
|
||||
class SideBar extends React.Component<SideBarProps & Classes, SideBarState> {
|
||||
constructor(props: SideBarProps & Classes) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static styles(_theme: Object): Object {
|
||||
return {
|
||||
drawerPaper: {
|
||||
width: 320
|
||||
},
|
||||
flex: {
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
this.props.onCloseRequest();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Drawer open={this.props.open}
|
||||
anchor="right"
|
||||
onRequestClose={this.close}
|
||||
classes={{paper: this.props.classes.drawerPaper}}
|
||||
type="persistent"
|
||||
>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
{this.props.icon == null
|
||||
|| renderIcon(this.props.icon, "mdi-36px")}
|
||||
<Typography type="title" className={this.props.classes.flex}>
|
||||
{this.props.control == null || this.props.control.name}
|
||||
</Typography>
|
||||
<IconButton onClick={this.close.bind(this)}>
|
||||
<i className="mdi mdi-close mdi-36px"></i>
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<List id="drawer_uiComponents">
|
||||
{this.props.children}
|
||||
</List>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(SideBar.styles)(SideBar);
|
||||
42
src/components/TopBar.js
Normal file
42
src/components/TopBar.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
|
||||
import AppBar from "material-ui/AppBar";
|
||||
import Toolbar from "material-ui/Toolbar";
|
||||
import Typography from "material-ui/Typography";
|
||||
import { CircularProgress } from "material-ui/Progress";
|
||||
|
||||
export type TopBarProps = {
|
||||
title: string,
|
||||
connected: boolean
|
||||
};
|
||||
|
||||
export type TopBarState = {
|
||||
|
||||
};
|
||||
|
||||
export default class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
constructor(props: TopBarProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
{this.renderConnectionIndicator()}
|
||||
<Typography type="title">{this.props.title}</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
||||
renderConnectionIndicator() {
|
||||
if (this.props.connected) {
|
||||
return (<i style={{fontSize: 48}} className="mdi mdi-map"></i>);
|
||||
}
|
||||
return (
|
||||
<CircularProgress size={48} style={{color: "rgba(0, 0, 0, 0.54)"}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
214
src/components/UiItemList/UiItem.js
Normal file
214
src/components/UiItemList/UiItem.js
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
ListSubheader
|
||||
} from "material-ui/List";
|
||||
import Switch from "material-ui/Switch";
|
||||
import Input, { InputLabel } from "material-ui/Input";
|
||||
import { FormControl } from "material-ui/Form";
|
||||
import Select from "material-ui/Select";
|
||||
import { MenuItem } from "material-ui/Menu";
|
||||
import Button from "material-ui/Button";
|
||||
|
||||
import keyOf from "utils/keyOf";
|
||||
|
||||
type UiItemProps<I> = {
|
||||
item: I,
|
||||
state: State,
|
||||
onChangeState: (topic: string, nextState: any) => void
|
||||
};
|
||||
|
||||
export default class UiItem<I:Object> extends React.Component<UiItemProps<I>> {
|
||||
constructor(props: UiItemProps<I>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
runPrimaryAction() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: The type system can't really check if the enableCondition is of
|
||||
* any function type or if it is a TopicDependentOption or a
|
||||
* StateDependentOption. This should be fixed.
|
||||
*/
|
||||
isEnabled() {
|
||||
if (Object.keys(this.props.item).includes("enableCondition") &&
|
||||
typeof this.props.item.enableCondition == "function") {
|
||||
const enableCondition = this.props.item.enableCondition;
|
||||
const state = this.props.state;
|
||||
const internals = _.mapValues(state, (x) => x.internal);
|
||||
const actuals = _.mapValues(state, (x) => x.actual);
|
||||
return enableCondition(internals, actuals, state);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class UiControl<I: UIControl> extends UiItem<I> {
|
||||
constructor(props: UiItemProps<I>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
changeState(next: any) {
|
||||
if (this.props.item.topic == null) {
|
||||
throw new Error(
|
||||
`Missing topic in ${this.props.item.type} "${this.props.item.text}"`
|
||||
);
|
||||
}
|
||||
this.props.onChangeState(this.props.item.topic, next);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
const control = this.props.item;
|
||||
const topic: string = control.topic || "";
|
||||
const value = this.props.state[topic];
|
||||
if (value == null) {
|
||||
if (topic === "") {
|
||||
throw new Error(
|
||||
`Missing topic in ${control.type} "${control.text}"`
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`Unknown topic "${topic}" in ${control.type} "${control.text}"`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
if (Object.keys(this.props.item).includes("enableCondition") &&
|
||||
typeof this.props.item.enableCondition == "function") {
|
||||
const enableCondition = this.props.item.enableCondition;
|
||||
const value = this.getValue();
|
||||
return enableCondition(
|
||||
value.internal || value.actual, value.actual, this.props.state);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Toggle extends UiControl<UIToggle> {
|
||||
isToggled = () => {
|
||||
const value = this.getValue();
|
||||
const control = this.props.item;
|
||||
const isChecked = control.toggled ||
|
||||
((i, _a, _s) => i === (control.on || "on"));
|
||||
const checked = isChecked(
|
||||
value.internal || value.actual, value.actual, this.props.state);
|
||||
return checked;
|
||||
}
|
||||
|
||||
runPrimaryAction = () => {
|
||||
if (this.isEnabled()) {
|
||||
const control = this.props.item;
|
||||
const toggled = this.isToggled();
|
||||
const next = toggled ? (control.off || "off") : (control.on || "on");
|
||||
this.changeState(next);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return [
|
||||
<ListItemText key="label" primary={this.props.item.text} />,
|
||||
<ListItemSecondaryAction key="action">
|
||||
<Switch label={this.props.item.text}
|
||||
checked={this.isToggled()}
|
||||
onChange={this.runPrimaryAction}
|
||||
disabled={!this.isEnabled()} />
|
||||
</ListItemSecondaryAction>
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export class DropDown extends UiControl<UIDropDown> {
|
||||
runPrimaryAction = (next?: any) => {
|
||||
if (this.isEnabled()) {
|
||||
const control = this.props.item;
|
||||
const keys = _.keys(control.options);
|
||||
const value = this.getValue();
|
||||
const valueIndex = keyOf(keys, value);
|
||||
if (next == null) {
|
||||
this.changeState(keys[(valueIndex + 1) % keys.length]);
|
||||
} else {
|
||||
this.changeState(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const control = this.props.item;
|
||||
const value = this.getValue();
|
||||
const id = `${control.topic}-${control.text}`;
|
||||
const options = control.options;
|
||||
if (options == null) {
|
||||
throw new Error(
|
||||
`Parameter "options" missing for ${control.type} "${control.text}"`
|
||||
);
|
||||
}
|
||||
return (
|
||||
<FormControl>
|
||||
<InputLabel htmlFor={id}>{control.text}</InputLabel>
|
||||
<Select value={value.internal || value.actual}
|
||||
onChange={(event) => this.runPrimaryAction(event.target.value)}
|
||||
disabled={!this.isEnabled()}
|
||||
input={<Input id={id} />}
|
||||
>
|
||||
{_.map(options, (v, k) => <MenuItem value={k} key={k}>{v}</MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Link extends UiItem<UILink> {
|
||||
runPrimaryAction = () => {
|
||||
const control = this.props.item;
|
||||
if (control.link == null) {
|
||||
throw new Error(
|
||||
`Parameter "link" missing for ${control.type} "${control.text}"`
|
||||
);
|
||||
}
|
||||
if (this.isEnabled()) {
|
||||
window.open(control.link, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button raised
|
||||
onClick={this.runPrimaryAction}
|
||||
color="primary"
|
||||
disabled={!this.isEnabled()}
|
||||
>
|
||||
{this.props.item.text}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Section extends UiItem<UISection> {
|
||||
render() {
|
||||
return (
|
||||
<ListSubheader>{this.props.item.text}</ListSubheader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Text extends UiControl<UIText> {
|
||||
render() {
|
||||
return [
|
||||
<ListItemText key="label" primary={this.props.item.text} />,
|
||||
<ListItemText key="val" secondary={this.getValue().internal} />
|
||||
];
|
||||
}
|
||||
}
|
||||
116
src/components/UiItemList/index.js
Normal file
116
src/components/UiItemList/index.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText
|
||||
} from "material-ui/List";
|
||||
import { renderIcon } from "utils/parseIconName";
|
||||
|
||||
// TODO: Use something else
|
||||
import Slider from "material-ui-old/Slider";
|
||||
import MuiThemeProvider from "material-ui-old/styles/MuiThemeProvider";
|
||||
|
||||
import { Toggle, DropDown, Link, Section, Text } from "./UiItem";
|
||||
|
||||
export type UiItemListProps = {
|
||||
controls: Array<ControlUI>,
|
||||
state: State,
|
||||
onChangeState: (topic: string, nextState: any) => void
|
||||
};
|
||||
|
||||
export default class UiItemList extends React.Component<UiItemListProps> {
|
||||
constructor(props: UiItemListProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.controls.map((control, key) => {
|
||||
if (control.type == null) {
|
||||
throw new Error(
|
||||
"A control is missing the \"type\" parameter"
|
||||
);
|
||||
}
|
||||
if (control.type === "section") {
|
||||
return this.renderControl(control);
|
||||
}
|
||||
return (
|
||||
<ListItem key={key}>
|
||||
{control.icon == null ||
|
||||
<ListItemIcon>{renderIcon(control.icon, "mdi-24px")}</ListItemIcon>}
|
||||
{this.renderControl(control)}
|
||||
</ListItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderControl(control: ControlUI) {
|
||||
switch (control.type) {
|
||||
case "toggle": {
|
||||
return <Toggle item={control}
|
||||
state={this.props.state}
|
||||
onChangeState={this.props.onChangeState} />;
|
||||
}
|
||||
case "dropDown": {
|
||||
return <DropDown item={control}
|
||||
state={this.props.state}
|
||||
onChangeState={this.props.onChangeState} />;
|
||||
}
|
||||
case "section": {
|
||||
return <Section item={control}
|
||||
state={this.props.state}
|
||||
onChangeState={this.props.onChangeState} />;
|
||||
}
|
||||
case "link": {
|
||||
return <Link item={control}
|
||||
state={this.props.state}
|
||||
onChangeState={this.props.onChangeState} />;
|
||||
}
|
||||
case "slider": {
|
||||
return this.renderSlider(control);
|
||||
}
|
||||
case "text": {
|
||||
return <Text item={control}
|
||||
state={this.props.state}
|
||||
onChangeState={this.props.onChangeState} />;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Unknown UI type "${control.type}" for "${control.text}" component`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getValue(control: UIControl) {
|
||||
const value = this.props.state[control.topic];
|
||||
if (value == null) {
|
||||
throw new Error(
|
||||
`Unknown topic "${control.topic}" in ${control.type} "${control.text}"`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
renderSlider(control: UISlider) {
|
||||
const value = this.getValue(control);
|
||||
return [
|
||||
<ListItemText primary={control.text} key="text" />,
|
||||
<ListItemSecondaryAction key="action">
|
||||
<MuiThemeProvider>
|
||||
<Slider value={value.internal || value.actual}
|
||||
min={control.min || 0}
|
||||
max={control.max || 100}
|
||||
step={control.step || 1}
|
||||
onChange={
|
||||
(_event, newvalue) =>
|
||||
this.props.onChangeState(control.topic, newvalue)
|
||||
}
|
||||
style={{width: 100}}
|
||||
/></MuiThemeProvider>
|
||||
</ListItemSecondaryAction>
|
||||
];
|
||||
}
|
||||
}
|
||||
57
src/connectMqtt.js
Normal file
57
src/connectMqtt.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// @flow
|
||||
import mqtt from "mqtt";
|
||||
|
||||
// TODO: type mqtt.js
|
||||
|
||||
export type MqttSettings = {
|
||||
onReconnect?: (mqtt: Object) => void,
|
||||
onDisconnect?: (mqtt: Object) => void,
|
||||
onMessage?: (topic: string, message: Object) => void,
|
||||
onMessageSent?: (topic: string, message: any) => void,
|
||||
onConnect?: (mqtt: Object) => void,
|
||||
subscribe?: Array<string>
|
||||
}
|
||||
|
||||
export type MessageCallback = (topic: string, message: any) => void;
|
||||
|
||||
export default function connectMqtt(
|
||||
url: string,
|
||||
settings: MqttSettings = {}
|
||||
): MessageCallback {
|
||||
const client = mqtt.connect(url);
|
||||
client.on("connect", () => {
|
||||
if (settings.subscribe != null) {
|
||||
client.subscribe(settings.subscribe);
|
||||
}
|
||||
if (settings.onConnect != null) {
|
||||
settings.onConnect(client);
|
||||
}
|
||||
});
|
||||
client.on("message", (topic, message) => {
|
||||
if (settings.onMessage != null) {
|
||||
settings.onMessage(topic, message);
|
||||
}
|
||||
});
|
||||
client.on("offline", () => {
|
||||
if (settings.onDisconnect != null) {
|
||||
settings.onDisconnect(client);
|
||||
}
|
||||
});
|
||||
client.on("close", () => {
|
||||
if (settings.onDisconnect != null) {
|
||||
settings.onDisconnect(client);
|
||||
}
|
||||
});
|
||||
client.on("reconnect", () => {
|
||||
if (settings.onReconnect != null) {
|
||||
settings.onReconnect(client);
|
||||
}
|
||||
});
|
||||
return (topic: string, message: any) => {
|
||||
client.publish(topic, message, null, (error) => {
|
||||
if (error == null && settings.onMessageSent != null) {
|
||||
settings.onMessageSent(topic, message);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -1,24 +1,11 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import MuiThemeProvider from "material-ui/styles/MuiThemeProvider";
|
||||
import createMuiTheme from "material-ui/styles/createMuiTheme";
|
||||
import withStyles from "material-ui/styles/withStyles";
|
||||
import Drawer from "material-ui/Drawer";
|
||||
import injectTapEventPlugin from "react-tap-event-plugin";
|
||||
import { store, Actions } from "./state";
|
||||
import connectMqtt from "./mqtt";
|
||||
import SpaceMapBar from "./appbar";
|
||||
import * as UiItems from "./UiItems.js";
|
||||
import SpaceMap from "./map.js";
|
||||
import R from "ramda";
|
||||
|
||||
import App from "components/App";
|
||||
|
||||
import Config from "./config";
|
||||
import Toolbar from "material-ui/Toolbar";
|
||||
import * as colors from "material-ui/colors";
|
||||
import Typography from "material-ui/Typography";
|
||||
import List from "material-ui/List";
|
||||
import IconButton from "material-ui/IconButton";
|
||||
import AppBar from "material-ui/AppBar";
|
||||
|
||||
import "../node_modules/mdi/css/materialdesignicons.min.css";
|
||||
import "../css/styles.css";
|
||||
|
|
@ -27,67 +14,6 @@ injectTapEventPlugin();
|
|||
|
||||
document.title = `${Config.space.name} Map`;
|
||||
|
||||
const UiItem = (state) => (props : ControlUI) =>
|
||||
UiItems[props.type](state, props);
|
||||
|
||||
const renderUi = (state: State, key: ?string) =>
|
||||
key != null && Config.controls[key] != null ?
|
||||
R.map(UiItem(state), Config.controls[key].ui) : null;
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: colors[Config.space.color]
|
||||
}
|
||||
});
|
||||
|
||||
const appStyles = withStyles((_theme) => ({
|
||||
drawerPaper: {
|
||||
width: 320
|
||||
}
|
||||
}));
|
||||
|
||||
class app extends React.Component<{state: State, classes: Object}> {
|
||||
render() {
|
||||
const state = this.props.state;
|
||||
const classes = this.props.classes;
|
||||
if (state == null) return (<div></div>);
|
||||
return (
|
||||
<div>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<div>
|
||||
<SpaceMapBar title={`${Config.space.name} Map`} {...state} />
|
||||
<Drawer open={state.uiOpened != null}
|
||||
anchor="right"
|
||||
onRequestClose={() => store.dispatch({type: Actions.CHANGE_UI})}
|
||||
classes={{paper: classes.drawerPaper}}
|
||||
type="persistent"
|
||||
>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton onClick={() => store.dispatch({type: Actions.CHANGE_UI})}>
|
||||
<i className="mdi mdi-format-horizontal-align-right mdi-36px"></i>
|
||||
</IconButton>
|
||||
<Typography type="title">
|
||||
{state.uiOpened == null ? "" : Config.controls[state.uiOpened].name}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<List id="drawer_uiComponents">
|
||||
{renderUi(state, state.uiOpened)}
|
||||
</List>
|
||||
</Drawer>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
<SpaceMap width={1000} height={700} image="rzl.png" zoom={0} state={state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const App = appStyles(app);
|
||||
|
||||
store.subscribe(() => ReactDOM.render(<App state={store.getState()} />, document.getElementById("content")));
|
||||
|
||||
store.dispatch({type:null});
|
||||
|
||||
connectMqtt(Config.space.mqtt, store);
|
||||
// $FlowFixMe
|
||||
const contentElement: Element = document.getElementById("content");
|
||||
ReactDOM.render(<App config={Config} />, contentElement);
|
||||
|
|
|
|||
83
src/map.js
83
src/map.js
|
|
@ -1,83 +0,0 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import { Map, ImageOverlay, Marker, LayersControl } from "react-leaflet";
|
||||
import Leaflet from "leaflet";
|
||||
import R from "ramda";
|
||||
import Config from "./config";
|
||||
import { Actions } from "./state";
|
||||
import { store } from "./state";
|
||||
|
||||
// convert width/height coordinates to -height/width coordinates
|
||||
const c = (p) => [-p[1], p[0]];
|
||||
|
||||
const color = (iconColor, state: State) => {
|
||||
// TODO: give iconColor not only internal but also actual values
|
||||
return iconColor == null ? "#000000" :
|
||||
iconColor(
|
||||
R.map(x => x.internal == null ?
|
||||
x.actual : x.internal, state.values == null ? {} : state.values)
|
||||
);
|
||||
};
|
||||
const iconHtml = (el, state: State) =>
|
||||
"<i class=\"mdi mdi-" + el.icon + " mdi-36px\" style=\""
|
||||
+ "color:" + color(el.iconColor, state) + ";\">"
|
||||
+ "</i>";
|
||||
|
||||
const Markers = (props) => R.values(R.mapObjIndexed((el, key) => (
|
||||
<Marker position={c(el.position)} key={el.name}
|
||||
icon={Leaflet.divIcon(
|
||||
{
|
||||
html: iconHtml(el, props.state),
|
||||
iconSize: Leaflet.point(36, 36),
|
||||
iconAnchor: Leaflet.point(18, 18)
|
||||
})}
|
||||
onClick={(e) => store.dispatch({
|
||||
type: Actions.CHANGE_UI,
|
||||
payload: key,
|
||||
toggle: e.originalEvent.ctrlKey})}>
|
||||
</Marker>
|
||||
), R.propOr({}, "controls", Config)));
|
||||
|
||||
type SpaceMapProps = {
|
||||
state: State,
|
||||
width: number,
|
||||
height: number,
|
||||
zoom: number,
|
||||
image: string
|
||||
};
|
||||
|
||||
class SpaceMap extends React.Component<SpaceMapProps> {
|
||||
|
||||
constructor(props: SpaceMapProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<Map center={c([props.width / 2, props.height / 2])} zoom={props.zoom}
|
||||
crs={Leaflet.CRS.Simple}>
|
||||
{Markers(props)}
|
||||
<LayersControl position="topright">
|
||||
{Config.layers.map(x =>
|
||||
this.renderLayer(x, [c([0, 0]), c([props.width, props.height])]))}
|
||||
</LayersControl>
|
||||
</Map>
|
||||
);
|
||||
}
|
||||
|
||||
renderLayer(layer, bounds) {
|
||||
const LayersControlType =
|
||||
layer.baseLayer ? LayersControl.BaseLayer : LayersControl.Overlay;
|
||||
return (
|
||||
<LayersControlType name={layer.name}
|
||||
checked={layer.defaultVisibility === "visible"}>
|
||||
<ImageOverlay url={layer.image}
|
||||
bounds={bounds}
|
||||
opacity={layer.opacity} />
|
||||
</LayersControlType>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SpaceMap;
|
||||
35
src/mqtt.js
35
src/mqtt.js
|
|
@ -1,35 +0,0 @@
|
|||
// @flow
|
||||
import mqtt from "mqtt";
|
||||
import { Actions } from "./state";
|
||||
import { Store } from "redux";
|
||||
import Config from "./config";
|
||||
import R from "ramda";
|
||||
|
||||
export default function connectMqtt(url: string, store: Store<*, *>) {
|
||||
const client = mqtt.connect(url);
|
||||
client.on("connect", () => {
|
||||
store.dispatch({
|
||||
type: Actions.MQTT_CONNECT, payload: client
|
||||
});
|
||||
R.forEachObjIndexed(v =>
|
||||
client.subscribe(v.state), Config.topics);
|
||||
});
|
||||
client.on("message", (topic, message) => {
|
||||
store.dispatch({
|
||||
type: Actions.MQTT_MESSAGE,
|
||||
payload: {
|
||||
message: message,
|
||||
topic: topic
|
||||
}
|
||||
});
|
||||
});
|
||||
client.on("offline", () => {
|
||||
store.dispatch({ type: null });
|
||||
});
|
||||
client.on("close", () => {
|
||||
store.dispatch({ type: null });
|
||||
});
|
||||
client.on("reconnect", () => {
|
||||
store.dispatch({ type: null });
|
||||
});
|
||||
}
|
||||
81
src/state.js
81
src/state.js
|
|
@ -1,81 +0,0 @@
|
|||
// @flow
|
||||
import R from "ramda";
|
||||
import { createStore } from "redux";
|
||||
import Config from "./config";
|
||||
import { keyOf } from "./util";
|
||||
import { onSwitch, isToggled } from "./UiItems";
|
||||
|
||||
export const Actions = Object.freeze({
|
||||
MQTT_CONNECT: "CONNECT",
|
||||
MQTT_MESSAGE: "MESSAGE",
|
||||
CHANGE_UI: "UI_POPUP"
|
||||
});
|
||||
|
||||
const initState : State = {
|
||||
mqtt: null,
|
||||
uiOpened: null,
|
||||
values: R.map(
|
||||
topic => {
|
||||
return {
|
||||
internal: keyOf(topic.values, topic.defaultValue),
|
||||
actual: topic.defaultValue
|
||||
};
|
||||
}, Config.topics),
|
||||
visibleLayers: []
|
||||
};
|
||||
|
||||
const onMessage = (state: State, action: StateAction) => {
|
||||
if (action.payload == null) {
|
||||
return state;
|
||||
}
|
||||
|
||||
/*
|
||||
* action.payload.topic is the mqtt topic
|
||||
* topics is the list of all internal topic references
|
||||
* that have their state topic set to action.payload.topic
|
||||
*/
|
||||
const payload = action.payload == null ? { topic: "", message: {} }
|
||||
: action.payload; // thx flow </3
|
||||
const topics = R.keys(R.pickBy(
|
||||
val => val.state === payload.topic, Config.topics));
|
||||
const message = payload.message;
|
||||
const parsedMessage = (topic: string) => {
|
||||
let parseFunction = Config.topics[topic].parseState;
|
||||
if (parseFunction == null) {
|
||||
return message.toString();
|
||||
} else {
|
||||
return parseFunction(message);
|
||||
}
|
||||
};
|
||||
const newValue = (topic: string) => {
|
||||
return {
|
||||
actual: parsedMessage(topic),
|
||||
internal: keyOf(Config.topics[topic].values, parsedMessage(topic))
|
||||
};
|
||||
};
|
||||
return R.mergeDeepRight(state, R.objOf("values", R.mergeAll(
|
||||
R.map(topic => R.objOf(topic, newValue(topic)), topics)
|
||||
)));
|
||||
};
|
||||
|
||||
const match = (value: any, array: Map<any, any>) => array[value];
|
||||
const handleEvent = (state: State = initState, action: StateAction) => {
|
||||
return match(action.type, {
|
||||
[Actions.MQTT_CONNECT]: R.merge(state, { mqtt: action.payload }),
|
||||
[Actions.MQTT_MESSAGE]: onMessage(state, action),
|
||||
[Actions.CHANGE_UI]: (() => {
|
||||
const control = Config.controls[action.payload];
|
||||
if (action.toggle && control.ui.length > 0
|
||||
&& control.ui[0].type === "toggle") {
|
||||
const props = control.ui[0];
|
||||
onSwitch(props.topic, props, state)(null, !isToggled(state, props));
|
||||
return state;
|
||||
} else {
|
||||
return R.merge(state, { uiOpened: action.payload });
|
||||
}
|
||||
})(),
|
||||
[null]: state
|
||||
});
|
||||
};
|
||||
|
||||
export const store = createStore(handleEvent, initState);
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// @flow
|
||||
import R from "ramda";
|
||||
|
||||
export const keyOf = <a, b> (map: Map<b, a>, value: a): ?b =>
|
||||
((keys) => keys[R.findIndex(k => map[k] === value, keys)])(R.keys(map));
|
||||
8
src/utils/keyOf.js
Normal file
8
src/utils/keyOf.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
import _ from "lodash";
|
||||
|
||||
const keyOf = <a, b> (map: Map<a, b>, value: b): ?a => (
|
||||
_.findKey(map, (x) => x === value)
|
||||
);
|
||||
|
||||
export default keyOf;
|
||||
23
src/utils/parseIconName.js
Normal file
23
src/utils/parseIconName.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
export default function parseIconName(name: string): string {
|
||||
return `mdi ${name.split(" ").map((icon) => "mdi-".concat(icon)).join(" ")}`;
|
||||
}
|
||||
|
||||
export const renderIcon = (name: string, extraClass?: string) => {
|
||||
return <i className={`${extraClass || ""} ${parseIconName(name)}`}></i>;
|
||||
};
|
||||
|
||||
export const controlGetIcon = (control: Control, state: State): string => {
|
||||
const internals = _.mapValues(state, (x) => x.internal || x.actual);
|
||||
const actuals = _.mapValues(state, (x) => x.actual);
|
||||
return typeof control.icon !== "function" ? control.icon
|
||||
: control.icon(internals, actuals, state);
|
||||
};
|
||||
|
||||
export const renderControlIcon = (control: Control,
|
||||
state: State, extraClass?: string) => {
|
||||
return renderIcon(controlGetIcon(control, state), extraClass);
|
||||
};
|
||||
143
types/types.js
143
types/types.js
|
|
@ -1,5 +1,9 @@
|
|||
declare type Map<K,V> = { [K]: V };
|
||||
|
||||
declare type Classes = {
|
||||
classes: Map<string, string>
|
||||
};
|
||||
|
||||
declare type Topic = {
|
||||
state: string,
|
||||
command: string,
|
||||
|
|
@ -9,37 +13,98 @@ declare type Topic = {
|
|||
};
|
||||
declare type Topics = Map<string,Topic>;
|
||||
|
||||
declare type ControlUI = {
|
||||
type: "toggle" | "dropDown" | "slider" | "section",
|
||||
declare type TopicDependentOption<T> = (
|
||||
internal: string, actual: any, state: State
|
||||
) => T;
|
||||
declare type StateDependentOption<T> = (
|
||||
internals: Map<string, string>, actuals: Map<string, any>, state: State
|
||||
) => T;
|
||||
|
||||
interface UIControl {
|
||||
+type: string,
|
||||
+text: string,
|
||||
+topic: string
|
||||
};
|
||||
|
||||
interface Enableable {
|
||||
enableCondition?: TopicDependentOption<boolean>
|
||||
};
|
||||
|
||||
declare type UIToggle = $ReadOnly<{|
|
||||
type: "toggle",
|
||||
text: string,
|
||||
topic?: string,
|
||||
topic: string,
|
||||
icon?: string,
|
||||
enableCondition?: TopicDependentOption<boolean>,
|
||||
on?: string,
|
||||
off?: string,
|
||||
toggled?: TopicDependentOption<boolean>
|
||||
|}>;
|
||||
|
||||
enableCondition?: (internal: string, actual: any) => boolean,
|
||||
declare type UIDropDown = $ReadOnly<{|
|
||||
type: "dropDown",
|
||||
text: string,
|
||||
topic: string,
|
||||
icon?: string,
|
||||
enableCondition?: TopicDependentOption<boolean>,
|
||||
options: Map<string, any>,
|
||||
renderValue?: (value: string) => string
|
||||
|}>;
|
||||
|
||||
// LINK optiona properties
|
||||
link?: string,
|
||||
|
||||
// TOGGLE optional properties
|
||||
on?: string, // on override for toggle
|
||||
off?: string, // off override for toggle
|
||||
toggled?: (internal: string, actual: any) => boolean,
|
||||
|
||||
// DROPDOWN optional properties
|
||||
options?: Map<string,any>, //options for dropDown
|
||||
renderValue?: (value: string) => string,
|
||||
|
||||
// SLIDER optional properties
|
||||
declare type UISlider = $ReadOnly<{|
|
||||
type: "slider",
|
||||
text: string,
|
||||
topic: string,
|
||||
icon?: string,
|
||||
enableCondition?: TopicDependentOption<boolean>,
|
||||
min?: number,
|
||||
max?: number,
|
||||
step?: number
|
||||
};
|
||||
|}>;
|
||||
|
||||
declare type UISection = $ReadOnly<{|
|
||||
type: "section",
|
||||
text: string
|
||||
|}>;
|
||||
|
||||
declare type UILink = $ReadOnly<{|
|
||||
type: "link",
|
||||
text: string,
|
||||
link: string,
|
||||
enableCondition?: StateDependentOption<boolean>,
|
||||
|
||||
// TODO: check if both the following options are implemented
|
||||
icon?: string
|
||||
|}>;
|
||||
|
||||
declare type UIText = $ReadOnly<{|
|
||||
type: "text",
|
||||
text: string,
|
||||
topic: string,
|
||||
icon?: string
|
||||
|}>;
|
||||
|
||||
declare type ControlUI =
|
||||
UIToggle
|
||||
| UIDropDown
|
||||
| UISlider
|
||||
| UISection
|
||||
| UILink
|
||||
| UIText
|
||||
|
||||
declare type Control = {
|
||||
name: string,
|
||||
position: Array<number>,
|
||||
icon: string,
|
||||
iconColor?: (state: Map<string,any>) => string,
|
||||
position: [number, number],
|
||||
icon: string | (
|
||||
internals: Map<string, string>,
|
||||
actuals: Map<string, any>,
|
||||
state: State
|
||||
) => string,
|
||||
iconColor?: (
|
||||
internals: Map<string, string>,
|
||||
actuals: Map<string, any>,
|
||||
state: State
|
||||
) => string,
|
||||
ui: Array<ControlUI>
|
||||
};
|
||||
declare type Controls = Map<string,Control>;
|
||||
|
|
@ -54,31 +119,39 @@ declare type Config = {
|
|||
declare type Space = {
|
||||
name: string,
|
||||
color: "red"|"pink"|"purple"|"deepPurple"|"indigo"|"blue"|"lightBlue"|"cyan"|"teal"|
|
||||
"green"|"lightgreen"|"lime"|"yellow"|"amber"|"orange"|"deepOrange"|"brown"|"grey"|"blueGrey"
|
||||
"green"|"lightGreen"|"lime"|"yellow"|"amber"|"orange"|"deepOrange"|"brown"|"grey"|"blueGrey",
|
||||
mqtt: string
|
||||
};
|
||||
|
||||
declare type State = {
|
||||
mqtt: ?any,
|
||||
uiOpened: ?string,
|
||||
declare type StateValue = {
|
||||
internal: string,
|
||||
actual: any
|
||||
};
|
||||
declare type State = Map<string,StateValue>;
|
||||
|
||||
//declare type State = {
|
||||
// mqtt: ?any,
|
||||
// uiOpened: ?string,
|
||||
// A map of the actual state values for each topic.
|
||||
// internal is the internal term for the value,
|
||||
// that is equal to the key in the values section of that
|
||||
// topic, for example given by:
|
||||
// values: { off: "OFF", on: "ON" }
|
||||
// and actual is the value of that or whatever is given by mqtt.
|
||||
values: Map<string, { internal: ?string, actual: any }>,
|
||||
visibleLayers: Array<string>
|
||||
};
|
||||
// values: Map<string, { internal: ?string, actual: any }>,
|
||||
// visibleLayers: Array<string>
|
||||
//};
|
||||
|
||||
declare type Point = [number, number];
|
||||
|
||||
declare type Layer = {
|
||||
image: string,
|
||||
name: string,
|
||||
baseLayer: boolean,
|
||||
baseLayer?: boolean,
|
||||
defaultVisibility: "visible" | "hidden",
|
||||
opacity: number
|
||||
};
|
||||
|
||||
declare type StateAction = {
|
||||
type: "DISCONNECT" | "CONNECT" | "MESSAGE" | "UI_POPUP",
|
||||
payload?: any
|
||||
opacity?: number,
|
||||
bounds: {
|
||||
topLeft: Point,
|
||||
bottomRight: Point
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const preBuildScripts = process.env.NO_FLOW == undefined ?
|
|||
|
||||
module.exports = {
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, "src"), "node_modules"],
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
output: {
|
||||
|
|
@ -24,7 +25,7 @@ module.exports = {
|
|||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(["dist"]),
|
||||
// new WebpackShellPlugin({onBuildStart:preBuildScripts}),
|
||||
new WebpackShellPlugin({onBuildStart:preBuildScripts}),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Space Map',
|
||||
template: 'index.ejs'
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const extractCSS = ExtractTextPlugin.extract({
|
|||
module.exports = merge(common, {
|
||||
entry: {
|
||||
main: path.resolve(__dirname, 'src/index.jsx'),
|
||||
vendor: ['react', 'material-ui', 'mqtt', 'ramda']
|
||||
vendor: ['react', 'material-ui', 'mqtt', 'lodash']
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue