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. 7
      app/styles/app.scss

10
app/components/App.tsx

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

6
app/components/DeviceView.tsx

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

4
app/components/DurationInput.tsx

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

11
app/components/NavBar.tsx

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

19
app/components/ProgramSequenceView.tsx

@ -0,0 +1,19 @@ @@ -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 @@ @@ -1,47 +1,78 @@
import flatMap from "lodash-es/flatMap";
import { observer } from "mobx-react";
import * as moment from "moment";
import { RouterStore } from "mobx-react-router";
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 { DateOfYear, Program, Schedule, Section, TimeOfDay, Weekday } 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");
}
import { ProgramSequenceView, ScheduleView } from "@app/components";
import * as rp from "@app/routePaths";
import { Program, Section, SprinklersDevice } from "@common/sprinklersRpc";
@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() {
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, "From ");
const to = formatDateOfYear(schedule.to, "To ");
const { program, device, expanded, routerStore } = this.props;
const { sections } = device;
const { name, running, enabled, schedule, sequence } = program;
const buttonStyle: ButtonProps = { size: "small", compact: true };
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 (
<div>
At {times} <br/>
On {weekdays} <br/>
{from} <br/>
{to}
</div>
<React.Fragment>
{mainRow}
{detailRow}
</React.Fragment>
);
}
private cancelOrRun = () => {
const { program } = this.props;
program.running ? program.cancel() : program.run();
}
private toggleExpanded = () => {
this.props.toggleExpanded(this.props.program);
}
}
@observer
export default class ProgramTable extends React.Component<{
programs: Program[], sections: Section[],
device: SprinklersDevice, routerStore: RouterStore,
}, {
expandedPrograms: Program[],
}> {
@ -51,8 +82,8 @@ export default class ProgramTable extends React.Component<{ @@ -51,8 +82,8 @@ export default class ProgramTable extends React.Component<{
}
render() {
const programRows = Array.prototype.concat.apply([],
this.props.programs.map(this.renderRows));
const { programs, sections } = this.props.device;
const programRows = programs.map(this.renderRows);
return (
<Table celled>
@ -65,6 +96,7 @@ export default class ProgramTable extends React.Component<{ @@ -65,6 +96,7 @@ export default class ProgramTable extends React.Component<{
<Table.HeaderCell className="program--name">Name</Table.HeaderCell>
<Table.HeaderCell className="program--enabled">Enabled?</Table.HeaderCell>
<Table.HeaderCell className="program--running">Running?</Table.HeaderCell>
<Table.HeaderCell className="program--actions">Actions</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
@ -78,42 +110,29 @@ export default class ProgramTable extends React.Component<{ @@ -78,42 +110,29 @@ export default class ProgramTable extends React.Component<{
if (!program) {
return null;
}
const { name, running, enabled, schedule, sequence } = program;
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>
);
const expanded = this.state.expandedPrograms.indexOf(program) !== -1;
return (
<React.Fragment key={i}>
{mainRow}
{detailRow}
</React.Fragment>
<ProgramRows
program={program}
device={this.props.device}
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 @@ @@ -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"; @@ -5,7 +5,9 @@ export { default as DurationInput } from "./DurationInput";
export { default as MessagesView } from "./MessagesView";
export { default as ProgramTable } from "./ProgramTable";
export { default as RunSectionForm } from "./RunSectionForm";
export { default as ScheduleView } from "./ScheduleView";
export { default as SectionRunnerView } from "./SectionRunnerView";
export { default as SectionTable } from "./SectionTable";
export { default as NavBar } from "./NavBar";
export { default as MessageTest } from "./MessageTest";
export { default as ProgramSequenceView } from "./ProgramSequenceView";

69
app/pages/ProgramPage.tsx

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

23
app/routePaths.ts

@ -0,0 +1,23 @@ @@ -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}`;
}

7
app/styles/app.scss

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

Loading…
Cancel
Save