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>
|
||||
<Route path={rp.device(":deviceId")} component={p.DevicePage}/>
|
||||
<Route path={rp.messagesTest} component={p.MessagesTestPage}/>
|
||||
{/*<Switch>*/}
|
||||
{/*<Redirect to="/"/>*/}
|
||||
{/*</Switch>*/}
|
||||
|
||||
<MessagesView/>
|
||||
</Container>
|
||||
|
@ -36,4 +36,37 @@
|
||||
max-height: 14em;
|
||||
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";
|
||||
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>
|
||||
);
|
||||
});
|
||||
return <ul>{sequenceItems}</ul>;
|
||||
}
|
||||
|
||||
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 |
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
|
||||
|
||||
|
||||
<title>Sprinklers3</title>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -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