From fd9f67f555c2389219a1a247eabf149f28f73a06 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Tue, 26 Jun 2018 11:53:22 -0600 Subject: [PATCH] Lots of ui improvments --- app/components/App.tsx | 15 +-- app/components/DeviceView.scss | 55 +++++++---- app/components/DeviceView.tsx | 10 +- app/components/ProgramTable.tsx | 46 ++++++--- app/components/RunSectionForm.tsx | 13 +-- app/components/SectionRunnerView.tsx | 140 ++++++++++++++++++++++----- app/components/SectionTable.tsx | 2 +- app/styles/{app.css => app.scss} | 31 +++++- app/webpack.config.js | 1 + common/sprinklers/schema/index.ts | 2 +- 10 files changed, 235 insertions(+), 80 deletions(-) rename app/styles/{app.css => app.scss} (61%) diff --git a/app/components/App.tsx b/app/components/App.tsx index 76e31bf..f689c28 100644 --- a/app/components/App.tsx +++ b/app/components/App.tsx @@ -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 ( - + - - - + + + + + diff --git a/app/components/DeviceView.scss b/app/components/DeviceView.scss index 80f1c8f..72cd540 100644 --- a/app/components/DeviceView.scss +++ b/app/components/DeviceView.scss @@ -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; +} \ No newline at end of file diff --git a/app/components/DeviceView.tsx b/app/components/DeviceView.tsx index 6e5cc69..eb72e10 100644 --- a/app/components/DeviceView.tsx +++ b/app/components/DeviceView.tsx @@ -64,16 +64,16 @@ class DeviceView extends React.Component { Raspberry Pi Grinklers Device - - + + + - - + + - ); diff --git a/app/components/ProgramTable.tsx b/app/components/ProgramTable.tsx index 602b309..89eddc6 100644 --- a/app/components/ProgramTable.tsx +++ b/app/components/ProgramTable.tsx @@ -30,9 +30,9 @@ export class ScheduleView extends React.Component<{ schedule: Schedule }> { const to = formatDateOfYear(schedule.to, "To "); return (
- At {times}
- On {weekdays}
- {from}
+ At {times}
+ On {weekdays}
+ {from}
{to}
); @@ -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[], # Name - Running? Enabled? + Running? @@ -78,25 +87,30 @@ export default class ProgramTable extends React.Component<{ programs: Program[], ]; }); const cancelOrRun = () => running ? program.cancel() : program.run(); - return [( + const rows = [( {"" + (i + 1)} {name} + {enabled ? "Enabled" : "Not enabled"} - {running ? "Running" : "Not running"} - - {enabled ? "Enabled" : "Not enabled"} - - ), ( - - -

Sequence:

{sequenceItems} -

Schedule:

-
)]; + if (false) { + rows.push( + + +

Sequence:

{sequenceItems} +

Schedule:

+
+
, + ); + } + return rows; } } diff --git a/app/components/RunSectionForm.tsx b/app/components/RunSectionForm.tsx index c3d732a..f7231f1 100644 --- a/app/components/RunSectionForm.tsx +++ b/app/components/RunSectionForm.tsx @@ -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, })); diff --git a/app/components/SectionRunnerView.tsx b/app/components/SectionRunnerView.tsx index d745526..8c878c4 100644 --- a/app/components/SectionRunnerView.tsx +++ b/app/components/SectionRunnerView.tsx @@ -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 ( - - + ); } -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 ( - - '{section.name}' for {duration.toString()} - - - ); +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 = + ; + } + return ( + +
+ {description} + +
+ {progressBar} +
+ ); + } } @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) => - ); + ); + if (current) { + queueView.unshift(); + } + if (queueView.length === 0) { + queueView.push(No items in queue); + } return ( - -

Section Runner Queue

- - {current && } + +
+

Section Runner Queue

+
+ +
+ {queueView} ); } + + 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")); + } } diff --git a/app/components/SectionTable.tsx b/app/components/SectionTable.tsx index 34617f5..972611e 100644 --- a/app/components/SectionTable.tsx +++ b/app/components/SectionTable.tsx @@ -34,7 +34,7 @@ export default class SectionTable extends React.Component<{ sections: Section[] render() { const rows = this.props.sections.map(SectionTable.renderRow); return ( - +
Sections diff --git a/app/styles/app.css b/app/styles/app.scss similarity index 61% rename from app/styles/app.css rename to app/styles/app.scss index e9539e2..d4ea1b4 100644 --- a/app/styles/app.css +++ b/app/styles/app.scss @@ -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; } \ No newline at end of file diff --git a/app/webpack.config.js b/app/webpack.config.js index dbcb87e..418bab6 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -176,6 +176,7 @@ const getConfig = module.exports = (env) => { sourceMap: shouldUseSourceMap, }), isDev && new webpack.HotModuleReplacementPlugin(), + new webpack.NamedModulesPlugin(), ].filter(Boolean); return { diff --git a/common/sprinklers/schema/index.ts b/common/sprinklers/schema/index.ts index 6d8b6d3..5589286 100644 --- a/common/sprinklers/schema/index.ts +++ b/common/sprinklers/schema/index.ts @@ -36,7 +36,7 @@ export const sectionRun: ModelSchema = { section: primitive(), duration: common.duration, startTime: common.date, - endTime: common.date, + pauseTime: common.date, }, };