2017-05-02 20:03:48 -06:00
|
|
|
import * as React from "react";
|
2017-05-27 14:03:13 -06:00
|
|
|
import {SyntheticEvent} from "react";
|
|
|
|
import {computed} from "mobx";
|
2017-05-07 14:25:52 -06:00
|
|
|
import DevTools from "mobx-react-devtools";
|
2017-05-27 14:03:13 -06:00
|
|
|
import {observer} from "mobx-react";
|
|
|
|
import {SprinklersDevice, Section, Program, Duration, Schedule} from "./sprinklers";
|
2017-05-30 16:45:25 -06:00
|
|
|
import {Item, Table, Header, Segment, Form, Input, Button, DropdownItemProps, DropdownProps, Message} from "semantic-ui-react";
|
2017-05-03 16:12:51 -06:00
|
|
|
import FontAwesome = require("react-fontawesome");
|
2017-05-02 20:03:48 -06:00
|
|
|
import * as classNames from "classnames";
|
|
|
|
|
2017-05-03 16:12:51 -06:00
|
|
|
import "semantic-ui-css/semantic.css";
|
2017-05-06 15:39:25 -06:00
|
|
|
import "font-awesome/css/font-awesome.css";
|
2017-05-03 16:12:51 -06:00
|
|
|
import "app/style/app.css";
|
2017-05-30 16:45:25 -06:00
|
|
|
import {Message as UiMessage, UiStore} from "./ui";
|
2017-05-03 16:12:51 -06:00
|
|
|
|
2017-05-06 15:39:25 -06:00
|
|
|
/* tslint:disable:object-literal-sort-keys */
|
|
|
|
|
2017-05-02 20:03:48 -06:00
|
|
|
@observer
|
2017-05-04 20:26:51 -06:00
|
|
|
class SectionTable extends React.PureComponent<{ sections: Section[] }, void> {
|
2017-05-06 15:39:25 -06:00
|
|
|
private static renderRow(section: Section, index: number) {
|
2017-05-07 23:30:36 -06:00
|
|
|
if (!section) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-05-27 14:03:13 -06:00
|
|
|
const {name, state} = section;
|
2017-05-03 16:12:51 -06:00
|
|
|
return (
|
2017-05-04 20:26:51 -06:00
|
|
|
<Table.Row key={index}>
|
2017-05-06 15:39:25 -06:00
|
|
|
<Table.Cell className="section--number">{"" + (index + 1)}</Table.Cell>
|
|
|
|
<Table.Cell className="section--name">{name}</Table.Cell>
|
|
|
|
<Table.Cell className={classNames({
|
|
|
|
"section--state": true,
|
|
|
|
"section--state-true": state,
|
|
|
|
"section--state-false": !state,
|
|
|
|
})}>{state ?
|
2017-05-27 14:03:13 -06:00
|
|
|
(<span><FontAwesome name="tint"/> Irrigating</span>)
|
2017-05-06 15:39:25 -06:00
|
|
|
: "Not irrigating"}
|
|
|
|
</Table.Cell>
|
2017-05-03 16:12:51 -06:00
|
|
|
</Table.Row>
|
|
|
|
);
|
|
|
|
}
|
2017-05-04 20:26:51 -06:00
|
|
|
|
2017-05-06 15:39:25 -06:00
|
|
|
public render() {
|
2017-05-04 20:26:51 -06:00
|
|
|
return (<Table celled striped>
|
2017-05-27 14:03:13 -06:00
|
|
|
<Table.Header>
|
|
|
|
<Table.Row>
|
|
|
|
<Table.HeaderCell colSpan="3">Sections</Table.HeaderCell>
|
|
|
|
</Table.Row>
|
|
|
|
<Table.Row>
|
|
|
|
<Table.HeaderCell className="section--number">#</Table.HeaderCell>
|
|
|
|
<Table.HeaderCell className="section--name">Name</Table.HeaderCell>
|
|
|
|
<Table.HeaderCell className="section--state">State</Table.HeaderCell>
|
|
|
|
</Table.Row>
|
|
|
|
</Table.Header>
|
|
|
|
<Table.Body>
|
|
|
|
{
|
|
|
|
this.props.sections.map(SectionTable.renderRow)
|
|
|
|
}
|
|
|
|
</Table.Body>
|
|
|
|
</Table>
|
2017-05-04 20:26:51 -06:00
|
|
|
);
|
|
|
|
}
|
2017-05-03 16:12:51 -06:00
|
|
|
}
|
|
|
|
|
2017-05-07 23:30:36 -06:00
|
|
|
class DurationInput extends React.Component<{
|
|
|
|
duration: Duration,
|
|
|
|
onDurationChange?: (newDuration: Duration) => void;
|
|
|
|
}, void> {
|
|
|
|
public render() {
|
|
|
|
const duration = this.props.duration;
|
2017-05-27 14:03:13 -06:00
|
|
|
// const editing = this.props.onDurationChange != null;
|
2017-05-07 23:30:36 -06:00
|
|
|
return <div className="field durationInput">
|
|
|
|
<label>Duration</label>
|
|
|
|
<div className="fields">
|
2017-05-13 14:10:06 -06:00
|
|
|
<Input type="number" className="field durationInput--minutes"
|
2017-05-27 14:03:13 -06:00
|
|
|
value={duration.minutes} onChange={this.onMinutesChange}
|
|
|
|
label="M" labelPosition="right"/>
|
2017-05-13 14:10:06 -06:00
|
|
|
<Input type="number" className="field durationInput--seconds"
|
2017-05-27 14:03:13 -06:00
|
|
|
value={duration.seconds} onChange={this.onSecondsChange} max="60"
|
|
|
|
label="S" labelPosition="right"/>
|
2017-05-07 23:30:36 -06:00
|
|
|
</div>
|
|
|
|
</div>;
|
|
|
|
}
|
2017-05-13 14:10:06 -06:00
|
|
|
|
2017-05-27 14:03:13 -06:00
|
|
|
private onMinutesChange = (e, {value}) => {
|
|
|
|
if (value.length === 0 || isNaN(value)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const newMinutes = parseInt(value, 10);
|
|
|
|
this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
|
2017-05-13 14:10:06 -06:00
|
|
|
}
|
|
|
|
|
2017-05-27 14:03:13 -06:00
|
|
|
private onSecondsChange = (e, {value}) => {
|
|
|
|
if (value.length === 0 || isNaN(value)) {
|
|
|
|
return;
|
2017-05-13 14:10:06 -06:00
|
|
|
}
|
2017-05-27 14:03:13 -06:00
|
|
|
const newSeconds = parseInt(value, 10);
|
|
|
|
this.props.onDurationChange(this.props.duration.withSeconds(newSeconds));
|
2017-05-13 14:10:06 -06:00
|
|
|
}
|
2017-05-07 23:30:36 -06:00
|
|
|
}
|
|
|
|
|
2017-05-07 14:25:52 -06:00
|
|
|
@observer
|
2017-05-27 14:03:13 -06:00
|
|
|
class RunSectionForm extends React.Component<{
|
|
|
|
sections: Section[],
|
|
|
|
}, {
|
|
|
|
duration: Duration,
|
|
|
|
section: number | "",
|
|
|
|
}> {
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.state = {
|
2017-05-13 14:10:06 -06:00
|
|
|
duration: new Duration(1, 1),
|
2017-05-27 14:03:13 -06:00
|
|
|
section: "",
|
|
|
|
};
|
2017-05-13 14:10:06 -06:00
|
|
|
}
|
|
|
|
|
2017-05-07 14:25:52 -06:00
|
|
|
public render() {
|
2017-05-27 14:03:13 -06:00
|
|
|
const {section, duration} = this.state;
|
2017-05-07 14:25:52 -06:00
|
|
|
return <Segment>
|
|
|
|
<Header>Run Section</Header>
|
|
|
|
<Form>
|
|
|
|
<Form.Group>
|
2017-05-27 14:03:13 -06:00
|
|
|
<Form.Select label="Section" placeholder="Section" options={this.sectionOptions} value={section}
|
|
|
|
onChange={this.onSectionChange}/>
|
|
|
|
<DurationInput duration={duration} onDurationChange={this.onDurationChange}/>
|
|
|
|
{/*Label must be to align it properly*/}
|
|
|
|
<Form.Button label=" " primary onClick={this.run} disabled={!this.isValid}>Run</Form.Button>
|
2017-05-07 14:25:52 -06:00
|
|
|
</Form.Group>
|
|
|
|
</Form>
|
|
|
|
</Segment>;
|
|
|
|
}
|
|
|
|
|
2017-05-27 14:03:13 -06:00
|
|
|
private onSectionChange = (e: SyntheticEvent<HTMLElement>, v: DropdownProps) => {
|
|
|
|
this.setState({section: v.value as number});
|
|
|
|
}
|
|
|
|
|
|
|
|
private onDurationChange = (newDuration: Duration) => {
|
|
|
|
this.setState({duration: newDuration});
|
|
|
|
}
|
|
|
|
|
|
|
|
private run = (e: SyntheticEvent<HTMLElement>) => {
|
|
|
|
e.preventDefault();
|
|
|
|
const section: Section = this.props.sections[this.state.section];
|
|
|
|
console.log(`should run section ${section} for ${this.state.duration}`);
|
2017-05-30 16:45:25 -06:00
|
|
|
section.run(this.state.duration)
|
|
|
|
.then((a) => console.log("ran section", a))
|
|
|
|
.catch((err) => console.error("error running section", err));
|
2017-05-27 14:03:13 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
private get isValid(): boolean {
|
|
|
|
return typeof this.state.section === "number";
|
|
|
|
}
|
|
|
|
|
2017-05-07 14:25:52 -06:00
|
|
|
@computed
|
|
|
|
private get sectionOptions(): DropdownItemProps[] {
|
2017-05-07 23:30:36 -06:00
|
|
|
return this.props.sections.map((s, i) => ({
|
|
|
|
text: s ? s.name : null,
|
|
|
|
value: i,
|
|
|
|
}));
|
2017-05-07 14:25:52 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-13 14:10:06 -06:00
|
|
|
@observer
|
|
|
|
class ScheduleView extends React.PureComponent<{ schedule: Schedule }, void> {
|
|
|
|
public render() {
|
|
|
|
return (
|
|
|
|
<div>{JSON.stringify(this.props.schedule)}</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-03 23:00:10 -06:00
|
|
|
@observer
|
2017-05-04 20:26:51 -06:00
|
|
|
class ProgramTable extends React.PureComponent<{ programs: Program[] }, void> {
|
2017-05-27 14:03:13 -06:00
|
|
|
private static renderRow(program: Program, i: number): JSX.Element[] {
|
2017-05-07 23:30:36 -06:00
|
|
|
if (!program) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-05-27 14:03:13 -06:00
|
|
|
const {name, running, enabled, schedule, sequence} = program;
|
2017-05-13 14:10:06 -06:00
|
|
|
return [
|
2017-05-04 20:26:51 -06:00
|
|
|
<Table.Row key={i}>
|
2017-05-06 15:39:25 -06:00
|
|
|
<Table.Cell className="program--number">{"" + (i + 1)}</Table.Cell>
|
|
|
|
<Table.Cell className="program--name">{name}</Table.Cell>
|
|
|
|
<Table.Cell className="program--running">{running ? "Running" : "Not running"}</Table.Cell>
|
2017-05-13 14:10:06 -06:00
|
|
|
<Table.Cell className="program--enabled">{enabled ? "Enabled" : "Not enabled"}</Table.Cell>
|
2017-05-03 23:00:10 -06:00
|
|
|
</Table.Row>
|
2017-05-13 14:10:06 -06:00
|
|
|
,
|
|
|
|
<Table.Row key={i + .5}>
|
|
|
|
<Table.Cell className="program--sequence" colSpan="4">
|
|
|
|
<ul>
|
|
|
|
{sequence.map((item) =>
|
2017-05-27 14:03:13 -06:00
|
|
|
(<li>Section {item.section + 1 + ""} for
|
|
|
|
{item.duration.minutes}M {item.duration.seconds}S</li>))}
|
|
|
|
</ul>
|
|
|
|
<ScheduleView schedule={schedule}/>
|
2017-05-13 14:10:06 -06:00
|
|
|
</Table.Cell>
|
|
|
|
</Table.Row>
|
|
|
|
,
|
|
|
|
];
|
2017-05-03 23:00:10 -06:00
|
|
|
}
|
2017-05-04 20:26:51 -06:00
|
|
|
|
2017-05-06 15:39:25 -06:00
|
|
|
public render() {
|
2017-05-04 20:26:51 -06:00
|
|
|
return (
|
2017-05-06 15:39:25 -06:00
|
|
|
<Table celled>
|
2017-05-04 20:26:51 -06:00
|
|
|
<Table.Header>
|
|
|
|
<Table.Row>
|
2017-05-13 14:10:06 -06:00
|
|
|
<Table.HeaderCell colSpan="7">Programs</Table.HeaderCell>
|
2017-05-04 20:26:51 -06:00
|
|
|
</Table.Row>
|
|
|
|
<Table.Row>
|
2017-05-06 15:39:25 -06:00
|
|
|
<Table.HeaderCell className="program--number">#</Table.HeaderCell>
|
|
|
|
<Table.HeaderCell className="program--name">Name</Table.HeaderCell>
|
2017-05-13 14:10:06 -06:00
|
|
|
<Table.HeaderCell className="program--running">Running?</Table.HeaderCell>
|
|
|
|
<Table.HeaderCell className="program--enabled">Enabled?</Table.HeaderCell>
|
2017-05-04 20:26:51 -06:00
|
|
|
</Table.Row>
|
|
|
|
</Table.Header>
|
|
|
|
<Table.Body>
|
|
|
|
{
|
2017-05-13 14:10:06 -06:00
|
|
|
Array.prototype.concat.apply([], this.props.programs.map(ProgramTable.renderRow))
|
2017-05-04 20:26:51 -06:00
|
|
|
}
|
|
|
|
</Table.Body>
|
|
|
|
</Table>
|
|
|
|
);
|
|
|
|
}
|
2017-05-03 23:00:10 -06:00
|
|
|
}
|
|
|
|
|
2017-05-27 14:03:13 -06:00
|
|
|
const ConnectionState = ({connected}: { connected: boolean }) =>
|
2017-05-06 15:39:25 -06:00
|
|
|
<span className={classNames({
|
|
|
|
"device--connectionState": true,
|
|
|
|
"device--connectionState-connected": connected,
|
|
|
|
"device--connectionState-disconnected": !connected,
|
|
|
|
})}>
|
2017-05-27 14:03:13 -06:00
|
|
|
<FontAwesome name={connected ? "plug" : "chain-broken"}/>
|
2017-05-06 15:39:25 -06:00
|
|
|
|
|
|
|
{connected ? "Connected" : "Disconnected"}
|
|
|
|
</span>;
|
|
|
|
|
2017-05-03 16:12:51 -06:00
|
|
|
@observer
|
|
|
|
class DeviceView extends React.PureComponent<{ device: SprinklersDevice }, void> {
|
2017-05-06 15:39:25 -06:00
|
|
|
public render() {
|
2017-05-27 14:03:13 -06:00
|
|
|
const {id, connected, sections, programs} = this.props.device;
|
2017-05-02 20:03:48 -06:00
|
|
|
return (
|
|
|
|
<Item>
|
2017-05-27 14:03:13 -06:00
|
|
|
<Item.Image src={require<string>("app/images/raspberry_pi.png")}/>
|
2017-05-02 20:03:48 -06:00
|
|
|
<Item.Content>
|
2017-05-03 16:12:51 -06:00
|
|
|
<Header as="h1">
|
2017-05-02 20:03:48 -06:00
|
|
|
<span>Device </span><kbd>{id}</kbd>
|
2017-05-27 14:03:13 -06:00
|
|
|
<ConnectionState connected={connected}/>
|
2017-05-03 16:12:51 -06:00
|
|
|
</Header>
|
|
|
|
<Item.Meta>
|
|
|
|
|
2017-05-02 20:03:48 -06:00
|
|
|
</Item.Meta>
|
2017-05-27 14:03:13 -06:00
|
|
|
<SectionTable sections={sections}/>
|
|
|
|
<RunSectionForm sections={sections}/>
|
|
|
|
<ProgramTable programs={programs}/>
|
2017-05-02 20:03:48 -06:00
|
|
|
</Item.Content>
|
|
|
|
</Item>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@observer
|
2017-05-30 16:45:25 -06:00
|
|
|
class MessagesView extends React.PureComponent<{uiStore: UiStore}, void> {
|
|
|
|
public render() {
|
|
|
|
return <div>
|
|
|
|
{this.props.uiStore.messages.map(this.renderMessage)}
|
|
|
|
</div>;
|
|
|
|
}
|
|
|
|
|
|
|
|
private renderMessage = (message: UiMessage, index: number) => {
|
|
|
|
const {header, content, type} = message;
|
|
|
|
return <Message header={header} content={content} success={type === UiMessage.Type.Success}
|
|
|
|
info={type === UiMessage.Type.Info} warning={type === UiMessage.Type.Warning}
|
|
|
|
error={type === UiMessage.Type.Error} onDismiss={() => this.dismiss(index)}/>;
|
|
|
|
}
|
|
|
|
|
|
|
|
private dismiss(index: number) {
|
|
|
|
this.props.uiStore.messages.splice(index, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@observer
|
|
|
|
export default class App extends React.PureComponent<{ device: SprinklersDevice, uiStore: UiStore }, any> {
|
2017-05-06 15:39:25 -06:00
|
|
|
public render() {
|
2017-05-07 14:25:52 -06:00
|
|
|
return <Item.Group divided>
|
2017-05-30 16:45:25 -06:00
|
|
|
<MessagesView uiStore={this.props.uiStore} />
|
2017-05-27 14:03:13 -06:00
|
|
|
<DeviceView device={this.props.device}/>
|
2017-05-07 23:30:36 -06:00
|
|
|
<DevTools />
|
2017-05-07 14:25:52 -06:00
|
|
|
</Item.Group>;
|
2017-05-02 20:03:48 -06:00
|
|
|
}
|
2017-05-06 15:39:25 -06:00
|
|
|
}
|