Lots of ui improvments

This commit is contained in:
Alex Mikhalev 2018-06-26 11:53:22 -06:00
parent 1a9c1f5cbc
commit fd9f67f555
10 changed files with 235 additions and 80 deletions

View File

@ -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>

View File

@ -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;
}

View File

@ -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>
);

View File

@ -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;
}
}

View File

@ -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,
}));

View File

@ -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"));
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -176,6 +176,7 @@ const getConfig = module.exports = (env) => {
sourceMap: shouldUseSourceMap,
}),
isDev && new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(),
].filter(Boolean);
return {

View File

@ -36,7 +36,7 @@ export const sectionRun: ModelSchema<s.SectionRun> = {
section: primitive(),
duration: common.duration,
startTime: common.date,
endTime: common.date,
pauseTime: common.date,
},
};