Started on HMR support; Working towards adding more functionality
This commit is contained in:
parent
9148ccb34f
commit
2000e0126c
@ -1,30 +1,62 @@
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { SprinklersDevice } from "./sprinklers";
|
||||
import "semantic-ui-css/semantic.min.css";
|
||||
import "app/style/app.css";
|
||||
import { Item } from "semantic-ui-react";
|
||||
import { SprinklersDevice, Section } from "./sprinklers";
|
||||
import { Item, Table, Header } from "semantic-ui-react";
|
||||
import FontAwesome = require("react-fontawesome");
|
||||
import * as classNames from "classnames";
|
||||
|
||||
import "semantic-ui-css/semantic.css";
|
||||
import "font-awesome/css/font-awesome.css"
|
||||
import "app/style/app.css";
|
||||
|
||||
@observer
|
||||
class Device extends React.Component<{ device: SprinklersDevice }, any> {
|
||||
class SectionRow extends React.PureComponent<{ section: Section }, void> {
|
||||
render() {
|
||||
const {id, connected} = this.props.device;
|
||||
const { name, state } = this.props.section;
|
||||
return (
|
||||
<Table.Row>
|
||||
<Table.Cell className="section--name">Section {name}</Table.Cell>
|
||||
<Table.Cell className="section--state">State: {state + ""}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> {
|
||||
render() {
|
||||
const { id, connected, sections } = this.props.device; //src={require("app/images/raspberry_pi.png")}
|
||||
return (
|
||||
<Item>
|
||||
<Item.Image src={require("app/images/raspberry_pi.png")} />
|
||||
<Item.Image />
|
||||
<Item.Content>
|
||||
<Item.Header>
|
||||
<Header as="h1">
|
||||
<span>Device </span><kbd>{id}</kbd>
|
||||
</Item.Header>
|
||||
<Item.Meta>
|
||||
<span className={classNames({
|
||||
"device--connected": connected,
|
||||
"device--disconnected": !connected
|
||||
<small className={classNames({
|
||||
"device--connectedState": true,
|
||||
"device--connectedState-connected": connected,
|
||||
"device--connectedState-disconnected": !connected
|
||||
})}>
|
||||
<FontAwesome name={connected ? "plug" : "chain-broken"} />
|
||||
|
||||
{connected ? "Connected" : "Disconnected"}
|
||||
</span>
|
||||
</small>
|
||||
</Header>
|
||||
<Item.Meta>
|
||||
|
||||
</Item.Meta>
|
||||
<Table celled striped>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan="3">Sections</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{
|
||||
sections.map((s, i) => <SectionRow section={s} key={i} />)
|
||||
}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Item.Content>
|
||||
</Item>
|
||||
);
|
||||
@ -32,8 +64,8 @@ class Device extends React.Component<{ device: SprinklersDevice }, any> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class App extends React.Component<{ device: SprinklersDevice }, any> {
|
||||
export default class App extends React.PureComponent<{ device: SprinklersDevice }, any> {
|
||||
render() {
|
||||
return <Item.Group divided><Device device={this.props.device} /></Item.Group>
|
||||
return <Item.Group divided><DeviceView device={this.props.device} /></Item.Group>
|
||||
}
|
||||
}
|
@ -1,14 +1,26 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { AppContainer } from "react-hot-loader";
|
||||
|
||||
import App from "./App";
|
||||
import {MqttApiClient} from "./mqtt";
|
||||
import { MqttApiClient } from "./mqtt";
|
||||
|
||||
const client = new MqttApiClient();
|
||||
client.start();
|
||||
const device = client.getDevice("grinklers");
|
||||
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const rootElem = document.createElement("div");
|
||||
document.body.appendChild(rootElem);
|
||||
|
||||
ReactDOM.render(<App device={device} />, container);
|
||||
ReactDOM.render(<AppContainer>
|
||||
<App device={device} />
|
||||
</AppContainer>, rootElem);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept("./App", () => {
|
||||
const NextApp = require<any>("./App").default;
|
||||
ReactDOM.render(<AppContainer>
|
||||
<NextApp device={device} />
|
||||
</AppContainer>, rootElem);
|
||||
})
|
||||
}
|
@ -3,7 +3,7 @@ import "paho-mqtt/mqttws31";
|
||||
import MQTT = Paho.MQTT;
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { SprinklersDevice, SprinklersApi } from "./sprinklers";
|
||||
import { SprinklersDevice, SprinklersApi, Section } from "./sprinklers";
|
||||
|
||||
|
||||
export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
||||
@ -42,6 +42,9 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
||||
}
|
||||
|
||||
getDevice(prefix: string): SprinklersDevice {
|
||||
if (/\//.test(prefix)) {
|
||||
throw new Error("Prefix cannot contain a /");
|
||||
}
|
||||
if (!this.devices[prefix]) {
|
||||
const device = this.devices[prefix] = new MqttSprinklersDevice(this, prefix);
|
||||
if (this.connected) {
|
||||
@ -60,10 +63,15 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
||||
|
||||
private onMessageArrived(m: MQTT.Message) {
|
||||
// console.log("message arrived: ", m)
|
||||
for (const prefix in this.devices) {
|
||||
const device = this.devices[prefix];
|
||||
device.onMessage(m);
|
||||
const topicIdx = m.destinationName.indexOf('/'); // find the first /
|
||||
const prefix = m.destinationName.substr(0, topicIdx); // assume prefix does not contain a /
|
||||
const topic = m.destinationName.substr(topicIdx + 1);
|
||||
const device = this.devices[prefix];
|
||||
if (!device) {
|
||||
console.warn(`recieved message for unknown device. prefix: ${prefix}`);
|
||||
return;
|
||||
}
|
||||
device.onMessage(topic, m.payloadString);
|
||||
}
|
||||
|
||||
private onConnectionLost(e: MQTT.MQTTError) {
|
||||
@ -83,15 +91,17 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
||||
|
||||
private getSubscriptions() {
|
||||
return [
|
||||
`${this.prefix}/connected`
|
||||
`${this.prefix}/connected`,
|
||||
`${this.prefix}/sections`,
|
||||
`${this.prefix}/sections/+/#`
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
doSubscribe() {
|
||||
const c = this.apiClient.client;
|
||||
this.getSubscriptions()
|
||||
.forEach(filter => c.subscribe(filter, { qos: 1 }));
|
||||
|
||||
|
||||
}
|
||||
|
||||
doUnsubscribe() {
|
||||
@ -100,21 +110,51 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
||||
.forEach(filter => c.unsubscribe(filter));
|
||||
}
|
||||
|
||||
onMessage(m: MQTT.Message) {
|
||||
const postfix = m.destinationName.replace(`${this.prefix}/`, "");
|
||||
if (postfix === m.destinationName)
|
||||
return;
|
||||
switch (postfix) {
|
||||
case "connected":
|
||||
this.connected = (m.payloadString == "true");
|
||||
console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`)
|
||||
break;
|
||||
default:
|
||||
console.warn(`MqttSprinklersDevice recieved invalid message`, m)
|
||||
/**
|
||||
* Updates this device with the specified message
|
||||
* @param topic The topic, with prefix removed
|
||||
* @param payload The payload string
|
||||
*/
|
||||
onMessage(topic: string, payload: string) {
|
||||
var matches;
|
||||
if (topic == "connected") {
|
||||
this.connected = (payload == "true");
|
||||
// console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`)
|
||||
} else if ((matches = topic.match(/^sections(?:\/(\d+)(?:\/?(.+))?)?$/)) != null) {
|
||||
const [topic, secStr, subTopic] = matches;
|
||||
// console.log(`section: ${secStr}, topic: ${subTopic}, payload: ${payload}`);
|
||||
if (!secStr) { // new number of sections
|
||||
this.sections = new Array(Number(payload));
|
||||
} else {
|
||||
const secNum = Number(secStr);
|
||||
var section = this.sections[secNum];
|
||||
if (!section) {
|
||||
this.sections[secNum] = section = new MqttSection();
|
||||
}
|
||||
(section as MqttSection).onMessage(subTopic, payload);
|
||||
}
|
||||
} else {
|
||||
console.warn(`MqttSprinklersDevice recieved invalid topic: ${topic}`)
|
||||
}
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.prefix;
|
||||
}
|
||||
}
|
||||
|
||||
interface SectionJSON {
|
||||
name: string;
|
||||
pin: number;
|
||||
}
|
||||
|
||||
class MqttSection extends Section {
|
||||
onMessage(topic: string, payload: string) {
|
||||
if (topic == "state") {
|
||||
this.state = (payload == "true");
|
||||
} else if (topic == null) {
|
||||
const json = JSON.parse(payload) as SectionJSON;
|
||||
this.name = json.name;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { observable } from "mobx";
|
||||
|
||||
class Section {
|
||||
export class Section {
|
||||
@observable
|
||||
name: string = ""
|
||||
|
||||
|
@ -1,7 +1,15 @@
|
||||
.device--connected {
|
||||
color: #00FF00;
|
||||
.device--connectedState-connected {
|
||||
color: #13D213;
|
||||
}
|
||||
|
||||
.device--disconnected {
|
||||
color: #FF0000;
|
||||
.device--connectedState-disconnected {
|
||||
color: #D20000;
|
||||
}
|
||||
|
||||
.section--name {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.section--state {
|
||||
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"clean": "rm -rf ./dist",
|
||||
"build": "webpack",
|
||||
"start:dev": "webpack-dev-server --content-base ./dist/"
|
||||
"start:dev": "webpack-dev-server"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -25,20 +25,25 @@
|
||||
"@types/node": "^7.0.13",
|
||||
"@types/react": "^15.0.23",
|
||||
"@types/react-dom": "^15.5.0",
|
||||
"@types/react-fontawesome": "^1.5.0",
|
||||
"classnames": "^2.2.5",
|
||||
"font-awesome": "^4.7.0",
|
||||
"mobx": "^3.1.9",
|
||||
"mobx-react": "^4.1.8",
|
||||
"paho-mqtt": "^1.0.3",
|
||||
"react": "^15.5.4",
|
||||
"react-dom": "^15.5.4",
|
||||
"react-fontawesome": "^1.6.1",
|
||||
"semantic-ui-css": "^2.2.10",
|
||||
"semantic-ui-react": "^0.67.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/webpack-env": "^1.13.0",
|
||||
"awesome-typescript-loader": "^3.1.3",
|
||||
"css-loader": "^0.28.0",
|
||||
"file-loader": "^0.11.1",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"source-map-loader": "^0.2.1",
|
||||
"style-loader": "^0.17.0",
|
||||
"ts-loader": "^2.0.3",
|
||||
|
@ -5,5 +5,9 @@
|
||||
"experimentalDecorators": true,
|
||||
"target": "es5",
|
||||
"typeRoots": ["node_modules/@types"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"./node_modules/@types/webpack-env/index.d.ts",
|
||||
"./app/script/index.tsx"
|
||||
]
|
||||
}
|
@ -3,7 +3,10 @@ const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
entry: "./app/script/index.tsx",
|
||||
entry: [
|
||||
"react-hot-loader/patch",
|
||||
"./app/script/index.tsx"
|
||||
],
|
||||
devtool: "sourcemap",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
@ -17,7 +20,7 @@ module.exports = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, loader: "awesome-typescript-loader" },
|
||||
{ test: /\.tsx?$/, loaders: ["react-hot-loader/webpack", "awesome-typescript-loader"] },
|
||||
{ test: /\.css$/, loader: "style-loader!css-loader" },
|
||||
{ test: /\.(ttf|eot|svg|woff(2)?|png|jpg)(\?[a-z0-9=&.]+)?$/, loader: "file-loader" }
|
||||
]
|
||||
@ -25,6 +28,16 @@ module.exports = {
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
title: "sprinklers3"
|
||||
})
|
||||
]
|
||||
}),
|
||||
new webpack.NamedModulesPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin()
|
||||
],
|
||||
externals: {
|
||||
"react": "React",
|
||||
"react-dom": "ReactDOM"
|
||||
},
|
||||
devServer: {
|
||||
hot: true,
|
||||
contentBase: "./dist"
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user