Browse Source

UI improvements; added program page

update-deps
Alex Mikhalev 7 years ago
parent
commit
9d217a594a
  1. 10
      app/components/App.tsx
  2. 6
      app/components/DeviceView.tsx
  3. 4
      app/components/DurationInput.tsx
  4. 11
      app/components/NavBar.tsx
  5. 19
      app/components/ProgramSequenceView.tsx
  6. 155
      app/components/ProgramTable.tsx
  7. 38
      app/components/ScheduleView.tsx
  8. 2
      app/components/index.ts
  9. 69
      app/pages/ProgramPage.tsx
  10. 1
      app/pages/index.tsx
  11. 23
      app/routePaths.ts
  12. 9
      app/styles/app.scss

10
app/components/App.tsx

@ -5,6 +5,7 @@ import { Container } from "semantic-ui-react";
import { MessagesView, NavBar } from "@app/components"; import { MessagesView, NavBar } from "@app/components";
import * as p from "@app/pages"; import * as p from "@app/pages";
import * as rp from "@app/routePaths";
// tslint:disable:ordered-imports // tslint:disable:ordered-imports
import "font-awesome/css/font-awesome.css"; import "font-awesome/css/font-awesome.css";
@ -17,8 +18,9 @@ function NavContainer() {
<NavBar/> <NavBar/>
<Switch> <Switch>
<Route path="/devices/:deviceId" component={p.DevicePage}/> <Route path={rp.program(":deviceId", ":programId")} component={p.ProgramPage}/>
<Route path="/messagesTest" component={p.MessagesTestPage}/> <Route path={rp.device(":deviceId")} component={p.DevicePage}/>
<Route path={rp.messagesTest} component={p.MessagesTestPage}/>
<Redirect to="/"/> <Redirect to="/"/>
</Switch> </Switch>
@ -30,8 +32,8 @@ function NavContainer() {
export default function App() { export default function App() {
return ( return (
<Switch> <Switch>
<Route path="/login" component={p.LoginPage}/> <Route path={rp.login} component={p.LoginPage}/>
<Route path="/logout" component={p.LogoutPage}/> <Route path={rp.logout} component={p.LogoutPage}/>
<NavContainer/> <NavContainer/>
</Switch> </Switch>
); );

6
app/components/DeviceView.tsx

@ -46,9 +46,9 @@ interface DeviceViewProps {
class DeviceView extends React.Component<DeviceViewProps> { class DeviceView extends React.Component<DeviceViewProps> {
render() { render() {
const { uiStore, sprinklersRpc } = this.props.appState; const { uiStore, sprinklersRpc, routerStore } = this.props.appState;
const device = sprinklersRpc.getDevice(this.props.deviceId); const device = sprinklersRpc.getDevice(this.props.deviceId);
const { id, connectionState, sections, programs, sectionRunner } = device; const { id, connectionState, sections, sectionRunner } = device;
const deviceBody = connectionState.isAvailable && ( const deviceBody = connectionState.isAvailable && (
<React.Fragment> <React.Fragment>
<SectionRunnerView sectionRunner={sectionRunner} sections={sections}/> <SectionRunnerView sectionRunner={sectionRunner} sections={sections}/>
@ -60,7 +60,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
<RunSectionForm device={device} uiStore={uiStore}/> <RunSectionForm device={device} uiStore={uiStore}/>
</Grid.Column> </Grid.Column>
</Grid> </Grid>
<ProgramTable programs={programs} sections={sections}/> <ProgramTable device={device} routerStore={routerStore}/>
</React.Fragment> </React.Fragment>
); );
return ( return (

4
app/components/DurationInput.tsx

@ -40,7 +40,7 @@ export default class DurationInput extends React.Component<{
} }
private onMinutesChange: InputProps["onChange"] = (e, { value }) => { private onMinutesChange: InputProps["onChange"] = (e, { value }) => {
if (value.length === 0 || isNaN(Number(value))) { if (isNaN(Number(value))) {
return; return;
} }
const newMinutes = parseInt(value, 10); const newMinutes = parseInt(value, 10);
@ -48,7 +48,7 @@ export default class DurationInput extends React.Component<{
} }
private onSecondsChange: InputProps["onChange"] = (e, { value }) => { private onSecondsChange: InputProps["onChange"] = (e, { value }) => {
if (value.length === 0 || isNaN(Number(value))) { if (isNaN(Number(value))) {
return; return;
} }
const newSeconds = parseInt(value, 10); const newSeconds = parseInt(value, 10);

11
app/components/NavBar.tsx

@ -1,9 +1,10 @@
import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Menu } from "semantic-ui-react"; import { Menu } from "semantic-ui-react";
import * as rp from "@app/routePaths";
import { AppState, ConsumeState, injectState } from "@app/state"; import { AppState, ConsumeState, injectState } from "@app/state";
import { observer } from "mobx-react";
interface NavItemProps { interface NavItemProps {
to: string; to: string;
@ -25,17 +26,17 @@ function NavBar({ appState }: { appState: AppState }) {
let loginMenu; let loginMenu;
if (appState.isLoggedIn) { if (appState.isLoggedIn) {
loginMenu = ( loginMenu = (
<NavItem to="/logout">Logout</NavItem> <NavItem to={rp.logout}>Logout</NavItem>
); );
} else { } else {
loginMenu = ( loginMenu = (
<NavItem to="/login">Login</NavItem> <NavItem to={rp.login}>Login</NavItem>
); );
} }
return ( return (
<Menu> <Menu>
<NavItem to="/devices/grinklers">Device grinklers</NavItem> <NavItem to={rp.device("grinklers")}>Device grinklers</NavItem>
<NavItem to="/messagesTest">Messages test</NavItem> <NavItem to={rp.messagesTest}>Messages test</NavItem>
<Menu.Menu position="right"> <Menu.Menu position="right">
{loginMenu} {loginMenu}
</Menu.Menu> </Menu.Menu>

19
app/components/ProgramSequenceView.tsx

@ -0,0 +1,19 @@
import * as React from "react";
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);
return (
<li key={index}>
<em>"{section.name}"</em> for {duration.toString()}
</li>
);
});
return <ul>{sequenceItems}</ul>;
}

155
app/components/ProgramTable.tsx

@ -1,47 +1,78 @@
import flatMap from "lodash-es/flatMap";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as moment from "moment"; import { RouterStore } from "mobx-react-router";
import * as React from "react"; import * as React from "react";
import { Button, Table } from "semantic-ui-react"; import { Link } from "react-router-dom";
import { Button, ButtonProps, Table } from "semantic-ui-react";
import { Duration } from "@common/Duration"; import { ProgramSequenceView, ScheduleView } from "@app/components";
import { DateOfYear, Program, Schedule, Section, TimeOfDay, Weekday } from "@common/sprinklersRpc"; import * as rp from "@app/routePaths";
import { Program, Section, SprinklersDevice } from "@common/sprinklersRpc";
function timeToString(time: TimeOfDay) {
return moment(time).format("LTS");
}
function formatDateOfYear(day: DateOfYear | null, prefix: string) {
if (day == null) {
return null;
}
return prefix + moment(day).format("l");
}
@observer @observer
export class ScheduleView extends React.Component<{ schedule: Schedule }> { class ProgramRows extends React.Component<{
program: Program, device: SprinklersDevice,
routerStore: RouterStore,
expanded: boolean, toggleExpanded: (program: Program) => void,
}> {
render() { render() {
const { schedule } = this.props; const { program, device, expanded, routerStore } = this.props;
const times = schedule.times.map((time, i) => timeToString(time)) const { sections } = device;
.join(", ");
const weekdays = schedule.weekdays.map((weekday) => const { name, running, enabled, schedule, sequence } = program;
Weekday[weekday]).join(", ");
const from = formatDateOfYear(schedule.from, "From "); const buttonStyle: ButtonProps = { size: "small", compact: true };
const to = formatDateOfYear(schedule.to, "To "); const detailUrl = rp.program(device.id, program.id);
const mainRow = (
<Table.Row>
<Table.Cell className="program--number">{"" + program.id}</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">
<span>{running ? "Running" : "Not running"}</span>
</Table.Cell>
<Table.Cell>
<Button onClick={this.cancelOrRun} {...buttonStyle} positive={!running} negative={running}>
{running ? "Cancel" : "Run"}
</Button>
<Button as={Link} to={detailUrl} {...buttonStyle} primary>
Open
</Button>
<Button onClick={this.toggleExpanded} {...buttonStyle}>
{expanded ? "Hide Details" : "Show Details"}
</Button>
</Table.Cell>
</Table.Row>
);
const detailRow = expanded && (
<Table.Row>
<Table.Cell className="program--sequence" colSpan="4">
<h4>Sequence: </h4> <ProgramSequenceView sequence={sequence} sections={sections}/>
<h4>Schedule: </h4> <ScheduleView schedule={schedule}/>
</Table.Cell>
</Table.Row>
);
return ( return (
<div> <React.Fragment>
At {times} <br/> {mainRow}
On {weekdays} <br/> {detailRow}
{from} <br/> </React.Fragment>
{to}
</div>
); );
} }
private cancelOrRun = () => {
const { program } = this.props;
program.running ? program.cancel() : program.run();
}
private toggleExpanded = () => {
this.props.toggleExpanded(this.props.program);
}
} }
@observer @observer
export default class ProgramTable extends React.Component<{ export default class ProgramTable extends React.Component<{
programs: Program[], sections: Section[], device: SprinklersDevice, routerStore: RouterStore,
}, { }, {
expandedPrograms: Program[], expandedPrograms: Program[],
}> { }> {
@ -51,8 +82,8 @@ export default class ProgramTable extends React.Component<{
} }
render() { render() {
const programRows = Array.prototype.concat.apply([], const { programs, sections } = this.props.device;
this.props.programs.map(this.renderRows)); const programRows = programs.map(this.renderRows);
return ( return (
<Table celled> <Table celled>
@ -65,6 +96,7 @@ export default class ProgramTable extends React.Component<{
<Table.HeaderCell className="program--name">Name</Table.HeaderCell> <Table.HeaderCell className="program--name">Name</Table.HeaderCell>
<Table.HeaderCell className="program--enabled">Enabled?</Table.HeaderCell> <Table.HeaderCell className="program--enabled">Enabled?</Table.HeaderCell>
<Table.HeaderCell className="program--running">Running?</Table.HeaderCell> <Table.HeaderCell className="program--running">Running?</Table.HeaderCell>
<Table.HeaderCell className="program--actions">Actions</Table.HeaderCell>
</Table.Row> </Table.Row>
</Table.Header> </Table.Header>
<Table.Body> <Table.Body>
@ -78,42 +110,29 @@ export default class ProgramTable extends React.Component<{
if (!program) { if (!program) {
return null; return null;
} }
const { name, running, enabled, schedule, sequence } = program; const expanded = this.state.expandedPrograms.indexOf(program) !== -1;
const sequenceItems = flatMap(sequence, (item, index) => {
const section = this.props.sections[item.section];
const duration = Duration.fromSeconds(item.duration);
return [
<em key={index}>"{section.name}"</em>, ` for ${duration.toString()}, `,
];
});
const cancelOrRun = () => running ? program.cancel() : program.run();
const mainRow = (
<Table.Row>
<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">
<span>{running ? "Running" : "Not running"}</span>
<div className="flex-spacer"/>
<Button size="small" onClick={cancelOrRun}>
{running ? "Cancel" : "Run"}
</Button>
</Table.Cell>
</Table.Row>
);
const detailRow = false && (
<Table.Row>
<Table.Cell className="program--sequence" colSpan="4">
<h4>Sequence: </h4> {sequenceItems}
<h4>Schedule: </h4> <ScheduleView schedule={schedule}/>
</Table.Cell>
</Table.Row>
);
return ( return (
<React.Fragment key={i}> <ProgramRows
{mainRow} program={program}
{detailRow} device={this.props.device}
</React.Fragment> routerStore={this.props.routerStore}
expanded={expanded}
toggleExpanded={this.toggleExpanded}
key={i}
/>
); );
} }
private toggleExpanded = (program: Program) => {
const { expandedPrograms } = this.state;
const idx = expandedPrograms.indexOf(program);
if (idx !== -1) {
expandedPrograms.splice(idx, 1);
} else {
expandedPrograms.push(program);
}
this.setState({
expandedPrograms,
});
}
} }

38
app/components/ScheduleView.tsx

@ -0,0 +1,38 @@
import { observer } from "mobx-react";
import * as moment from "moment";
import * as React from "react";
import { DateOfYear, Schedule, TimeOfDay, Weekday } from "@common/sprinklersRpc";
function timeToString(time: TimeOfDay) {
return moment(time).format("LTS");
}
function formatDateOfYear(day: DateOfYear | null, prefix: React.ReactNode) {
if (day == null) {
return null;
}
const format = (day.year === 0) ? "M/D" : "l";
return <React.Fragment>{prefix}{moment(day).format(format)}</React.Fragment>;
}
@observer
export default class ScheduleView extends React.Component<{ schedule: Schedule }> {
render() {
const { schedule } = this.props;
const times = schedule.times.map((time, i) => timeToString(time))
.join(", ");
const weekdays = schedule.weekdays.map((weekday) =>
Weekday[weekday]).join(", ");
const from = formatDateOfYear(schedule.from, <b>From </b>);
const to = formatDateOfYear(schedule.to, <b>To </b>);
return (
<div>
<b>At</b> {times} <br/>
<b>On</b> {weekdays} <br/>
{from} <br/>
{to}
</div>
);
}
}

2
app/components/index.ts

@ -5,7 +5,9 @@ export { default as DurationInput } from "./DurationInput";
export { default as MessagesView } from "./MessagesView"; export { default as MessagesView } from "./MessagesView";
export { default as ProgramTable } from "./ProgramTable"; export { default as ProgramTable } from "./ProgramTable";
export { default as RunSectionForm } from "./RunSectionForm"; export { default as RunSectionForm } from "./RunSectionForm";
export { default as ScheduleView } from "./ScheduleView";
export { default as SectionRunnerView } from "./SectionRunnerView"; export { default as SectionRunnerView } from "./SectionRunnerView";
export { default as SectionTable } from "./SectionTable"; export { default as SectionTable } from "./SectionTable";
export { default as NavBar } from "./NavBar"; export { default as NavBar } from "./NavBar";
export { default as MessageTest } from "./MessageTest"; export { default as MessageTest } from "./MessageTest";
export { default as ProgramSequenceView } from "./ProgramSequenceView";

69
app/pages/ProgramPage.tsx

@ -0,0 +1,69 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Button, Menu, Segment} from "semantic-ui-react";
import { ProgramSequenceView, ScheduleView } from "@app/components";
import { AppState, injectState } from "@app/state";
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;
render() {
const { deviceId, programId } = this.props.match.params;
const device = this.device = this.props.appState.sprinklersRpc.getDevice(deviceId);
// TODO: check programId
if (device.programs.length <= programId || !device.programs[programId]) {
return null;
}
const program = device.programs[programId];
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>
);
}
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>
);
}
}
const DecoratedProgramPage = injectState(observer(ProgramPage));
export default DecoratedProgramPage;

1
app/pages/index.tsx

@ -5,6 +5,7 @@ import { DevicesView, MessageTest} from "@app/components";
export { LoginPage } from "./LoginPage"; export { LoginPage } from "./LoginPage";
export { LogoutPage } from "./LogoutPage"; export { LogoutPage } from "./LogoutPage";
export { default as ProgramPage } from "./ProgramPage";
export function DevicePage({ match }: RouteComponentProps<{ deviceId: string }>) { export function DevicePage({ match }: RouteComponentProps<{ deviceId: string }>) {
return ( return (

23
app/routePaths.ts

@ -0,0 +1,23 @@
export interface RouteParams {
deviceId: string;
programId: string;
}
export const routerRouteParams: RouteParams = {
deviceId: ":deviceId",
programId: ":programId",
};
export const home = "/";
export const messagesTest = "/messagesTest";
export const login = "/login";
export const logout = "/logout";
export function device(deviceId?: string | number): string {
return `/devices/${deviceId || ""}`;
}
export function program(deviceId: string | number, programId?: string | number): string {
return `${device(deviceId)}/programs/${programId}`;
}

9
app/styles/app.scss

@ -47,13 +47,8 @@
} }
.ui.table { .flex {
tr > td.program--running { display: flex !important;
display: flex !important;
@media only screen and (min-width: 768px) {
//line-height: 36px;
}
}
} }
.section--state-true { .section--state-true {

Loading…
Cancel
Save