From 9d217a594ac26884c843e38fbaeba8fd433f3c53 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Sun, 22 Jul 2018 14:50:08 -0600 Subject: [PATCH] UI improvements; added program page --- app/components/App.tsx | 10 +- app/components/DeviceView.tsx | 6 +- app/components/DurationInput.tsx | 4 +- app/components/NavBar.tsx | 11 +- app/components/ProgramSequenceView.tsx | 19 +++ app/components/ProgramTable.tsx | 155 ++++++++++++++----------- app/components/ScheduleView.tsx | 38 ++++++ app/components/index.ts | 2 + app/pages/ProgramPage.tsx | 69 +++++++++++ app/pages/index.tsx | 1 + app/routePaths.ts | 23 ++++ app/styles/app.scss | 9 +- 12 files changed, 258 insertions(+), 89 deletions(-) create mode 100644 app/components/ProgramSequenceView.tsx create mode 100644 app/components/ScheduleView.tsx create mode 100644 app/pages/ProgramPage.tsx create mode 100644 app/routePaths.ts diff --git a/app/components/App.tsx b/app/components/App.tsx index 286a31d..400256b 100644 --- a/app/components/App.tsx +++ b/app/components/App.tsx @@ -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() { - - + + + @@ -30,8 +32,8 @@ function NavContainer() { export default function App() { return ( - - + + ); diff --git a/app/components/DeviceView.tsx b/app/components/DeviceView.tsx index 2625ba4..839faf5 100644 --- a/app/components/DeviceView.tsx +++ b/app/components/DeviceView.tsx @@ -46,9 +46,9 @@ interface DeviceViewProps { class DeviceView extends React.Component { 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 && ( @@ -60,7 +60,7 @@ class DeviceView extends React.Component { - + ); return ( diff --git a/app/components/DurationInput.tsx b/app/components/DurationInput.tsx index 27ae26f..f028144 100644 --- a/app/components/DurationInput.tsx +++ b/app/components/DurationInput.tsx @@ -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<{ } private onSecondsChange: InputProps["onChange"] = (e, { value }) => { - if (value.length === 0 || isNaN(Number(value))) { + if (isNaN(Number(value))) { return; } const newSeconds = parseInt(value, 10); diff --git a/app/components/NavBar.tsx b/app/components/NavBar.tsx index f0c684d..3ea7a6c 100644 --- a/app/components/NavBar.tsx +++ b/app/components/NavBar.tsx @@ -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 }) { let loginMenu; if (appState.isLoggedIn) { loginMenu = ( - Logout + Logout ); } else { loginMenu = ( - Login + Login ); } return ( - Device grinklers - Messages test + Device grinklers + Messages test {loginMenu} diff --git a/app/components/ProgramSequenceView.tsx b/app/components/ProgramSequenceView.tsx new file mode 100644 index 0000000..39ea92d --- /dev/null +++ b/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 ( +
  • + "{section.name}" for {duration.toString()} +
  • + ); + }); + return
      {sequenceItems}
    ; +} diff --git a/app/components/ProgramTable.tsx b/app/components/ProgramTable.tsx index 0b6c2d1..64f9508 100644 --- a/app/components/ProgramTable.tsx +++ b/app/components/ProgramTable.tsx @@ -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 = ( + + {"" + program.id} + {name} + {enabled ? "Enabled" : "Not enabled"} + + {running ? "Running" : "Not running"} + + + + + + + + ); + const detailRow = expanded && ( + + +

    Sequence:

    +

    Schedule:

    +
    +
    + ); return ( -
    - At {times}
    - On {weekdays}
    - {from}
    - {to} -
    + + {mainRow} + {detailRow} + ); } + + 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<{ } 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 ( @@ -65,6 +96,7 @@ export default class ProgramTable extends React.Component<{ NameEnabled?Running? + Actions @@ -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 [ - "{section.name}", ` for ${duration.toString()}, `, - ]; - }); - const cancelOrRun = () => running ? program.cancel() : program.run(); - const mainRow = ( - - {"" + (i + 1)} - {name} - {enabled ? "Enabled" : "Not enabled"} - - {running ? "Running" : "Not running"} -
    - - - - ); - const detailRow = false && ( - - -

    Sequence:

    {sequenceItems} -

    Schedule:

    -
    -
    - ); + const expanded = this.state.expandedPrograms.indexOf(program) !== -1; return ( - - {mainRow} - {detailRow} - + ); } + + 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, + }); + } } diff --git a/app/components/ScheduleView.tsx b/app/components/ScheduleView.tsx new file mode 100644 index 0000000..5447074 --- /dev/null +++ b/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 {prefix}{moment(day).format(format)}; +} + +@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, From ); + const to = formatDateOfYear(schedule.to, To ); + return ( +
    + At {times}
    + On {weekdays}
    + {from}
    + {to} +
    + ); + } +} diff --git a/app/components/index.ts b/app/components/index.ts index 7d4f5f2..fe3f345 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -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"; diff --git a/app/pages/ProgramPage.tsx b/app/pages/ProgramPage.tsx new file mode 100644 index 0000000..4dbfe30 --- /dev/null +++ b/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 ( +
    + + Program {program.name} ({program.id}) + + + + + + + + {programRows} + +
    + ); + } + + private renderRows = (program: Program, i: number): JSX.Element | null => { + const { name, running, enabled, schedule, sequence } = program; + const cancelOrRun = () => running ? program.cancel() : program.run(); + return ( + + Enabled: {enabled ? "Enabled" : "Not enabled"}
    + Running: {running ? "Running" : "Not running"}
    + +

    Sequence:

    +

    Schedule:

    +
    + ); + } +} + +const DecoratedProgramPage = injectState(observer(ProgramPage)); +export default DecoratedProgramPage; diff --git a/app/pages/index.tsx b/app/pages/index.tsx index c530c26..48d1c2c 100644 --- a/app/pages/index.tsx +++ b/app/pages/index.tsx @@ -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 ( diff --git a/app/routePaths.ts b/app/routePaths.ts new file mode 100644 index 0000000..ee50b39 --- /dev/null +++ b/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}`; +} diff --git a/app/styles/app.scss b/app/styles/app.scss index 39fd218..b6b461a 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -47,13 +47,8 @@ } -.ui.table { - tr > td.program--running { - display: flex !important; - @media only screen and (min-width: 768px) { - //line-height: 36px; - } - } +.flex { + display: flex !important; } .section--state-true {