Many ui improvements; saving program updates works
This commit is contained in:
		
							parent
							
								
									9d217a594a
								
							
						
					
					
						commit
						120c719623
					
				| @ -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> | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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 { | ||||
|     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> { | ||||
|                     </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> { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default injectState(observer(DeviceView)); | ||||
| export default injectState(withRouter(observer(DeviceView))); | ||||
|  | ||||
| @ -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}> { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default observer(DevicesView); | ||||
| export default withRouter(observer(DevicesView)); | ||||
|  | ||||
| @ -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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/components/DurationView.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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
 | ||||
|     } | ||||
| } | ||||
| @ -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"; | ||||
| 
 | ||||
| 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 ( | ||||
|             <li key={index}> | ||||
|                 <em>"{section.name}"</em> for {duration.toString()} | ||||
|             </li> | ||||
|                 <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> | ||||
|             ); | ||||
|     }); | ||||
|     return <ul>{sequenceItems}</ul>; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <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 <List className={className}>{sequenceItems}</List>; | ||||
|     } | ||||
| 
 | ||||
|     private changeItem = (newItem: ProgramItem, index: number) => { | ||||
|         this.props.sequence[index] = newItem; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<{ | ||||
|     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<{ | ||||
|         ); | ||||
|         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<{ | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         const { programs, sections } = this.props.device; | ||||
|         const { programs } = this.props.device; | ||||
|         const programRows = programs.map(this.renderRows); | ||||
| 
 | ||||
|         return ( | ||||
|  | ||||
| @ -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<{ | ||||
|     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<{ | ||||
|             <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   to align it properly*/} | ||||
|                     <Form.Button | ||||
|                         label=" " | ||||
|                         primary | ||||
|                         onClick={this.run} | ||||
|                         disabled={!this.isValid} | ||||
| @ -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<{ | ||||
| 
 | ||||
|     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<{ | ||||
|     } | ||||
| 
 | ||||
|     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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/components/SectionChooser.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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, | ||||
|         })); | ||||
|     } | ||||
| } | ||||
| @ -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>) | ||||
|  | ||||
| @ -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"; | ||||
| 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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/images/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 955 B | 
							
								
								
									
										
											BIN
										
									
								
								app/images/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/images/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/images/favicon-96x96.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/images/favicon-96x96.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								app/images/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/images/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
| @ -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<{ | ||||
|         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!; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| .app { | ||||
|   margin-top: 1em; | ||||
|   padding-top: 1em; | ||||
| } | ||||
| 
 | ||||
| .sectionRunner--pausedState { | ||||
| @ -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,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) => { | ||||
|                 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.
 | ||||
|  | ||||
| @ -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 { | ||||
|         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 }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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}'`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
| @ -7,6 +7,9 @@ export class SprinklersDevice { | ||||
|     @PrimaryGeneratedColumn() | ||||
|     id!: number; | ||||
| 
 | ||||
|     @Column({ nullable: true, type: "uuid" }) | ||||
|     deviceId: string | null = null; | ||||
| 
 | ||||
|     @Column() | ||||
|     name: string = ""; | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user