Lots of ui improvments
This commit is contained in:
parent
1a9c1f5cbc
commit
fd9f67f555
@ -1,15 +1,16 @@
|
||||
import { observer } from "mobx-react";
|
||||
// import DevTools from "mobx-react-devtools";
|
||||
import * as React from "react";
|
||||
import { Redirect, Route } from "react-router";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Container } from "semantic-ui-react";
|
||||
|
||||
import { DevicesView, MessagesView, MessageTest, NavBar } from "@app/components";
|
||||
|
||||
import "@app/styles/app.css";
|
||||
// tslint:disable:ordered-imports
|
||||
import "font-awesome/css/font-awesome.css";
|
||||
import "semantic-ui-css/semantic.css";
|
||||
import "@app/styles/app.scss";
|
||||
|
||||
function DevicePage() {
|
||||
return (
|
||||
@ -28,11 +29,13 @@ class App extends React.Component {
|
||||
return (
|
||||
<Router>
|
||||
<Container className="app">
|
||||
<NavBar />
|
||||
<NavBar/>
|
||||
|
||||
<Route path="/devices/grinklers" component={DevicePage}/>
|
||||
<Route path="/messagesTest" component={MessagesTestPage}/>
|
||||
<Redirect to="/"/>
|
||||
<Switch>
|
||||
<Route path="/devices/grinklers" component={DevicePage}/>
|
||||
<Route path="/messagesTest" component={MessagesTestPage}/>
|
||||
<Redirect to="/"/>
|
||||
</Switch>
|
||||
|
||||
<MessagesView/>
|
||||
</Container>
|
||||
|
@ -1,24 +1,39 @@
|
||||
.device {
|
||||
.header {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
@media only screen and (min-width : 768px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.header {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
@media only screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.connectionState {
|
||||
@media only screen and (min-width : 768px) {
|
||||
margin-left: .75em;
|
||||
}
|
||||
font-size: .75em;
|
||||
font-weight: lighter;
|
||||
|
||||
&.connected {
|
||||
color: #13D213;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
color: #D20000;
|
||||
}
|
||||
}
|
||||
.ui.stackable.grid.in-container > .row > .column {
|
||||
@media only screen and (max-width: 991px) {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
.ui.stackable.grid.in-container {
|
||||
margin-top: 0;
|
||||
}
|
||||
.connectionState {
|
||||
@media only screen and (min-width: 768px) {
|
||||
margin-left: .75em;
|
||||
}
|
||||
font-size: .75em;
|
||||
font-weight: lighter;
|
||||
|
||||
&.connected {
|
||||
color: #13D213;
|
||||
}
|
||||
|
||||
&.disconnected {
|
||||
color: #D20000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sectionRunner .queue {
|
||||
max-height: 14em;
|
||||
height: 14em;
|
||||
overflow-y: scroll;
|
||||
}
|
@ -64,16 +64,16 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
||||
<Item.Meta>
|
||||
Raspberry Pi Grinklers Device
|
||||
</Item.Meta>
|
||||
<Grid stackable>
|
||||
<Grid.Column width="8">
|
||||
<SectionRunnerView sectionRunner={sectionRunner} sections={sections}/>
|
||||
<Grid>
|
||||
<Grid.Column mobile="16" tablet="16" computer="8">
|
||||
<SectionTable sections={sections}/>
|
||||
</Grid.Column>
|
||||
<Grid.Column width="8">
|
||||
<RunSectionForm sections={sections} uiStore={uiStore}/>
|
||||
<Grid.Column mobile="16" tablet="16" computer="8">
|
||||
<RunSectionForm device={this.device} uiStore={uiStore}/>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
<ProgramTable programs={programs} sections={sections}/>
|
||||
<SectionRunnerView sectionRunner={sectionRunner} sections={sections}/>
|
||||
</Item.Content>
|
||||
</Item>
|
||||
);
|
||||
|
@ -30,9 +30,9 @@ export class ScheduleView extends React.Component<{ schedule: Schedule }> {
|
||||
const to = formatDateOfYear(schedule.to, "To ");
|
||||
return (
|
||||
<div>
|
||||
At {times} <br />
|
||||
On {weekdays} <br />
|
||||
{from} <br />
|
||||
At {times} <br/>
|
||||
On {weekdays} <br/>
|
||||
{from} <br/>
|
||||
{to}
|
||||
</div>
|
||||
);
|
||||
@ -40,7 +40,16 @@ export class ScheduleView extends React.Component<{ schedule: Schedule }> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export default class ProgramTable extends React.Component<{ programs: Program[], sections: Section[] }> {
|
||||
export default class ProgramTable extends React.Component<{
|
||||
programs: Program[], sections: Section[],
|
||||
}, {
|
||||
expandedPrograms: Program[],
|
||||
}> {
|
||||
constructor(p: any) {
|
||||
super(p);
|
||||
this.state = { expandedPrograms: [] };
|
||||
}
|
||||
|
||||
render() {
|
||||
const programRows = Array.prototype.concat.apply([],
|
||||
this.props.programs.map(this.renderRows));
|
||||
@ -54,8 +63,8 @@ export default class ProgramTable extends React.Component<{ programs: Program[],
|
||||
<Table.Row>
|
||||
<Table.HeaderCell className="program--number">#</Table.HeaderCell>
|
||||
<Table.HeaderCell className="program--name">Name</Table.HeaderCell>
|
||||
<Table.HeaderCell className="program--running">Running?</Table.HeaderCell>
|
||||
<Table.HeaderCell className="program--enabled">Enabled?</Table.HeaderCell>
|
||||
<Table.HeaderCell className="program--running">Running?</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
@ -78,25 +87,30 @@ export default class ProgramTable extends React.Component<{ programs: Program[],
|
||||
];
|
||||
});
|
||||
const cancelOrRun = () => running ? program.cancel() : program.run();
|
||||
return [(
|
||||
const rows = [(
|
||||
<Table.Row key={i}>
|
||||
<Table.Cell className="program--number">{"" + (i + 1)}</Table.Cell>
|
||||
<Table.Cell className="program--name">{name}</Table.Cell>
|
||||
<Table.Cell className="program--enabled">{enabled ? "Enabled" : "Not enabled"}</Table.Cell>
|
||||
<Table.Cell className="program--running">
|
||||
{running ? "Running" : "Not running"}
|
||||
<Button className="program--runningButton" onClick={cancelOrRun}>
|
||||
<span>{running ? "Running" : "Not running"}</span>
|
||||
<div className="flex-spacer"/>
|
||||
<Button size="small" onClick={cancelOrRun}>
|
||||
{running ? "Cancel" : "Run"}
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="program--enabled">{enabled ? "Enabled" : "Not enabled"}</Table.Cell>
|
||||
</Table.Row>
|
||||
), (
|
||||
<Table.Row key={i + .5}>
|
||||
<Table.Cell className="program--sequence" colSpan="4">
|
||||
<h4>Sequence: </h4> {sequenceItems}
|
||||
<h4>Schedule: </h4> <ScheduleView schedule={schedule} />
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
)];
|
||||
if (false) {
|
||||
rows.push(
|
||||
<Table.Row key={i + .5}>
|
||||
<Table.Cell className="program--sequence" colSpan="4">
|
||||
<h4>Sequence: </h4> {sequenceItems}
|
||||
<h4>Schedule: </h4> <ScheduleView schedule={schedule}/>
|
||||
</Table.Cell>
|
||||
</Table.Row>,
|
||||
);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,13 @@ import { DropdownItemProps, DropdownProps, Form, Header, Segment } from "semanti
|
||||
import { UiStore } from "@app/state";
|
||||
import { Duration } from "@common/Duration";
|
||||
import log from "@common/logger";
|
||||
import { Section } from "@common/sprinklers";
|
||||
import { Section, SprinklersDevice } from "@common/sprinklers";
|
||||
import { RunSectionResponse } from "@common/sprinklers/requests";
|
||||
import DurationInput from "./DurationInput";
|
||||
|
||||
@observer
|
||||
export default class RunSectionForm extends React.Component<{
|
||||
sections: Section[],
|
||||
device: SprinklersDevice,
|
||||
uiStore: UiStore,
|
||||
}, {
|
||||
duration: Duration,
|
||||
@ -70,7 +70,7 @@ export default class RunSectionForm extends React.Component<{
|
||||
if (typeof this.state.section !== "number") {
|
||||
return;
|
||||
}
|
||||
const section: Section = this.props.sections[this.state.section];
|
||||
const section: Section = this.props.device.sections[this.state.section];
|
||||
const { duration } = this.state;
|
||||
section.run(duration.toSeconds())
|
||||
.then(this.onRunSuccess)
|
||||
@ -80,14 +80,15 @@ export default class RunSectionForm extends React.Component<{
|
||||
private onRunSuccess = (result: RunSectionResponse) => {
|
||||
log.debug({ result }, "requested section run");
|
||||
this.props.uiStore.addMessage({
|
||||
color: "green", header: "Section running",
|
||||
success: true, header: "Section running",
|
||||
content: result.message, timeout: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
private onRunError = (err: RunSectionResponse) => {
|
||||
log.error(err, "error running section");
|
||||
this.props.uiStore.addMessage({
|
||||
color: "red", header: "Error running section",
|
||||
error: true, header: "Error running section",
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
@ -98,7 +99,7 @@ export default class RunSectionForm extends React.Component<{
|
||||
|
||||
@computed
|
||||
private get sectionOptions(): DropdownItemProps[] {
|
||||
return this.props.sections.map((s, i) => ({
|
||||
return this.props.device.sections.map((s, i) => ({
|
||||
text: s ? s.name : null,
|
||||
value: i,
|
||||
}));
|
||||
|
@ -1,37 +1,116 @@
|
||||
import * as classNames from "classnames";
|
||||
import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { Button, Icon, Segment } from "semantic-ui-react";
|
||||
import { Button, Icon, Progress, Segment } from "semantic-ui-react";
|
||||
|
||||
import { Duration } from "@common/Duration";
|
||||
import log from "@common/logger";
|
||||
import { Section, SectionRun, SectionRunner } from "@common/sprinklers";
|
||||
|
||||
function PausedState({ paused }: { paused: boolean }) {
|
||||
interface PausedStateProps {
|
||||
paused: boolean;
|
||||
togglePaused: () => void;
|
||||
}
|
||||
|
||||
function PausedState({ paused, togglePaused }: PausedStateProps) {
|
||||
const classes = classNames({
|
||||
"sectionRunner--pausedState": true,
|
||||
"sectionRunner--pausedState-paused": paused,
|
||||
"sectionRunner--pausedState-unpaused": !paused,
|
||||
});
|
||||
return (
|
||||
<span className={classes}>
|
||||
<Icon name={paused ? "pause" : "play"} />
|
||||
<Button className={classes} size="tiny" onClick={togglePaused}>
|
||||
<Icon name={paused ? "pause" : "play"}/>
|
||||
{paused ? "Paused" : "Processing"}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionRunView({ run, sections }:
|
||||
{ run: SectionRun, sections: Section[] }) {
|
||||
const section = sections[run.section];
|
||||
const current = run.startTime != null;
|
||||
const duration = Duration.fromSeconds(run.duration);
|
||||
const cancel = run.cancel;
|
||||
return (
|
||||
<Segment inverted={current} color={current ? "green" : undefined}>
|
||||
'{section.name}' for {duration.toString()}
|
||||
<Button onClick={cancel} icon><Icon name="remove" /></Button>
|
||||
</Segment>
|
||||
);
|
||||
class SectionRunView extends React.Component<{
|
||||
run: SectionRun;
|
||||
sections: Section[];
|
||||
}, {
|
||||
now: number;
|
||||
}> {
|
||||
animationFrameHandle: number | null = null;
|
||||
startTime: number;
|
||||
|
||||
constructor(p: any) {
|
||||
super(p);
|
||||
const now = performance.now();
|
||||
this.state = { now };
|
||||
this.startTime = Date.now() - now;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.requestAnimationFrame();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.requestAnimationFrame();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cancelAnimationFrame();
|
||||
}
|
||||
|
||||
cancelAnimationFrame = () => {
|
||||
if (this.animationFrameHandle != null) {
|
||||
cancelAnimationFrame(this.animationFrameHandle);
|
||||
this.animationFrameHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame = () => {
|
||||
const startTime = this.props.run.startTime;
|
||||
if (startTime != null) {
|
||||
if (this.animationFrameHandle == null) {
|
||||
this.animationFrameHandle = requestAnimationFrame(this.updateNow);
|
||||
}
|
||||
} else {
|
||||
this.cancelAnimationFrame();
|
||||
}
|
||||
}
|
||||
|
||||
updateNow = (now: number) => {
|
||||
this.animationFrameHandle = null;
|
||||
this.setState({
|
||||
now: this.startTime + now,
|
||||
});
|
||||
this.requestAnimationFrame();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { run, sections } = this.props;
|
||||
let now = this.state.now;
|
||||
const section = sections[run.section];
|
||||
const duration = Duration.fromSeconds(run.duration);
|
||||
const cancel = run.cancel;
|
||||
const description = `'${section.name}' for ${duration.toString()}`;
|
||||
let running: boolean = false;
|
||||
let paused: boolean = false;
|
||||
let progressBar: React.ReactNode | undefined;
|
||||
if (run.startTime != null) {
|
||||
running = true;
|
||||
if (run.pauseTime) {
|
||||
now = run.pauseTime.valueOf();
|
||||
paused = true;
|
||||
}
|
||||
const elapsed = (now.valueOf() - run.startTime.valueOf()) / 1000;
|
||||
const percentage = elapsed / run.duration;
|
||||
progressBar =
|
||||
<Progress color={paused ? "yellow" : "blue"} size="tiny" percent={percentage * 100}/>;
|
||||
}
|
||||
return (
|
||||
<Segment className="sectionRun">
|
||||
<div className="flex-horizontal-space-between">
|
||||
{description}
|
||||
<Button onClick={cancel} icon size="mini"><Icon name="remove"/></Button>
|
||||
</div>
|
||||
{progressBar}
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -42,15 +121,32 @@ export default class SectionRunnerView extends React.Component<{
|
||||
const { current, queue, paused } = this.props.sectionRunner;
|
||||
const { sections } = this.props;
|
||||
const queueView = queue.map((run) =>
|
||||
<SectionRunView key={run.id} run={run} sections={sections} />);
|
||||
<SectionRunView key={run.id} run={run} sections={sections}/>);
|
||||
if (current) {
|
||||
queueView.unshift(<SectionRunView run={current} sections={sections}/>);
|
||||
}
|
||||
if (queueView.length === 0) {
|
||||
queueView.push(<Segment>No items in queue</Segment>);
|
||||
}
|
||||
return (
|
||||
<Segment>
|
||||
<h4>Section Runner Queue <PausedState paused={paused} /></h4>
|
||||
<Segment.Group>
|
||||
{current && <SectionRunView run={current} sections={sections} />}
|
||||
<Segment className="sectionRunner">
|
||||
<div style={{ display: "flex", alignContent: "baseline" }}>
|
||||
<h4 style={{ marginBottom: 0 }}>Section Runner Queue</h4>
|
||||
<div className="flex-spacer"/>
|
||||
<PausedState paused={paused} togglePaused={this.togglePaused}/>
|
||||
</div>
|
||||
<Segment.Group className="queue">
|
||||
{queueView}
|
||||
</Segment.Group>
|
||||
</Segment>
|
||||
);
|
||||
}
|
||||
|
||||
togglePaused = () => {
|
||||
const { sectionRunner } = this.props;
|
||||
const paused = !sectionRunner.paused;
|
||||
sectionRunner.setPaused(paused)
|
||||
.then((res) => log.info(res, "set section runner paused to " + paused))
|
||||
.catch((err) => log.info({ err }, "error setting section runner paused status"));
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export default class SectionTable extends React.Component<{ sections: Section[]
|
||||
render() {
|
||||
const rows = this.props.sections.map(SectionTable.renderRow);
|
||||
return (
|
||||
<Table celled striped unstackable>
|
||||
<Table celled striped unstackable compact>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan="3">Sections</Table.HeaderCell>
|
||||
|
@ -13,7 +13,22 @@
|
||||
}
|
||||
|
||||
.sectionRunner--pausedState-unpaused {
|
||||
color: #BBBBBB;
|
||||
}
|
||||
|
||||
.flex-horizontal-space-between {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sectionRun .progress {
|
||||
margin: 1em 0 0 !important;
|
||||
}
|
||||
|
||||
.sectionRun .ui.progress .bar {
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.section--number,
|
||||
@ -31,10 +46,16 @@
|
||||
|
||||
}
|
||||
|
||||
.program--runningButton {
|
||||
margin-left: 1em !important;
|
||||
.ui.table {
|
||||
tr > td.program--running {
|
||||
display: flex !important;
|
||||
@media only screen and (min-width: 768px) {
|
||||
//line-height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.section--state-true {
|
||||
color: green;
|
||||
}
|
||||
@ -62,4 +83,8 @@
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-spacer {
|
||||
flex: 1;
|
||||
}
|
@ -176,6 +176,7 @@ const getConfig = module.exports = (env) => {
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
isDev && new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NamedModulesPlugin(),
|
||||
].filter(Boolean);
|
||||
|
||||
return {
|
||||
|
@ -36,7 +36,7 @@ export const sectionRun: ModelSchema<s.SectionRun> = {
|
||||
section: primitive(),
|
||||
duration: common.duration,
|
||||
startTime: common.date,
|
||||
endTime: common.date,
|
||||
pauseTime: common.date,
|
||||
},
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user