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 * as React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { SprinklersDevice } from "./sprinklers";
|
import { SprinklersDevice, Section } from "./sprinklers";
|
||||||
import "semantic-ui-css/semantic.min.css";
|
import { Item, Table, Header } from "semantic-ui-react";
|
||||||
import "app/style/app.css";
|
import FontAwesome = require("react-fontawesome");
|
||||||
import { Item } from "semantic-ui-react";
|
|
||||||
import * as classNames from "classnames";
|
import * as classNames from "classnames";
|
||||||
|
|
||||||
|
import "semantic-ui-css/semantic.css";
|
||||||
|
import "font-awesome/css/font-awesome.css"
|
||||||
|
import "app/style/app.css";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Device extends React.Component<{ device: SprinklersDevice }, any> {
|
class SectionRow extends React.PureComponent<{ section: Section }, void> {
|
||||||
render() {
|
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 (
|
return (
|
||||||
<Item>
|
<Item>
|
||||||
<Item.Image src={require("app/images/raspberry_pi.png")} />
|
<Item.Image />
|
||||||
<Item.Content>
|
<Item.Content>
|
||||||
<Item.Header>
|
<Header as="h1">
|
||||||
<span>Device </span><kbd>{id}</kbd>
|
<span>Device </span><kbd>{id}</kbd>
|
||||||
</Item.Header>
|
<small className={classNames({
|
||||||
<Item.Meta>
|
"device--connectedState": true,
|
||||||
<span className={classNames({
|
"device--connectedState-connected": connected,
|
||||||
"device--connected": connected,
|
"device--connectedState-disconnected": !connected
|
||||||
"device--disconnected": !connected
|
|
||||||
})}>
|
})}>
|
||||||
|
<FontAwesome name={connected ? "plug" : "chain-broken"} />
|
||||||
|
|
||||||
{connected ? "Connected" : "Disconnected"}
|
{connected ? "Connected" : "Disconnected"}
|
||||||
</span>
|
</small>
|
||||||
|
</Header>
|
||||||
|
<Item.Meta>
|
||||||
|
|
||||||
</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.Content>
|
||||||
</Item>
|
</Item>
|
||||||
);
|
);
|
||||||
@ -32,8 +64,8 @@ class Device extends React.Component<{ device: SprinklersDevice }, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class App extends React.Component<{ device: SprinklersDevice }, any> {
|
export default class App extends React.PureComponent<{ device: SprinklersDevice }, any> {
|
||||||
render() {
|
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 React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
|
import { AppContainer } from "react-hot-loader";
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import {MqttApiClient} from "./mqtt";
|
import { MqttApiClient } from "./mqtt";
|
||||||
|
|
||||||
const client = new MqttApiClient();
|
const client = new MqttApiClient();
|
||||||
client.start();
|
client.start();
|
||||||
const device = client.getDevice("grinklers");
|
const device = client.getDevice("grinklers");
|
||||||
|
|
||||||
const container = document.createElement("div");
|
const rootElem = document.createElement("div");
|
||||||
document.body.appendChild(container);
|
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 MQTT = Paho.MQTT;
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { SprinklersDevice, SprinklersApi } from "./sprinklers";
|
import { SprinklersDevice, SprinklersApi, Section } from "./sprinklers";
|
||||||
|
|
||||||
|
|
||||||
export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
||||||
@ -42,6 +42,9 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDevice(prefix: string): SprinklersDevice {
|
getDevice(prefix: string): SprinklersDevice {
|
||||||
|
if (/\//.test(prefix)) {
|
||||||
|
throw new Error("Prefix cannot contain a /");
|
||||||
|
}
|
||||||
if (!this.devices[prefix]) {
|
if (!this.devices[prefix]) {
|
||||||
const device = this.devices[prefix] = new MqttSprinklersDevice(this, prefix);
|
const device = this.devices[prefix] = new MqttSprinklersDevice(this, prefix);
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
@ -60,10 +63,15 @@ export class MqttApiClient extends EventEmitter implements SprinklersApi {
|
|||||||
|
|
||||||
private onMessageArrived(m: MQTT.Message) {
|
private onMessageArrived(m: MQTT.Message) {
|
||||||
// console.log("message arrived: ", m)
|
// console.log("message arrived: ", m)
|
||||||
for (const prefix in this.devices) {
|
const topicIdx = m.destinationName.indexOf('/'); // find the first /
|
||||||
const device = this.devices[prefix];
|
const prefix = m.destinationName.substr(0, topicIdx); // assume prefix does not contain a /
|
||||||
device.onMessage(m);
|
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) {
|
private onConnectionLost(e: MQTT.MQTTError) {
|
||||||
@ -83,15 +91,17 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|||||||
|
|
||||||
private getSubscriptions() {
|
private getSubscriptions() {
|
||||||
return [
|
return [
|
||||||
`${this.prefix}/connected`
|
`${this.prefix}/connected`,
|
||||||
|
`${this.prefix}/sections`,
|
||||||
|
`${this.prefix}/sections/+/#`
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
doSubscribe() {
|
doSubscribe() {
|
||||||
const c = this.apiClient.client;
|
const c = this.apiClient.client;
|
||||||
this.getSubscriptions()
|
this.getSubscriptions()
|
||||||
.forEach(filter => c.subscribe(filter, { qos: 1 }));
|
.forEach(filter => c.subscribe(filter, { qos: 1 }));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doUnsubscribe() {
|
doUnsubscribe() {
|
||||||
@ -100,21 +110,51 @@ class MqttSprinklersDevice extends SprinklersDevice {
|
|||||||
.forEach(filter => c.unsubscribe(filter));
|
.forEach(filter => c.unsubscribe(filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(m: MQTT.Message) {
|
/**
|
||||||
const postfix = m.destinationName.replace(`${this.prefix}/`, "");
|
* Updates this device with the specified message
|
||||||
if (postfix === m.destinationName)
|
* @param topic The topic, with prefix removed
|
||||||
return;
|
* @param payload The payload string
|
||||||
switch (postfix) {
|
*/
|
||||||
case "connected":
|
onMessage(topic: string, payload: string) {
|
||||||
this.connected = (m.payloadString == "true");
|
var matches;
|
||||||
console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`)
|
if (topic == "connected") {
|
||||||
break;
|
this.connected = (payload == "true");
|
||||||
default:
|
// console.log(`MqttSprinklersDevice with prefix ${this.prefix}: ${this.connected}`)
|
||||||
console.warn(`MqttSprinklersDevice recieved invalid message`, m)
|
} 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 {
|
get id(): string {
|
||||||
return this.prefix;
|
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";
|
import { observable } from "mobx";
|
||||||
|
|
||||||
class Section {
|
export class Section {
|
||||||
@observable
|
@observable
|
||||||
name: string = ""
|
name: string = ""
|
||||||
|
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
.device--connected {
|
.device--connectedState-connected {
|
||||||
color: #00FF00;
|
color: #13D213;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device--disconnected {
|
.device--connectedState-disconnected {
|
||||||
color: #FF0000;
|
color: #D20000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section--name {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section--state {
|
||||||
|
|
||||||
}
|
}
|
@ -8,7 +8,7 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"clean": "rm -rf ./dist",
|
"clean": "rm -rf ./dist",
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
"start:dev": "webpack-dev-server --content-base ./dist/"
|
"start:dev": "webpack-dev-server"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -25,20 +25,25 @@
|
|||||||
"@types/node": "^7.0.13",
|
"@types/node": "^7.0.13",
|
||||||
"@types/react": "^15.0.23",
|
"@types/react": "^15.0.23",
|
||||||
"@types/react-dom": "^15.5.0",
|
"@types/react-dom": "^15.5.0",
|
||||||
|
"@types/react-fontawesome": "^1.5.0",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
"mobx": "^3.1.9",
|
"mobx": "^3.1.9",
|
||||||
"mobx-react": "^4.1.8",
|
"mobx-react": "^4.1.8",
|
||||||
"paho-mqtt": "^1.0.3",
|
"paho-mqtt": "^1.0.3",
|
||||||
"react": "^15.5.4",
|
"react": "^15.5.4",
|
||||||
"react-dom": "^15.5.4",
|
"react-dom": "^15.5.4",
|
||||||
|
"react-fontawesome": "^1.6.1",
|
||||||
"semantic-ui-css": "^2.2.10",
|
"semantic-ui-css": "^2.2.10",
|
||||||
"semantic-ui-react": "^0.67.0"
|
"semantic-ui-react": "^0.67.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/webpack-env": "^1.13.0",
|
||||||
"awesome-typescript-loader": "^3.1.3",
|
"awesome-typescript-loader": "^3.1.3",
|
||||||
"css-loader": "^0.28.0",
|
"css-loader": "^0.28.0",
|
||||||
"file-loader": "^0.11.1",
|
"file-loader": "^0.11.1",
|
||||||
"html-webpack-plugin": "^2.28.0",
|
"html-webpack-plugin": "^2.28.0",
|
||||||
|
"react-hot-loader": "^3.0.0-beta.6",
|
||||||
"source-map-loader": "^0.2.1",
|
"source-map-loader": "^0.2.1",
|
||||||
"style-loader": "^0.17.0",
|
"style-loader": "^0.17.0",
|
||||||
"ts-loader": "^2.0.3",
|
"ts-loader": "^2.0.3",
|
||||||
|
@ -5,5 +5,9 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"typeRoots": ["node_modules/@types"]
|
"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");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: "./app/script/index.tsx",
|
entry: [
|
||||||
|
"react-hot-loader/patch",
|
||||||
|
"./app/script/index.tsx"
|
||||||
|
],
|
||||||
devtool: "sourcemap",
|
devtool: "sourcemap",
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, "dist"),
|
path: path.resolve(__dirname, "dist"),
|
||||||
@ -17,7 +20,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
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: /\.css$/, loader: "style-loader!css-loader" },
|
||||||
{ test: /\.(ttf|eot|svg|woff(2)?|png|jpg)(\?[a-z0-9=&.]+)?$/, loader: "file-loader" }
|
{ test: /\.(ttf|eot|svg|woff(2)?|png|jpg)(\?[a-z0-9=&.]+)?$/, loader: "file-loader" }
|
||||||
]
|
]
|
||||||
@ -25,6 +28,16 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
title: "sprinklers3"
|
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