Improved error handling
This commit is contained in:
parent
4175482212
commit
1a9c1f5cbc
@ -52,6 +52,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
||||
|
||||
render() {
|
||||
const { id, connectionState, sections, programs, sectionRunner } = this.device;
|
||||
const { uiStore } = this.props.state;
|
||||
return (
|
||||
<Item>
|
||||
<Item.Image src={require("@app/images/raspberry_pi.png")}/>
|
||||
@ -68,7 +69,7 @@ class DeviceView extends React.Component<DeviceViewProps> {
|
||||
<SectionTable sections={sections}/>
|
||||
</Grid.Column>
|
||||
<Grid.Column width="8">
|
||||
<RunSectionForm sections={sections}/>
|
||||
<RunSectionForm sections={sections} uiStore={uiStore}/>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
<ProgramTable programs={programs} sections={sections}/>
|
||||
|
@ -3,14 +3,17 @@ import { observer } from "mobx-react";
|
||||
import * as React from "react";
|
||||
import { DropdownItemProps, DropdownProps, Form, Header, Segment } from "semantic-ui-react";
|
||||
|
||||
import { UiStore } from "@app/state";
|
||||
import { Duration } from "@common/Duration";
|
||||
import log from "@common/logger";
|
||||
import { Section } from "@common/sprinklers";
|
||||
import { RunSectionResponse } from "@common/sprinklers/requests";
|
||||
import DurationInput from "./DurationInput";
|
||||
|
||||
@observer
|
||||
export default class RunSectionForm extends React.Component<{
|
||||
sections: Section[],
|
||||
uiStore: UiStore,
|
||||
}, {
|
||||
duration: Duration,
|
||||
section: number | "",
|
||||
@ -70,8 +73,23 @@ export default class RunSectionForm extends React.Component<{
|
||||
const section: Section = this.props.sections[this.state.section];
|
||||
const { duration } = this.state;
|
||||
section.run(duration.toSeconds())
|
||||
.then((result) => log.debug({ result }, "requested section run"))
|
||||
.catch((err) => log.error(err, "error running section"));
|
||||
.then(this.onRunSuccess)
|
||||
.catch(this.onRunError);
|
||||
}
|
||||
|
||||
private onRunSuccess = (result: RunSectionResponse) => {
|
||||
log.debug({ result }, "requested section run");
|
||||
this.props.uiStore.addMessage({
|
||||
color: "green", header: "Section running",
|
||||
});
|
||||
}
|
||||
|
||||
private onRunError = (err: RunSectionResponse) => {
|
||||
log.error(err, "error running section");
|
||||
this.props.uiStore.addMessage({
|
||||
color: "red", header: "Error running section",
|
||||
content: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
private get isValid(): boolean {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { update } from "serializr";
|
||||
|
||||
import logger from "@common/logger";
|
||||
import { ErrorCode } from "@common/sprinklers/ErrorCode";
|
||||
import * as s from "@common/sprinklers/index";
|
||||
import * as requests from "@common/sprinklers/requests";
|
||||
import * as schema from "@common/sprinklers/schema/index";
|
||||
@ -10,6 +11,8 @@ import { action, autorun, observable } from "mobx";
|
||||
|
||||
const log = logger.child({ source: "websocket" });
|
||||
|
||||
const TIMEOUT_MS = 5000;
|
||||
|
||||
export class WSSprinklersDevice extends s.SprinklersDevice {
|
||||
readonly api: WebSocketApiClient;
|
||||
|
||||
@ -84,14 +87,26 @@ export class WebSocketApiClient implements s.ISprinklersApi {
|
||||
id, deviceName, data: requestData,
|
||||
};
|
||||
const promise = new Promise<requests.Response>((resolve, reject) => {
|
||||
let timeoutHandle: number;
|
||||
this.deviceResponseCallbacks[id] = (resData) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
delete this.deviceResponseCallbacks[id];
|
||||
if (resData.data.result === "success") {
|
||||
resolve(resData.data);
|
||||
} else {
|
||||
reject(resData.data);
|
||||
}
|
||||
delete this.deviceResponseCallbacks[id];
|
||||
};
|
||||
timeoutHandle = setTimeout(() => {
|
||||
delete this.deviceResponseCallbacks[id];
|
||||
const res: requests.RunSectionResponse = {
|
||||
type: "runSection",
|
||||
result: "error",
|
||||
code: ErrorCode.Timeout,
|
||||
message: "the request timed out",
|
||||
};
|
||||
reject(res);
|
||||
}, TIMEOUT_MS);
|
||||
});
|
||||
this.socket.send(JSON.stringify(data));
|
||||
return promise;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ISprinklersApi } from "@common/sprinklers";
|
||||
import { UiStore } from "./ui";
|
||||
import { UiStore } from "./UiStore";
|
||||
|
||||
export default abstract class StateBase {
|
||||
abstract readonly sprinklersApi: ISprinklersApi;
|
||||
|
@ -1,3 +1,3 @@
|
||||
export { UiMessage, UiStore } from "./ui";
|
||||
export * from "./inject";
|
||||
export { UiMessage, UiStore } from "./UiStore";
|
||||
export * from "./reactContext";
|
||||
export { default as StateBase } from "./StateBase";
|
||||
|
@ -1,44 +0,0 @@
|
||||
import * as PropTypes from "prop-types";
|
||||
import * as React from "react";
|
||||
|
||||
import { StateBase } from "@app/state";
|
||||
|
||||
interface IProvidedStateContext {
|
||||
providedState: StateBase;
|
||||
}
|
||||
|
||||
const providedStateContextTypes: PropTypes.ValidationMap<any> = {
|
||||
providedState: PropTypes.object,
|
||||
};
|
||||
|
||||
export class ProvideState extends React.Component<{
|
||||
state: StateBase,
|
||||
}> implements React.ChildContextProvider<IProvidedStateContext> {
|
||||
static childContextTypes = providedStateContextTypes;
|
||||
|
||||
getChildContext(): IProvidedStateContext {
|
||||
return {
|
||||
providedState: this.props.state,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.Children.only(this.props.children);
|
||||
}
|
||||
}
|
||||
|
||||
type Diff<T extends string | number | symbol, U extends string | number | symbol> =
|
||||
({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
|
||||
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
|
||||
|
||||
export function injectState<P extends { "state": StateBase }>(Component: React.ComponentType<P>) {
|
||||
return class extends React.Component<Omit<P, "state">> {
|
||||
static contextTypes = providedStateContextTypes;
|
||||
context!: IProvidedStateContext;
|
||||
|
||||
render() {
|
||||
const state = this.context.providedState;
|
||||
return <Component {...this.props} state={state} />;
|
||||
}
|
||||
};
|
||||
}
|
50
app/state/reactContext.tsx
Normal file
50
app/state/reactContext.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { StateBase } from "@app/state";
|
||||
|
||||
const StateContext = React.createContext<StateBase | null>(null);
|
||||
|
||||
export interface ProvideStateProps {
|
||||
state: StateBase;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ProvideState({state, children}: ProvideStateProps) {
|
||||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
{children}
|
||||
</StateContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ConsumeStateProps {
|
||||
children: (state: StateBase) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function ConsumeState({children}: ConsumeStateProps) {
|
||||
const consumeState = (state: StateBase | null) => {
|
||||
if (state == null) {
|
||||
throw new Error("Component with ConsumeState must be mounted inside ProvideState");
|
||||
}
|
||||
return children(state);
|
||||
};
|
||||
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
|
||||
}
|
||||
|
||||
type Diff<T extends string | number | symbol, U extends string | number | symbol> =
|
||||
({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
|
||||
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
|
||||
|
||||
export function injectState<P extends { state: StateBase }>(Component: React.ComponentType<P>) {
|
||||
return class extends React.Component<Omit<P, "state">> {
|
||||
render() {
|
||||
const consumeState = (state: StateBase | null) => {
|
||||
if (state == null) {
|
||||
throw new Error("Component with injectState must be mounted inside ProvideState");
|
||||
}
|
||||
return <Component {...this.props} state={state}/>;
|
||||
};
|
||||
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
|
||||
}
|
||||
};
|
||||
}
|
9
common/sprinklers/ErrorCode.ts
Normal file
9
common/sprinklers/ErrorCode.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum ErrorCode {
|
||||
BadRequest = 100,
|
||||
NotSpecified = 101,
|
||||
Parse = 102,
|
||||
Range = 103,
|
||||
InvalidData = 104,
|
||||
Internal = 200,
|
||||
Timeout = 300,
|
||||
}
|
@ -37,9 +37,10 @@ export interface SuccessResponseData<Type extends string = string> extends WithT
|
||||
|
||||
export interface ErrorResponseData<Type extends string = string> extends WithType<Type> {
|
||||
result: "error";
|
||||
error: string;
|
||||
offset?: number;
|
||||
code?: number;
|
||||
message: string;
|
||||
code: number;
|
||||
name?: string;
|
||||
cause?: any;
|
||||
}
|
||||
|
||||
export type Response<Type extends string = string, Res = {}> =
|
||||
|
@ -33,11 +33,11 @@ export class WebSocketApi {
|
||||
const stop = () => {
|
||||
disposers.forEach((disposer) => disposer());
|
||||
};
|
||||
socket.on("message", this.handleSocketMessage);
|
||||
socket.on("message", (data) => this.handleSocketMessage(socket, data));
|
||||
socket.on("close", () => stop());
|
||||
}
|
||||
|
||||
private handleSocketMessage = (socket: WebSocket, socketData: WebSocket.Data) => {
|
||||
private handleSocketMessage(socket: WebSocket, socketData: WebSocket.Data) {
|
||||
if (typeof socketData !== "string") {
|
||||
return log.error({ type: typeof socketData }, "received invalid socket data type from client");
|
||||
}
|
||||
|
24
yarn.lock
24
yarn.lock
@ -168,7 +168,7 @@
|
||||
|
||||
"@webassemblyjs/helper-code-frame@1.5.12":
|
||||
version "1.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.12.tgz#3cdc1953093760d1c0f0caf745ccd62bdb6627c7"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-errorCode-frame/-/helper-errorCode-frame-1.5.12.tgz#3cdc1953093760d1c0f0caf745ccd62bdb6627c7"
|
||||
dependencies:
|
||||
"@webassemblyjs/wast-printer" "1.5.12"
|
||||
|
||||
@ -265,7 +265,7 @@
|
||||
"@webassemblyjs/ast" "1.5.12"
|
||||
"@webassemblyjs/floating-point-hex-parser" "1.5.12"
|
||||
"@webassemblyjs/helper-api-error" "1.5.12"
|
||||
"@webassemblyjs/helper-code-frame" "1.5.12"
|
||||
"@webassemblyjs/helper-errorCode-frame" "1.5.12"
|
||||
"@webassemblyjs/helper-fsm" "1.5.12"
|
||||
long "^3.2.0"
|
||||
mamacro "^0.0.3"
|
||||
@ -564,7 +564,7 @@ aws4@^1.2.1:
|
||||
|
||||
babel-code-frame@6.26.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
resolved "https://registry.yarnpkg.com/babel-errorCode-frame/-/babel-errorCode-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
esutils "^2.0.2"
|
||||
@ -1076,7 +1076,7 @@ coa@~1.0.1:
|
||||
|
||||
code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
resolved "https://registry.yarnpkg.com/errorCode-point-at/-/errorCode-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
|
||||
collection-visit@^1.0.0:
|
||||
version "1.0.0"
|
||||
@ -1392,7 +1392,7 @@ css-loader@^0.28.11:
|
||||
version "0.28.11"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.11.tgz#c3f9864a700be2711bb5a2462b2389b1a392dab7"
|
||||
dependencies:
|
||||
babel-code-frame "^6.26.0"
|
||||
babel-errorCode-frame "^6.26.0"
|
||||
css-selector-tokenizer "^0.7.0"
|
||||
cssnano "^3.10.0"
|
||||
icss-utils "^2.1.0"
|
||||
@ -2960,13 +2960,13 @@ is-finite@^1.0.0:
|
||||
|
||||
is-fullwidth-code-point@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-errorCode-point/-/is-fullwidth-errorCode-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
|
||||
dependencies:
|
||||
number-is-nan "^1.0.0"
|
||||
|
||||
is-fullwidth-code-point@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-errorCode-point/-/is-fullwidth-errorCode-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
|
||||
|
||||
is-glob@^3.1.0:
|
||||
version "3.1.0"
|
||||
@ -5083,7 +5083,7 @@ react-dev-utils@^5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
|
||||
dependencies:
|
||||
address "1.0.3"
|
||||
babel-code-frame "6.26.0"
|
||||
babel-errorCode-frame "6.26.0"
|
||||
chalk "1.1.3"
|
||||
cross-spawn "5.1.0"
|
||||
detect-port-alt "1.1.6"
|
||||
@ -5973,15 +5973,15 @@ string-width@^1.0.1, string-width@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
|
||||
dependencies:
|
||||
code-point-at "^1.0.0"
|
||||
is-fullwidth-code-point "^1.0.0"
|
||||
errorCode-point-at "^1.0.0"
|
||||
is-fullwidth-errorCode-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
dependencies:
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
is-fullwidth-errorCode-point "^2.0.0"
|
||||
strip-ansi "^4.0.0"
|
||||
|
||||
string.prototype.padend@^3.0.0:
|
||||
@ -6242,7 +6242,7 @@ tslint@^5.10.0:
|
||||
version "5.10.0"
|
||||
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3"
|
||||
dependencies:
|
||||
babel-code-frame "^6.22.0"
|
||||
babel-errorCode-frame "^6.22.0"
|
||||
builtin-modules "^1.1.1"
|
||||
chalk "^2.3.0"
|
||||
commander "^2.12.1"
|
||||
|
Loading…
x
Reference in New Issue
Block a user