UI improvements; added program page
This commit is contained in:
parent
411b2ff045
commit
9d217a594a
@ -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() {
|
||||
<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() {
|
||||
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>
|
||||
);
|
||||
|
@ -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> {
|
||||
<RunSectionForm device={device} uiStore={uiStore}/>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
<ProgramTable programs={programs} sections={sections}/>
|
||||
<ProgramTable device={device} routerStore={routerStore}/>
|
||||
</React.Fragment>
|
||||
);
|
||||
return (
|
||||
|
@ -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);
|
||||
|
@ -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 = (
|
||||
<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
Normal file
19
app/components/ProgramSequenceView.tsx
Normal file
@ -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>;
|
||||
}
|
@ -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 ");
|
||||
return (
|
||||
<div>
|
||||
At {times} <br/>
|
||||
On {weekdays} <br/>
|
||||
{from} <br/>
|
||||
{to}
|
||||
</div>
|
||||
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 (
|
||||
<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<{
|
||||
}
|
||||
|
||||
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<{
|
||||
<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<{
|
||||
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
Normal file
38
app/components/ScheduleView.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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
Normal file
69
app/pages/ProgramPage.tsx
Normal file
@ -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;
|
@ -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
Normal file
23
app/routePaths.ts
Normal file
@ -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}`;
|
||||
}
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user