Browse Source

Started on HMR support; Working towards adding more functionality

update-deps
Alex Mikhalev 8 years ago
parent
commit
2000e0126c
  1. 64
      app/script/App.tsx
  2. 20
      app/script/index.tsx
  3. 76
      app/script/mqtt.ts
  4. 2
      app/script/sprinklers.ts
  5. 16
      app/style/app.css
  6. 7
      package.json
  7. 6
      tsconfig.json
  8. 21
      webpack.config.js

64
app/script/App.tsx

@ -1,30 +1,62 @@ @@ -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"} />
&nbsp;
{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> { @@ -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>
}
}

20
app/script/index.tsx

@ -1,14 +1,26 @@ @@ -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);
})
}

76
app/script/mqtt.ts

@ -3,7 +3,7 @@ import "paho-mqtt/mqttws31"; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;
}
}
}

2
app/script/sprinklers.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { observable } from "mobx";
class Section {
export class Section {
@observable
name: string = ""

16
app/style/app.css

@ -1,7 +1,15 @@ @@ -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 {
}

7
package.json

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

6
tsconfig.json

@ -5,5 +5,9 @@ @@ -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"
]
}

21
webpack.config.js

@ -3,7 +3,10 @@ const webpack = require("webpack"); @@ -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 = { @@ -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 = { @@ -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…
Cancel
Save