Use mobx actions on client side
This commit is contained in:
parent
e35f9bf0d9
commit
60cabb9e57
@ -8,6 +8,7 @@ import { DeviceImage } from "@client/components";
|
|||||||
import * as p from "@client/pages";
|
import * as p from "@client/pages";
|
||||||
import * as route from "@client/routePaths";
|
import * as route from "@client/routePaths";
|
||||||
import { AppState, injectState } from "@client/state";
|
import { AppState, injectState } from "@client/state";
|
||||||
|
import { ISprinklersDevice } from "@common/httpApi";
|
||||||
import { ConnectionState as ConState, SprinklersDevice } from "@common/sprinklersRpc";
|
import { ConnectionState as ConState, SprinklersDevice } from "@common/sprinklersRpc";
|
||||||
import { Route, RouteComponentProps, withRouter } from "react-router";
|
import { Route, RouteComponentProps, withRouter } from "react-router";
|
||||||
import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from ".";
|
import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from ".";
|
||||||
@ -52,7 +53,7 @@ interface DeviceViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<any>> {
|
class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<any>> {
|
||||||
renderBody(device: SprinklersDevice) {
|
renderBody(iDevice: ISprinklersDevice, device: SprinklersDevice) {
|
||||||
const { inList, appState: { uiStore, routerStore } } = this.props;
|
const { inList, appState: { uiStore, routerStore } } = this.props;
|
||||||
const { connectionState, sectionRunner, sections } = device;
|
const { connectionState, sectionRunner, sections } = device;
|
||||||
if (!connectionState.isAvailable || inList) {
|
if (!connectionState.isAvailable || inList) {
|
||||||
@ -71,7 +72,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a
|
|||||||
<RunSectionForm device={device} uiStore={uiStore} />
|
<RunSectionForm device={device} uiStore={uiStore} />
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
<ProgramTable device={device} routerStore={routerStore} />
|
<ProgramTable iDevice={iDevice} device={device} routerStore={routerStore} />
|
||||||
<Route path={route.program(":deviceId", ":programId")} component={p.ProgramPage} />
|
<Route path={route.program(":deviceId", ":programId")} component={p.ProgramPage} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
@ -79,10 +80,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { deviceId, inList, appState: { sprinklersRpc, userStore } } = this.props;
|
const { deviceId, inList, appState: { sprinklersRpc, userStore } } = this.props;
|
||||||
const { userData } = userStore;
|
const iDevice = userStore.findDevice(deviceId);
|
||||||
const iDevice = userData &&
|
|
||||||
userData.devices &&
|
|
||||||
userData.devices.find((dev) => dev.id === deviceId);
|
|
||||||
let itemContent: React.ReactNode;
|
let itemContent: React.ReactNode;
|
||||||
if (!iDevice || !iDevice.deviceId) {
|
if (!iDevice || !iDevice.deviceId) {
|
||||||
// TODO: better and link back to devices list
|
// TODO: better and link back to devices list
|
||||||
@ -107,7 +105,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a
|
|||||||
<Item.Meta>
|
<Item.Meta>
|
||||||
Raspberry Pi Grinklers Device
|
Raspberry Pi Grinklers Device
|
||||||
</Item.Meta>
|
</Item.Meta>
|
||||||
{this.renderBody(device)}
|
{this.renderBody(iDevice, device)}
|
||||||
</Item.Content>
|
</Item.Content>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
@ -9,6 +9,7 @@ import { Duration } from "@common/Duration";
|
|||||||
import { ProgramItem, Section } from "@common/sprinklersRpc";
|
import { ProgramItem, Section } from "@common/sprinklersRpc";
|
||||||
|
|
||||||
import "@client/styles/ProgramSequenceView";
|
import "@client/styles/ProgramSequenceView";
|
||||||
|
import { action } from "mobx";
|
||||||
|
|
||||||
type ItemChangeHandler = (index: number, newItem: ProgramItem) => void;
|
type ItemChangeHandler = (index: number, newItem: ProgramItem) => void;
|
||||||
type ItemRemoveHandler = (index: number) => void;
|
type ItemRemoveHandler = (index: number) => void;
|
||||||
@ -147,15 +148,18 @@ class ProgramSequenceView extends React.Component<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
private changeItem: ItemChangeHandler = (index, newItem) => {
|
private changeItem: ItemChangeHandler = (index, newItem) => {
|
||||||
this.props.sequence[index] = newItem;
|
this.props.sequence[index] = newItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
private removeItem: ItemRemoveHandler = (index) => {
|
private removeItem: ItemRemoveHandler = (index) => {
|
||||||
this.props.sequence.splice(index, 1);
|
this.props.sequence.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addItem = () => {
|
@action.bound
|
||||||
|
private addItem() {
|
||||||
let sectionId = 0;
|
let sectionId = 0;
|
||||||
for (const section of this.props.sections) {
|
for (const section of this.props.sections) {
|
||||||
const sectionNotIncluded = this.props.sequence
|
const sectionNotIncluded = this.props.sequence
|
||||||
@ -173,7 +177,8 @@ class ProgramSequenceView extends React.Component<{
|
|||||||
this.props.sequence.push(item);
|
this.props.sequence.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSortEnd = ({oldIndex, newIndex}: SortEnd) => {
|
@action.bound
|
||||||
|
private onSortEnd({oldIndex, newIndex}: SortEnd) {
|
||||||
const { sequence: array } = this.props;
|
const { sequence: array } = this.props;
|
||||||
if (newIndex >= array.length) {
|
if (newIndex >= array.length) {
|
||||||
return;
|
return;
|
||||||
|
@ -6,22 +6,25 @@ import { Button, ButtonProps, Form, Icon, Table } from "semantic-ui-react";
|
|||||||
|
|
||||||
import { ProgramSequenceView, ScheduleView } from "@client/components";
|
import { ProgramSequenceView, ScheduleView } from "@client/components";
|
||||||
import * as route from "@client/routePaths";
|
import * as route from "@client/routePaths";
|
||||||
|
import { ISprinklersDevice } from "@common/httpApi";
|
||||||
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class ProgramRows extends React.Component<{
|
class ProgramRows extends React.Component<{
|
||||||
program: Program, device: SprinklersDevice,
|
program: Program,
|
||||||
|
iDevice: ISprinklersDevice,
|
||||||
|
device: SprinklersDevice,
|
||||||
routerStore: RouterStore,
|
routerStore: RouterStore,
|
||||||
expanded: boolean, toggleExpanded: (program: Program) => void,
|
expanded: boolean, toggleExpanded: (program: Program) => void,
|
||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const { program, device, expanded } = this.props;
|
const { program, iDevice, device, expanded } = this.props;
|
||||||
const { sections } = device;
|
const { sections } = device;
|
||||||
|
|
||||||
const { name, running, enabled, schedule, sequence } = program;
|
const { name, running, enabled, schedule, sequence } = program;
|
||||||
|
|
||||||
const buttonStyle: ButtonProps = { size: "small", compact: false };
|
const buttonStyle: ButtonProps = { size: "small", compact: false };
|
||||||
const detailUrl = route.program(device.id, program.id);
|
const detailUrl = route.program(iDevice.id, program.id);
|
||||||
|
|
||||||
const stopStartButton = (
|
const stopStartButton = (
|
||||||
<Button onClick={this.cancelOrRun} {...buttonStyle} positive={!running} negative={running}>
|
<Button onClick={this.cancelOrRun} {...buttonStyle} positive={!running} negative={running}>
|
||||||
@ -81,7 +84,7 @@ class ProgramRows extends React.Component<{
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export default class ProgramTable extends React.Component<{
|
export default class ProgramTable extends React.Component<{
|
||||||
device: SprinklersDevice, routerStore: RouterStore,
|
iDevice: ISprinklersDevice, device: SprinklersDevice, routerStore: RouterStore,
|
||||||
}, {
|
}, {
|
||||||
expandedPrograms: Program[],
|
expandedPrograms: Program[],
|
||||||
}> {
|
}> {
|
||||||
@ -123,6 +126,7 @@ export default class ProgramTable extends React.Component<{
|
|||||||
return (
|
return (
|
||||||
<ProgramRows
|
<ProgramRows
|
||||||
program={program}
|
program={program}
|
||||||
|
iDevice={this.props.iDevice}
|
||||||
device={this.props.device}
|
device={this.props.device}
|
||||||
routerStore={this.props.routerStore}
|
routerStore={this.props.routerStore}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
|
@ -8,6 +8,7 @@ import ScheduleTimes from "./ScheduleTimes";
|
|||||||
import WeekdaysView from "./WeekdaysView";
|
import WeekdaysView from "./WeekdaysView";
|
||||||
|
|
||||||
import "@client/styles/ScheduleView";
|
import "@client/styles/ScheduleView";
|
||||||
|
import { action } from "mobx";
|
||||||
|
|
||||||
export interface ScheduleViewProps {
|
export interface ScheduleViewProps {
|
||||||
label?: string | React.ReactNode | undefined;
|
label?: string | React.ReactNode | undefined;
|
||||||
@ -39,19 +40,23 @@ export default class ScheduleView extends React.Component<ScheduleViewProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTimes = (newTimes: TimeOfDay[]) => {
|
@action.bound
|
||||||
|
private updateTimes(newTimes: TimeOfDay[]) {
|
||||||
this.props.schedule.times = newTimes;
|
this.props.schedule.times = newTimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateWeekdays = (newWeekdays: Weekday[]) => {
|
@action.bound
|
||||||
|
private updateWeekdays(newWeekdays: Weekday[]) {
|
||||||
this.props.schedule.weekdays = newWeekdays;
|
this.props.schedule.weekdays = newWeekdays;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateFromDate = (newFromDate: DateOfYear | null) => {
|
@action.bound
|
||||||
|
private updateFromDate(newFromDate: DateOfYear | null) {
|
||||||
this.props.schedule.from = newFromDate;
|
this.props.schedule.from = newFromDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateToDate = (newToDate: DateOfYear | null) => {
|
@action.bound
|
||||||
|
private updateToDate(newToDate: DateOfYear | null) {
|
||||||
this.props.schedule.to = newToDate;
|
this.props.schedule.to = newToDate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { computed, observable } from "mobx";
|
import { action, computed, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Container, Dimmer, Form, Header, InputOnChangeData, Loader, Message, Segment } from "semantic-ui-react";
|
import { Container, Dimmer, Form, Header, InputOnChangeData, Loader, Message, Segment } from "semantic-ui-react";
|
||||||
@ -19,28 +19,31 @@ class LoginPageState {
|
|||||||
return this.username.length > 0 && this.password.length > 0;
|
return this.username.length > 0 && this.password.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUsernameChange = (e: any, data: InputOnChangeData) => {
|
@action.bound
|
||||||
|
onUsernameChange(e: any, data: InputOnChangeData) {
|
||||||
this.username = data.value;
|
this.username = data.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPasswordChange = (e: any, data: InputOnChangeData) => {
|
@action.bound
|
||||||
|
onPasswordChange(e: any, data: InputOnChangeData) {
|
||||||
this.password = data.value;
|
this.password = data.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
login(appState: AppState) {
|
login(appState: AppState) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
appState.httpApi.grantPassword(this.username, this.password)
|
appState.httpApi.grantPassword(this.username, this.password)
|
||||||
.then(() => {
|
.then(action("loginSuccess", () => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
log.info("logged in");
|
log.info("logged in");
|
||||||
appState.history.push("/");
|
appState.history.push("/");
|
||||||
})
|
}))
|
||||||
.catch((err) => {
|
.catch(action("loginError", (err: any) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.error = err.message;
|
this.error = err.message;
|
||||||
log.error({ err }, "login error");
|
log.error({ err }, "login error");
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,8 +8,10 @@ import { Button, CheckboxProps, Form, Icon, Input, InputOnChangeData, Menu, Moda
|
|||||||
import { ProgramSequenceView, ScheduleView } from "@client/components";
|
import { ProgramSequenceView, ScheduleView } from "@client/components";
|
||||||
import * as route from "@client/routePaths";
|
import * as route from "@client/routePaths";
|
||||||
import { AppState, injectState } from "@client/state";
|
import { AppState, injectState } from "@client/state";
|
||||||
|
import { ISprinklersDevice } from "@common/httpApi";
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
|
||||||
|
import { action } from "mobx";
|
||||||
|
|
||||||
interface ProgramPageProps extends RouteComponentProps<{ deviceId: string, programId: string }> {
|
interface ProgramPageProps extends RouteComponentProps<{ deviceId: string, programId: string }> {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
@ -21,6 +23,7 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
return qs.parse(this.props.location.search).editing != null;
|
return qs.parse(this.props.location.search).editing != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iDevice!: ISprinklersDevice;
|
||||||
device!: SprinklersDevice;
|
device!: SprinklersDevice;
|
||||||
program!: Program;
|
program!: Program;
|
||||||
programView: Program | null = null;
|
programView: Program | null = null;
|
||||||
@ -95,11 +98,16 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { deviceId, programId: pid } = this.props.match.params;
|
const { deviceId: did, programId: pid } = this.props.match.params;
|
||||||
|
const { userStore, sprinklersRpc } = this.props.appState;
|
||||||
|
const deviceId = Number(did);
|
||||||
const programId = Number(pid);
|
const programId = Number(pid);
|
||||||
// tslint:disable-next-line:prefer-conditional-expression
|
// tslint:disable-next-line:prefer-conditional-expression
|
||||||
if (!this.device || this.device.id !== deviceId) {
|
if (!this.iDevice || this.iDevice.id !== deviceId) {
|
||||||
this.device = this.props.appState.sprinklersRpc.getDevice(deviceId);
|
this.iDevice = userStore.findDevice(deviceId)!;
|
||||||
|
}
|
||||||
|
if (this.iDevice && this.iDevice.deviceId && (!this.device || this.device.id !== this.iDevice.deviceId)) {
|
||||||
|
this.device = sprinklersRpc.getDevice(this.iDevice.deviceId);
|
||||||
}
|
}
|
||||||
// tslint:disable-next-line:prefer-conditional-expression
|
// tslint:disable-next-line:prefer-conditional-expression
|
||||||
if (!this.program || this.program.id !== programId) {
|
if (!this.program || this.program.id !== programId) {
|
||||||
@ -158,18 +166,21 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private cancelOrRun = () => {
|
@action.bound
|
||||||
|
private cancelOrRun() {
|
||||||
if (!this.program) {
|
if (!this.program) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.program.running ? this.program.cancel() : this.program.run();
|
this.program.running ? this.program.cancel() : this.program.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private startEditing = () => {
|
@action.bound
|
||||||
|
private startEditing() {
|
||||||
this.props.history.push({ search: qs.stringify({ editing: true }) });
|
this.props.history.push({ search: qs.stringify({ editing: true }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
private save = () => {
|
@action.bound
|
||||||
|
private save() {
|
||||||
if (!this.programView || !this.program) {
|
if (!this.programView || !this.program) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -183,22 +194,26 @@ class ProgramPage extends React.Component<ProgramPageProps> {
|
|||||||
this.stopEditing();
|
this.stopEditing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopEditing = () => {
|
@action.bound
|
||||||
|
private stopEditing() {
|
||||||
this.props.history.push({ search: "" });
|
this.props.history.push({ search: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private close = () => {
|
@action.bound
|
||||||
|
private close() {
|
||||||
const { deviceId } = this.props.match.params;
|
const { deviceId } = this.props.match.params;
|
||||||
this.props.history.push({ pathname: route.device(deviceId), search: "" });
|
this.props.history.push({ pathname: route.device(deviceId), search: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onNameChange = (e: any, p: InputOnChangeData) => {
|
@action.bound
|
||||||
|
private onNameChange(e: any, p: InputOnChangeData) {
|
||||||
if (this.programView) {
|
if (this.programView) {
|
||||||
this.programView.name = p.value;
|
this.programView.name = p.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEnabledChange = (e: any, p: CheckboxProps) => {
|
@action.bound
|
||||||
|
private onEnabledChange(e: any, p: CheckboxProps) {
|
||||||
if (this.programView) {
|
if (this.programView) {
|
||||||
this.programView.enabled = p.checked!;
|
this.programView.enabled = p.checked!;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { action, autorun, observable, when } from "mobx";
|
import { action, autorun, observable, runInAction, when } from "mobx";
|
||||||
import { update } from "serializr";
|
import { update } from "serializr";
|
||||||
|
|
||||||
import { TokenStore } from "@client/state/TokenStore";
|
import { TokenStore } from "@client/state/TokenStore";
|
||||||
@ -35,10 +35,7 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
|
|||||||
this.api = api;
|
this.api = api;
|
||||||
this._id = id;
|
this._id = id;
|
||||||
|
|
||||||
autorun(() => {
|
autorun(this.updateConnectionState);
|
||||||
this.connectionState.clientToServer = this.api.connectionState.clientToServer;
|
|
||||||
this.connectionState.serverToBroker = this.api.connectionState.serverToBroker;
|
|
||||||
});
|
|
||||||
this.waitSubscribe();
|
this.waitSubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,20 +43,31 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
|
|||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateConnectionState = () => {
|
||||||
|
const { clientToServer, serverToBroker } = this.api.connectionState;
|
||||||
|
runInAction("updateConnectionState", () => {
|
||||||
|
Object.assign(this.connectionState, { clientToServer, serverToBroker });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async subscribe() {
|
async subscribe() {
|
||||||
const subscribeRequest: ws.IDeviceSubscribeRequest = {
|
const subscribeRequest: ws.IDeviceSubscribeRequest = {
|
||||||
deviceId: this.id,
|
deviceId: this.id,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await this.api.makeRequest("deviceSubscribe", subscribeRequest);
|
await this.api.makeRequest("deviceSubscribe", subscribeRequest);
|
||||||
this.connectionState.brokerToDevice = true;
|
runInAction("deviceSubscribeSuccess", () => {
|
||||||
|
this.connectionState.brokerToDevice = true;
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.connectionState.brokerToDevice = false;
|
runInAction("deviceSubscribeError", () => {
|
||||||
if ((err as ws.IError).code === ErrorCode.NoPermission) {
|
this.connectionState.brokerToDevice = false;
|
||||||
this.connectionState.hasPermission = false;
|
if ((err as ws.IError).code === ErrorCode.NoPermission) {
|
||||||
} else {
|
this.connectionState.hasPermission = false;
|
||||||
log.error({ err });
|
} else {
|
||||||
}
|
log.error({ err });
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,7 +151,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
const res = await this.authenticate(this.tokenStore.accessToken.token!);
|
const res = await this.authenticate(this.tokenStore.accessToken.token!);
|
||||||
this.authenticated = res.authenticated;
|
this.authenticated = res.authenticated;
|
||||||
logger.info({ user: res.user }, "authenticated websocket connection");
|
logger.info({ user: res.user }, "authenticated websocket connection");
|
||||||
this.userStore.userData = res.user;
|
this.userStore.receiveUserData(res.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, "error authenticating websocket connection");
|
logger.error({ err }, "error authenticating websocket connection");
|
||||||
// TODO message?
|
// TODO message?
|
||||||
@ -288,7 +296,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
try {
|
try {
|
||||||
rpc.handleNotification(this.notificationHandlers, data);
|
rpc.handleNotification(this.notificationHandlers, data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, "error handling server notification");
|
logger.error(err, "error handling server notification");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,19 +308,31 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private notificationHandlers: ws.ServerNotificationHandlers = {
|
private notificationHandlers = new WSClientNotificationHandlers(this);
|
||||||
brokerConnectionUpdate: (data: ws.IBrokerConnectionUpdate) => {
|
|
||||||
this.connectionState.serverToBroker = data.brokerConnected;
|
|
||||||
},
|
|
||||||
deviceUpdate: (data: ws.IDeviceUpdate) => {
|
|
||||||
const device = this.devices.get(data.deviceId);
|
|
||||||
if (!device) {
|
|
||||||
return log.warn({ data }, "invalid deviceUpdate received");
|
|
||||||
}
|
|
||||||
update(schema.sprinklersDevice, device, data.data);
|
|
||||||
},
|
|
||||||
error: (data: ws.IError) => {
|
|
||||||
log.warn({ err: data }, "server error");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WSClientNotificationHandlers implements ws.ServerNotificationHandlers {
|
||||||
|
client: WebSocketRpcClient;
|
||||||
|
|
||||||
|
constructor(client: WebSocketRpcClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
brokerConnectionUpdate(data: ws.IBrokerConnectionUpdate) {
|
||||||
|
this.client.connectionState.serverToBroker = data.brokerConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
deviceUpdate(data: ws.IDeviceUpdate) {
|
||||||
|
const device = this.client.devices.get(data.deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return log.warn({ data }, "invalid deviceUpdate received");
|
||||||
|
}
|
||||||
|
update(schema.sprinklersDevice, device, data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(data: ws.IError) {
|
||||||
|
log.warn({ err: data }, "server error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { createBrowserHistory, History } from "history";
|
import { createBrowserHistory, History } from "history";
|
||||||
import { computed } from "mobx";
|
import { computed, configure } from "mobx";
|
||||||
import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
|
import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
|
||||||
|
|
||||||
import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient";
|
import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient";
|
||||||
@ -24,6 +24,10 @@ export default class AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
configure({
|
||||||
|
enforceActions: true,
|
||||||
|
});
|
||||||
|
|
||||||
syncHistoryWithStore(this.history, this.routerStore);
|
syncHistoryWithStore(this.history, this.routerStore);
|
||||||
this.tokenStore.loadLocalStorage();
|
this.tokenStore.loadLocalStorage();
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import ApiError from "@common/ApiError";
|
|||||||
import { ErrorCode } from "@common/ErrorCode";
|
import { ErrorCode } from "@common/ErrorCode";
|
||||||
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi";
|
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi";
|
||||||
import log from "@common/logger";
|
import log from "@common/logger";
|
||||||
|
import { runInAction } from "mobx";
|
||||||
|
|
||||||
export { ApiError };
|
export { ApiError };
|
||||||
|
|
||||||
@ -58,9 +59,11 @@ export default class HttpApi {
|
|||||||
const response: TokenGrantResponse = await this.makeRequest("/token/grant", {
|
const response: TokenGrantResponse = await this.makeRequest("/token/grant", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}, request);
|
}, request);
|
||||||
this.tokenStore.accessToken.token = response.access_token;
|
runInAction("grantPasswordSuccess", () => {
|
||||||
this.tokenStore.refreshToken.token = response.refresh_token;
|
this.tokenStore.accessToken.token = response.access_token;
|
||||||
this.tokenStore.saveLocalStorage();
|
this.tokenStore.refreshToken.token = response.refresh_token;
|
||||||
|
this.tokenStore.saveLocalStorage();
|
||||||
|
});
|
||||||
const { accessToken } = this.tokenStore;
|
const { accessToken } = this.tokenStore;
|
||||||
log.debug({ aud: accessToken.claims!.aud }, "got password grant tokens");
|
log.debug({ aud: accessToken.claims!.aud }, "got password grant tokens");
|
||||||
}
|
}
|
||||||
@ -76,9 +79,11 @@ export default class HttpApi {
|
|||||||
const response: TokenGrantResponse = await this.makeRequest("/token/grant", {
|
const response: TokenGrantResponse = await this.makeRequest("/token/grant", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}, request);
|
}, request);
|
||||||
this.tokenStore.accessToken.token = response.access_token;
|
runInAction("grantRefreshSuccess", () => {
|
||||||
this.tokenStore.refreshToken.token = response.refresh_token;
|
this.tokenStore.accessToken.token = response.access_token;
|
||||||
this.tokenStore.saveLocalStorage();
|
this.tokenStore.refreshToken.token = response.refresh_token;
|
||||||
|
this.tokenStore.saveLocalStorage();
|
||||||
|
});
|
||||||
const { accessToken } = this.tokenStore;
|
const { accessToken } = this.tokenStore;
|
||||||
log.debug({ aud: accessToken.claims!.aud }, "got refresh grant tokens");
|
log.debug({ aud: accessToken.claims!.aud }, "got refresh grant tokens");
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
|
|
||||||
import { Token } from "@client/state/Token";
|
import { Token } from "@client/state/Token";
|
||||||
import { AccessToken, RefreshToken } from "@common/TokenClaims";
|
import { AccessToken, RefreshToken } from "@common/TokenClaims";
|
||||||
@ -9,16 +9,19 @@ export class TokenStore {
|
|||||||
@observable accessToken: Token<AccessToken> = new Token();
|
@observable accessToken: Token<AccessToken> = new Token();
|
||||||
@observable refreshToken: Token<RefreshToken> = new Token();
|
@observable refreshToken: Token<RefreshToken> = new Token();
|
||||||
|
|
||||||
|
@action
|
||||||
clear() {
|
clear() {
|
||||||
this.accessToken.token = null;
|
this.accessToken.token = null;
|
||||||
this.refreshToken.token = null;
|
this.refreshToken.token = null;
|
||||||
this.saveLocalStorage();
|
this.saveLocalStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
saveLocalStorage() {
|
saveLocalStorage() {
|
||||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.toJSON()));
|
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.toJSON()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
loadLocalStorage() {
|
loadLocalStorage() {
|
||||||
const data = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
const data = window.localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -31,6 +34,7 @@ export class TokenStore {
|
|||||||
return { accessToken: this.accessToken.toJSON(), refreshToken: this.refreshToken.toJSON() };
|
return { accessToken: this.accessToken.toJSON(), refreshToken: this.refreshToken.toJSON() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
updateFromJson(json: any) {
|
updateFromJson(json: any) {
|
||||||
this.accessToken.token = json.accessToken;
|
this.accessToken.token = json.accessToken;
|
||||||
this.refreshToken.token = json.refreshToken;
|
this.refreshToken.token = json.refreshToken;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IObservableArray, observable } from "mobx";
|
import { action, IObservableArray, observable } from "mobx";
|
||||||
import { MessageProps } from "semantic-ui-react";
|
import { MessageProps } from "semantic-ui-react";
|
||||||
|
|
||||||
import { getRandomId } from "@common/utils";
|
import { getRandomId } from "@common/utils";
|
||||||
@ -14,7 +14,8 @@ export interface UiMessageProps extends MessageProps {
|
|||||||
export class UiStore {
|
export class UiStore {
|
||||||
messages: IObservableArray<UiMessage> = observable.array();
|
messages: IObservableArray<UiMessage> = observable.array();
|
||||||
|
|
||||||
addMessage(message: UiMessageProps) {
|
@action
|
||||||
|
addMessage(message: UiMessageProps): UiMessage {
|
||||||
const { timeout, ...otherProps } = message;
|
const { timeout, ...otherProps } = message;
|
||||||
const msg = observable({
|
const msg = observable({
|
||||||
...otherProps,
|
...otherProps,
|
||||||
@ -22,7 +23,15 @@ export class UiStore {
|
|||||||
});
|
});
|
||||||
this.messages.push(msg);
|
this.messages.push(msg);
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
setTimeout(() => this.messages.remove(msg), timeout);
|
setTimeout(() => {
|
||||||
|
this.removeMessage(msg);
|
||||||
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeMessage(message: UiMessage) {
|
||||||
|
return this.messages.remove(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
import { IUser } from "@common/httpApi";
|
import { ISprinklersDevice, IUser } from "@common/httpApi";
|
||||||
import { observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
|
|
||||||
export class UserStore {
|
export class UserStore {
|
||||||
@observable userData: IUser | null = null;
|
@observable userData: IUser | null = null;
|
||||||
|
|
||||||
|
@action
|
||||||
|
receiveUserData(userData: IUser) {
|
||||||
|
this.userData = userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
findDevice(id: number): ISprinklersDevice | null {
|
||||||
|
return this.userData &&
|
||||||
|
this.userData.devices &&
|
||||||
|
this.userData.devices.find((dev) => dev.id === id) || null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,8 @@ function formatLevel(value: any): ColoredString {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logger: pino.Logger = pino({
|
const logger: pino.Logger = pino({
|
||||||
browser: { write },
|
serializers: pino.stdSerializers,
|
||||||
|
browser: { serialize: true, write },
|
||||||
level: "trace",
|
level: "trace",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user