Browse Source

Many ui improvements; saving program updates works

update-deps
Alex Mikhalev 7 years ago
parent
commit
120c719623
  1. 7
      app/components/App.tsx
  2. 33
      app/components/DeviceView.scss
  3. 8
      app/components/DeviceView.tsx
  4. 5
      app/components/DevicesView.tsx
  5. 57
      app/components/DurationInput.tsx
  6. 74
      app/components/DurationView.tsx
  7. 97
      app/components/ProgramSequenceView.tsx
  8. 8
      app/components/ProgramTable.tsx
  9. 38
      app/components/RunSectionForm.tsx
  10. 47
      app/components/SectionChooser.tsx
  11. 5
      app/components/SectionTable.tsx
  12. 3
      app/components/index.ts
  13. BIN
      app/images/favicon-16x16.png
  14. BIN
      app/images/favicon-32x32.png
  15. BIN
      app/images/favicon-96x96.png
  16. BIN
      app/images/favicon.ico
  17. 214
      app/pages/ProgramPage.tsx
  18. 18
      app/styles/app.scss
  19. 2
      app/webpack.config.js
  20. 17
      common/sprinklersRpc/Program.ts
  21. 2
      common/sprinklersRpc/Section.ts
  22. 1
      package.json
  23. 3
      server/entities/SprinklersDevice.ts
  24. 612
      yarn.lock

7
app/components/App.tsx

@ -17,12 +17,11 @@ function NavContainer() { @@ -17,12 +17,11 @@ function NavContainer() {
<Container className="app">
<NavBar/>
<Switch>
<Route path={rp.program(":deviceId", ":programId")} component={p.ProgramPage}/>
<Route path={rp.device(":deviceId")} component={p.DevicePage}/>
<Route path={rp.messagesTest} component={p.MessagesTestPage}/>
<Redirect to="/"/>
</Switch>
{/*<Switch>*/}
{/*<Redirect to="/"/>*/}
{/*</Switch>*/}
<MessagesView/>
</Container>

33
app/components/DeviceView.scss

@ -37,3 +37,36 @@ @@ -37,3 +37,36 @@
height: 14em;
overflow-y: scroll;
}
.ui.modal.programEditor > .header > .header.item .inline.fields {
margin-bottom: 0;
}
.programSequence.editing .item .content {
width: 20em;
}
.durationInputs {
display: flex;
}
$durationInput-labelWidth: 2.5em;
.ui.form .field .ui.input.durationInput {
&.minutes {
margin-right: 1em;
}
&.seconds {
}
> input {
flex: 1 1 6em;
width: 100%;
}
> .label {
flex: 0 0 $durationInput-labelWidth;
text-align: center;
}
}

8
app/components/DeviceView.tsx

@ -3,8 +3,11 @@ import { observer } from "mobx-react"; @@ -3,8 +3,11 @@ import { observer } from "mobx-react";
import * as React from "react";
import { Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react";
import * as p from "@app/pages";
import * as rp from "@app/routePaths";
import { AppState, injectState } from "@app/state";
import { ConnectionState as ConState } from "@common/sprinklersRpc";
import { Route, RouteComponentProps, withRouter } from "react-router";
import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from ".";
import "./DeviceView.scss";
@ -44,7 +47,7 @@ interface DeviceViewProps { @@ -44,7 +47,7 @@ interface DeviceViewProps {
appState: AppState;
}
class DeviceView extends React.Component<DeviceViewProps> {
class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<any>> {
render() {
const { uiStore, sprinklersRpc, routerStore } = this.props.appState;
const device = sprinklersRpc.getDevice(this.props.deviceId);
@ -61,6 +64,7 @@ class DeviceView extends React.Component<DeviceViewProps> { @@ -61,6 +64,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
</Grid.Column>
</Grid>
<ProgramTable device={device} routerStore={routerStore}/>
<Route path={rp.program(":deviceId", ":programId")} component={p.ProgramPage}/>
</React.Fragment>
);
return (
@ -81,4 +85,4 @@ class DeviceView extends React.Component<DeviceViewProps> { @@ -81,4 +85,4 @@ class DeviceView extends React.Component<DeviceViewProps> {
}
}
export default injectState(observer(DeviceView));
export default injectState(withRouter(observer(DeviceView)));

5
app/components/DevicesView.tsx

@ -3,8 +3,9 @@ import * as React from "react"; @@ -3,8 +3,9 @@ import * as React from "react";
import { Item } from "semantic-ui-react";
import DeviceView from "@app/components/DeviceView";
import { RouteComponentProps, withRouter } from "react-router";
class DevicesView extends React.Component<{deviceId: string}> {
class DevicesView extends React.Component<{deviceId: string} & RouteComponentProps<any>> {
render() {
return (
<Item.Group divided>
@ -14,4 +15,4 @@ class DevicesView extends React.Component<{deviceId: string}> { @@ -14,4 +15,4 @@ class DevicesView extends React.Component<{deviceId: string}> {
}
}
export default observer(DevicesView);
export default withRouter(observer(DevicesView));

57
app/components/DurationInput.tsx

@ -1,57 +0,0 @@ @@ -1,57 +0,0 @@
import * as classNames from "classnames";
import * as React from "react";
import { Input, InputProps } from "semantic-ui-react";
import { Duration } from "@common/Duration";
export default class DurationInput extends React.Component<{
duration: Duration,
onDurationChange: (newDuration: Duration) => void,
className?: string,
}> {
render() {
const duration = this.props.duration;
const className = classNames("field", "durationInput", this.props.className);
// const editing = this.props.onDurationChange != null;
return (
<div className={className}>
<label>Duration</label>
<div className="ui two fields">
<Input
type="number"
className="field durationInput--minutes"
value={duration.minutes}
onChange={this.onMinutesChange}
label="M"
labelPosition="right"
/>
<Input
type="number"
className="field durationInput--seconds"
value={duration.seconds}
onChange={this.onSecondsChange}
max="60"
label="S"
labelPosition="right"
/>
</div>
</div>
);
}
private onMinutesChange: InputProps["onChange"] = (e, { value }) => {
if (isNaN(Number(value))) {
return;
}
const newMinutes = parseInt(value, 10);
this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
}
private onSecondsChange: InputProps["onChange"] = (e, { value }) => {
if (isNaN(Number(value))) {
return;
}
const newSeconds = parseInt(value, 10);
this.props.onDurationChange(this.props.duration.withSeconds(newSeconds));
}
}

74
app/components/DurationView.tsx

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
import * as classNames from "classnames";
import * as React from "react";
import { Form, Input, InputProps } from "semantic-ui-react";
import { Duration } from "@common/Duration";
export default class DurationView extends React.Component<{
label?: string,
inline?: boolean,
duration: Duration,
onDurationChange?: (newDuration: Duration) => void,
className?: string,
}> {
render() {
const { duration, label, inline, onDurationChange } = this.props;
const className = classNames("durationInput", this.props.className);
if (onDurationChange) {
return (
<React.Fragment>
<Form.Field inline={inline}>
{label && <label>{label}</label>}
<div className="durationInputs">
<Input
type="number"
className="durationInput minutes"
value={duration.minutes}
onChange={this.onMinutesChange}
label="M"
labelPosition="right"
onWheel={this.onWheel}
/>
<Input
type="number"
className="durationInput seconds"
value={duration.seconds}
onChange={this.onSecondsChange}
max="60"
label="S"
labelPosition="right"
onWheel={this.onWheel}
/>
</div>
</Form.Field>
</React.Fragment>
);
} else {
return (
<span className={className}>
{label && <label>{label}</label>} {duration.minutes}M {duration.seconds}S
</span>
);
}
}
private onMinutesChange: InputProps["onChange"] = (e, { value }) => {
if (!this.props.onDurationChange || isNaN(Number(value))) {
return;
}
const newMinutes = Number(value);
this.props.onDurationChange(this.props.duration.withMinutes(newMinutes));
}
private onSecondsChange: InputProps["onChange"] = (e, { value }) => {
if (!this.props.onDurationChange || isNaN(Number(value))) {
return;
}
const newSeconds = Number(value);
this.props.onDurationChange(this.props.duration.withSeconds(newSeconds));
}
private onWheel = () => {
// do nothing
}
}

97
app/components/ProgramSequenceView.tsx

@ -1,19 +1,94 @@ @@ -1,19 +1,94 @@
import classNames = require("classnames");
import { observer } from "mobx-react";
import * as React from "react";
import { Form, List } from "semantic-ui-react";
import { DurationView, SectionChooser } from "@app/components/index";
import { Duration } from "@common/Duration";
import { ProgramItem, Section} from "@common/sprinklersRpc";
import { ProgramItem, Section } from "@common/sprinklersRpc";
export default function ProgramSequenceView({ sequence, sections }: {
sequence: ProgramItem[], sections: Section[],
}) {
const sequenceItems = sequence.map((item, index) => {
const section = sections[item.section];
const duration = Duration.fromSeconds(item.duration);
@observer
class ProgramSequenceItem extends React.Component<{
sequenceItem: ProgramItem, sections: Section[], onChange?: (newItem: ProgramItem) => void,
}> {
renderContent() {
const { sequenceItem, sections } = this.props;
const editing = this.props.onChange != null;
const section = sections[sequenceItem.section];
const duration = Duration.fromSeconds(sequenceItem.duration);
if (editing) {
return (
<Form.Group inline>
<SectionChooser
label="Section"
inline
sections={sections}
value={section}
onChange={this.onSectionChange}
/>
<DurationView
label="Duration"
inline
duration={duration}
onDurationChange={this.onDurationChange}
/>
</Form.Group>
);
} else {
return (
<React.Fragment>
<List.Header>{section.toString()}</List.Header>
<List.Description>for {duration.toString()}</List.Description>
</React.Fragment>
);
}
}
render() {
return (
<li key={index}>
<em>"{section.name}"</em> for {duration.toString()}
</li>
<List.Item>
<List.Icon name="caret right"/>
<List.Content>{this.renderContent()}</List.Content>
</List.Item>
);
}
private onSectionChange = (newSection: Section) => {
if (!this.props.onChange) {
return;
}
this.props.onChange(new ProgramItem({
...this.props.sequenceItem, section: newSection.id,
}));
}
private onDurationChange = (newDuration: Duration) => {
if (!this.props.onChange) {
return;
}
this.props.onChange(new ProgramItem({
...this.props.sequenceItem, duration: newDuration.toSeconds(),
}));
}
}
@observer
export default class ProgramSequenceView extends React.Component<{
sequence: ProgramItem[], sections: Section[], editing?: boolean,
}> {
render() {
const { sequence, sections } = this.props;
const editing = this.props.editing || false;
const className = classNames("programSequence", { editing });
const sequenceItems = sequence.map((item, index) => {
const onChange = editing ? (newItem: ProgramItem) => this.changeItem(newItem, index) : undefined;
return <ProgramSequenceItem sequenceItem={item} sections={sections} key={index} onChange={onChange}/>;
});
return <ul>{sequenceItems}</ul>;
return <List className={className}>{sequenceItems}</List>;
}
private changeItem = (newItem: ProgramItem, index: number) => {
this.props.sequence[index] = newItem;
}
}

8
app/components/ProgramTable.tsx

@ -6,7 +6,7 @@ import { Button, ButtonProps, Table } from "semantic-ui-react"; @@ -6,7 +6,7 @@ import { Button, ButtonProps, Table } from "semantic-ui-react";
import { ProgramSequenceView, ScheduleView } from "@app/components";
import * as rp from "@app/routePaths";
import { Program, Section, SprinklersDevice } from "@common/sprinklersRpc";
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
@observer
class ProgramRows extends React.Component<{
@ -15,7 +15,7 @@ class ProgramRows extends React.Component<{ @@ -15,7 +15,7 @@ class ProgramRows extends React.Component<{
expanded: boolean, toggleExpanded: (program: Program) => void,
}> {
render() {
const { program, device, expanded, routerStore } = this.props;
const { program, device, expanded } = this.props;
const { sections } = device;
const { name, running, enabled, schedule, sequence } = program;
@ -46,7 +46,7 @@ class ProgramRows extends React.Component<{ @@ -46,7 +46,7 @@ class ProgramRows extends React.Component<{
);
const detailRow = expanded && (
<Table.Row>
<Table.Cell className="program--sequence" colSpan="4">
<Table.Cell className="program--sequence" colSpan="5">
<h4>Sequence: </h4> <ProgramSequenceView sequence={sequence} sections={sections}/>
<h4>Schedule: </h4> <ScheduleView schedule={schedule}/>
</Table.Cell>
@ -82,7 +82,7 @@ export default class ProgramTable extends React.Component<{ @@ -82,7 +82,7 @@ export default class ProgramTable extends React.Component<{
}
render() {
const { programs, sections } = this.props.device;
const { programs } = this.props.device;
const programRows = programs.map(this.renderRows);
return (

38
app/components/RunSectionForm.tsx

@ -1,14 +1,13 @@ @@ -1,14 +1,13 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { DropdownItemProps, DropdownProps, Form, Header, Segment } from "semantic-ui-react";
import { Form, Header, Segment } from "semantic-ui-react";
import { DurationView, SectionChooser } from "@app/components";
import { UiStore } from "@app/state";
import { Duration } from "@common/Duration";
import log from "@common/logger";
import { Section, SprinklersDevice } from "@common/sprinklersRpc";
import { RunSectionResponse } from "@common/sprinklersRpc/deviceRequests";
import DurationInput from "./DurationInput";
@observer
export default class RunSectionForm extends React.Component<{
@ -16,13 +15,13 @@ export default class RunSectionForm extends React.Component<{ @@ -16,13 +15,13 @@ export default class RunSectionForm extends React.Component<{
uiStore: UiStore,
}, {
duration: Duration,
section: number | "",
section: Section | undefined,
}> {
constructor(props: any, context?: any) {
super(props, context);
this.state = {
duration: new Duration(0, 0),
section: "",
section: undefined,
};
}
@ -32,20 +31,18 @@ export default class RunSectionForm extends React.Component<{ @@ -32,20 +31,18 @@ export default class RunSectionForm extends React.Component<{
<Segment>
<Header>Run Section</Header>
<Form>
<Form.Select
<SectionChooser
label="Section"
placeholder="Section"
options={this.sectionOptions}
sections={this.props.device.sections}
value={section}
onChange={this.onSectionChange}
/>
<DurationInput
<DurationView
label="Duration"
duration={duration}
onDurationChange={this.onDurationChange}
/>
{/*Label must be &nbsp; to align it properly*/}
<Form.Button
label="&nbsp;"
primary
onClick={this.run}
disabled={!this.isValid}
@ -57,8 +54,8 @@ export default class RunSectionForm extends React.Component<{ @@ -57,8 +54,8 @@ export default class RunSectionForm extends React.Component<{
);
}
private onSectionChange = (e: React.SyntheticEvent<HTMLElement>, v: DropdownProps) => {
this.setState({ section: v.value as number });
private onSectionChange = (newSection: Section) => {
this.setState({ section: newSection });
}
private onDurationChange = (newDuration: Duration) => {
@ -67,11 +64,10 @@ export default class RunSectionForm extends React.Component<{ @@ -67,11 +64,10 @@ export default class RunSectionForm extends React.Component<{
private run = (e: React.SyntheticEvent<HTMLElement>) => {
e.preventDefault();
if (typeof this.state.section !== "number") {
const { section, duration } = this.state;
if (!section) {
return;
}
const section: Section = this.props.device.sections[this.state.section];
const { duration } = this.state;
section.run(duration.toSeconds())
.then(this.onRunSuccess)
.catch(this.onRunError);
@ -94,14 +90,6 @@ export default class RunSectionForm extends React.Component<{ @@ -94,14 +90,6 @@ export default class RunSectionForm extends React.Component<{
}
private get isValid(): boolean {
return typeof this.state.section === "number";
}
@computed
private get sectionOptions(): DropdownItemProps[] {
return this.props.device.sections.map((s, i) => ({
text: s ? s.name : null,
value: i,
}));
return this.state.section != null && this.state.duration.toSeconds() > 0;
}
}

47
app/components/SectionChooser.tsx

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { DropdownItemProps, DropdownProps, Form } from "semantic-ui-react";
import { Section } from "@common/sprinklersRpc";
@observer
export default class SectionChooser extends React.Component<{
label?: string,
inline?: boolean,
sections: Section[],
value?: Section,
onChange?: (section: Section) => void,
}> {
render() {
const { label, inline, sections, value, onChange } = this.props;
let section = (value == null) ? "" : sections.indexOf(value);
section = (section === -1) ? "" : section;
const onSectionChange = (onChange == null) ? undefined : this.onSectionChange;
if (onChange == null) {
return <React.Fragment>{label || ""} '{value ? value.toString() : ""}'</React.Fragment>;
}
return (
<Form.Select
label={label}
inline={inline}
placeholder="Section"
options={this.sectionOptions}
value={section}
onChange={onSectionChange}
/>
);
}
private onSectionChange = (e: React.SyntheticEvent<HTMLElement>, v: DropdownProps) => {
this.props.onChange!(this.props.sections[v.value as number]);
}
@computed
private get sectionOptions(): DropdownItemProps[] {
return this.props.sections.map((s, i) => ({
text: s ? `${s.id}: ${s.name}` : null,
value: i,
}));
}
}

5
app/components/SectionTable.tsx

@ -15,9 +15,8 @@ export default class SectionTable extends React.Component<{ sections: Section[] @@ -15,9 +15,8 @@ export default class SectionTable extends React.Component<{ sections: Section[]
}
const { name, state } = section;
const sectionStateClass = classNames({
"section--state": true,
"section--state-true": state,
"section--state-false": !state,
"section-state": true,
"running": state,
});
const sectionState = state ?
(<span><Icon name={"shower" as any} /> Irrigating</span>)

3
app/components/index.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
export { default as App } from "./App";
export { default as DevicesView } from "./DevicesView";
export { default as DeviceView } from "./DeviceView";
export { default as DurationInput } from "./DurationInput";
export { default as DurationView } from "./DurationView";
export { default as MessagesView } from "./MessagesView";
export { default as ProgramTable } from "./ProgramTable";
export { default as RunSectionForm } from "./RunSectionForm";
@ -11,3 +11,4 @@ export { default as SectionTable } from "./SectionTable"; @@ -11,3 +11,4 @@ export { default as SectionTable } from "./SectionTable";
export { default as NavBar } from "./NavBar";
export { default as MessageTest } from "./MessageTest";
export { default as ProgramSequenceView } from "./ProgramSequenceView";
export { default as SectionChooser } from "./SectionChooser";

BIN
app/images/favicon-16x16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

BIN
app/images/favicon-32x32.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
app/images/favicon-96x96.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
app/images/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

214
app/pages/ProgramPage.tsx

@ -1,23 +1,95 @@ @@ -1,23 +1,95 @@
import { observer } from "mobx-react";
import { createViewModel, IViewModel } from "mobx-utils";
import * as React from "react";
import { Button, Menu, Segment} from "semantic-ui-react";
import { RouteComponentProps } from "react-router";
import { Button, CheckboxProps, Form, Input, InputOnChangeData, Menu, Modal, Segment } from "semantic-ui-react";
import { ProgramSequenceView, ScheduleView } from "@app/components";
import * as rp from "@app/routePaths";
import { AppState, injectState } from "@app/state";
import log from "@common/logger";
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
import { RouteComponentProps } from "react-router";
import { Link } from "react-router-dom";
@observer
class ProgramDetailView extends React.Component {
}
@observer
class ProgramPage extends React.Component<{
appState: AppState,
} & RouteComponentProps<{ deviceId: string, programId: number }>> {
device!: SprinklersDevice;
} & RouteComponentProps<{ deviceId: string, programId: number }>, {
programView: Program & IViewModel<Program> | undefined,
}> {
private device!: SprinklersDevice;
private program!: Program;
constructor(p: any) {
super(p);
this.state = {
programView: undefined,
};
}
get isEditing(): boolean {
return this.state.programView != null;
}
renderName(program: Program) {
const { name } = program;
if (this.isEditing) {
return (
<Menu.Item header>
<Form>
<Form.Group inline>
<Form.Field inline>
<label><h4>Program</h4></label>
<Input
placeholder="Program Name"
type="text"
value={name}
onChange={this.onNameChange}
/>
</Form.Field>
({program.id})
</Form.Group>
</Form>
</Menu.Item>
);
} else {
return <Menu.Item header as="h4">Program {name} ({program.id})</Menu.Item>;
}
}
renderActions(program: Program) {
const { running } = program;
const editing = this.isEditing;
let editButtons;
if (editing) {
editButtons = (
<React.Fragment>
<Button primary onClick={this.save}>
Save
</Button>
<Button negative onClick={this.cancelEditing}>
Cancel
</Button>
</React.Fragment>
);
} else {
editButtons = (
<Button primary onClick={this.startEditing}>
Edit
</Button>
);
}
return (
<Modal.Actions>
<Button positive={!running} negative={running} onClick={this.cancelOrRun}>
{running ? "Cancel" : "Run"}
</Button>
{editButtons}
<Button onClick={this.close}>
Close
</Button>
</Modal.Actions>
);
}
render() {
const { deviceId, programId } = this.props.match.params;
@ -26,42 +98,102 @@ class ProgramPage extends React.Component<{ @@ -26,42 +98,102 @@ class ProgramPage extends React.Component<{
if (device.programs.length <= programId || !device.programs[programId]) {
return null;
}
const program = device.programs[programId];
this.program = device.programs[programId];
const { programView } = this.state;
const program = programView || this.program;
const editing = programView != null;
const { running, enabled, schedule, sequence } = program;
const programRows = this.renderRows(program, programId);
return (
<div>
<Menu attached="top">
<Menu.Item header>Program {program.name} ({program.id})</Menu.Item>
<Menu.Menu position="right">
<Menu.Item>
<Button as={Link} to={"/devices/" + deviceId}>
Back
</Button>
</Menu.Item>
</Menu.Menu>
</Menu>
<Segment attached="bottom">
{programRows}
</Segment>
</div>
<Modal open onClose={this.close} className="programEditor">
<Modal.Header>{this.renderName(program)}</Modal.Header>
<Modal.Content>
<Form>
<Form.Group>
<Form.Checkbox
toggle
label="Enabled"
checked={enabled}
readOnly={!editing}
onChange={this.onEnabledChange}
/>
<Form.Checkbox toggle label="Running" checked={running} readOnly={!editing}/>
</Form.Group>
<Form.Field>
<label><h4>Sequence</h4></label>
<ProgramSequenceView sequence={sequence} sections={this.device.sections} editing={editing}/>
</Form.Field>
<Form.Field>
<label><h4>Schedule</h4></label>
<ScheduleView schedule={schedule}/>
</Form.Field>
</Form>
</Modal.Content>
{this.renderActions(program)}
</Modal>
);
}
private renderRows = (program: Program, i: number): JSX.Element | null => {
const { name, running, enabled, schedule, sequence } = program;
const cancelOrRun = () => running ? program.cancel() : program.run();
return (
<React.Fragment key={i}>
<b>Enabled: </b>{enabled ? "Enabled" : "Not enabled"}<br/>
<b>Running: </b>{running ? "Running" : "Not running"}<br/>
<Button size="small" onClick={cancelOrRun}>
{running ? "Cancel" : "Run"}
</Button>
<h4>Sequence: </h4> <ProgramSequenceView sequence={sequence} sections={this.device.sections}/>
<h4>Schedule: </h4> <ScheduleView schedule={schedule}/>
</React.Fragment>
);
private cancelOrRun = () => {
if (!this.program) {
return;
}
this.program.running ? this.program.cancel() : this.program.run();
}
private startEditing = () => {
let { programView } = this.state;
if (programView) { // stop editing, so save
programView.submit();
programView = undefined;
} else { // start editing
programView = createViewModel(this.program);
}
this.setState({ programView });
}
private save = () => {
let { programView } = this.state;
if (programView) { // stop editing, so save
programView.submit();
programView = undefined;
}
this.setState({ programView });
this.program.update()
.then((data) => {
log.info({ data }, "Program updated");
}, (err) => {
log.error({ err }, "error updating Program");
});
}
private cancelEditing = () => {
let { programView } = this.state;
if (programView) {
programView = undefined; // stop editing
}
this.setState({ programView });
}
private close = () => {
const { deviceId } = this.props.match.params;
this.props.history.push({ pathname: rp.device(deviceId) });
}
private onNameChange = (e: any, p: InputOnChangeData) => {
const { programView } = this.state;
if (programView) {
programView.name = p.value;
}
}
private onEnabledChange = (e: any, p: CheckboxProps) => {
const { programView } = this.state;
if (programView) {
programView.enabled = p.checked!;
}
}
}

18
app/styles/app.scss

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
.app {
margin-top: 1em;
padding-top: 1em;
}
.sectionRunner--pausedState {
@ -51,26 +51,12 @@ @@ -51,26 +51,12 @@
display: flex !important;
}
.section--state-true {
.section-state.running {
color: green;
}
.section--state-false {
}
.durationInput--minutes,
.durationInput--seconds {
min-width: 6em !important;
}
.durationInput .ui.labeled.input > .label {
width: 3em;
}
.messages {
position: fixed;
/* top: 12px; */
bottom: 1em;
left: 1em;
right: 1em;

2
app/webpack.config.js

@ -2,6 +2,7 @@ const path = require("path"); @@ -2,6 +2,7 @@ const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const DashboardPlugin = require("webpack-dashboard/plugin");
@ -160,6 +161,7 @@ const getConfig = module.exports = (env) => { @@ -160,6 +161,7 @@ const getConfig = module.exports = (env) => {
minifyURLs: true,
} : undefined,
}),
new FaviconsWebpackPlugin(path.resolve(paths.appDir, "images", "favicon-96x96.png")),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === "production") { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV was set to production here.

17
common/sprinklersRpc/Program.ts

@ -1,16 +1,20 @@ @@ -1,16 +1,20 @@
import { observable } from "mobx";
import { serialize } from "serializr";
import { Schedule } from "./schedule";
import * as schema from "./schema";
import { SprinklersDevice } from "./SprinklersDevice";
export class ProgramItem {
// the section number
readonly section: number;
readonly section!: number;
// duration of the run, in seconds
readonly duration: number;
readonly duration!: number;
constructor(section: number = 0, duration: number = 0) {
this.section = section;
this.duration = duration;
constructor(data?: Partial<ProgramItem>) {
if (data) {
Object.assign(this, data);
}
}
}
@ -37,7 +41,8 @@ export class Program { @@ -37,7 +41,8 @@ export class Program {
return this.device.cancelProgram({ programId: this.id });
}
update(data: any) {
update() {
const data = serialize(schema.program, this);
return this.device.updateProgram({ programId: this.id, data });
}

2
common/sprinklersRpc/Section.ts

@ -23,6 +23,6 @@ export class Section { @@ -23,6 +23,6 @@ export class Section {
}
toString(): string {
return `Section{id=${this.id}, name="${this.name}", state=${this.state}}`;
return `Section ${this.id}: '${this.name}'`;
}
}

1
package.json

@ -81,6 +81,7 @@ @@ -81,6 +81,7 @@
"classnames": "^2.2.6",
"css-loader": "^0.28.11",
"dotenv": "^6.0.0",
"favicons-webpack-plugin": "^0.0.9",
"file-loader": "^1.1.11",
"font-awesome": "^4.7.0",
"happypack": "^5.0.0",

3
server/entities/SprinklersDevice.ts

@ -7,6 +7,9 @@ export class SprinklersDevice { @@ -7,6 +7,9 @@ export class SprinklersDevice {
@PrimaryGeneratedColumn()
id!: number;
@Column({ nullable: true, type: "uuid" })
deviceId: string | null = null;
@Column()
name: string = "";

612
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save