Browse Source

Use mobx actions on client side

update-deps
Alex Mikhalev 7 years ago
parent
commit
60cabb9e57
  1. 12
      client/components/DeviceView.tsx
  2. 9
      client/components/ProgramSequenceView.tsx
  3. 12
      client/components/ProgramTable.tsx
  4. 13
      client/components/ScheduleView/index.tsx
  5. 17
      client/pages/LoginPage.tsx
  6. 35
      client/pages/ProgramPage.tsx
  7. 54
      client/sprinklersRpc/WebSocketRpcClient.ts
  8. 6
      client/state/AppState.ts
  9. 5
      client/state/HttpApi.ts
  10. 6
      client/state/TokenStore.ts
  11. 15
      client/state/UiStore.ts
  12. 15
      client/state/UserStore.ts
  13. 3
      common/logger.ts

12
client/components/DeviceView.tsx

@ -8,6 +8,7 @@ import { DeviceImage } from "@client/components"; @@ -8,6 +8,7 @@ import { DeviceImage } from "@client/components";
import * as p from "@client/pages";
import * as route from "@client/routePaths";
import { AppState, injectState } from "@client/state";
import { ISprinklersDevice } from "@common/httpApi";
import { ConnectionState as ConState, SprinklersDevice } from "@common/sprinklersRpc";
import { Route, RouteComponentProps, withRouter } from "react-router";
import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from ".";
@ -52,7 +53,7 @@ interface DeviceViewProps { @@ -52,7 +53,7 @@ interface DeviceViewProps {
}
class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<any>> {
renderBody(device: SprinklersDevice) {
renderBody(iDevice: ISprinklersDevice, device: SprinklersDevice) {
const { inList, appState: { uiStore, routerStore } } = this.props;
const { connectionState, sectionRunner, sections } = device;
if (!connectionState.isAvailable || inList) {
@ -71,7 +72,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a @@ -71,7 +72,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a
<RunSectionForm device={device} uiStore={uiStore} />
</Grid.Column>
</Grid>
<ProgramTable device={device} routerStore={routerStore} />
<ProgramTable iDevice={iDevice} device={device} routerStore={routerStore} />
<Route path={route.program(":deviceId", ":programId")} component={p.ProgramPage} />
</React.Fragment>
);
@ -79,10 +80,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a @@ -79,10 +80,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a
render() {
const { deviceId, inList, appState: { sprinklersRpc, userStore } } = this.props;
const { userData } = userStore;
const iDevice = userData &&
userData.devices &&
userData.devices.find((dev) => dev.id === deviceId);
const iDevice = userStore.findDevice(deviceId);
let itemContent: React.ReactNode;
if (!iDevice || !iDevice.deviceId) {
// TODO: better and link back to devices list
@ -107,7 +105,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a @@ -107,7 +105,7 @@ class DeviceView extends React.Component<DeviceViewProps & RouteComponentProps<a
<Item.Meta>
Raspberry Pi Grinklers Device
</Item.Meta>
{this.renderBody(device)}
{this.renderBody(iDevice, device)}
</Item.Content>
</React.Fragment>
);

9
client/components/ProgramSequenceView.tsx

@ -9,6 +9,7 @@ import { Duration } from "@common/Duration"; @@ -9,6 +9,7 @@ import { Duration } from "@common/Duration";
import { ProgramItem, Section } from "@common/sprinklersRpc";
import "@client/styles/ProgramSequenceView";
import { action } from "mobx";
type ItemChangeHandler = (index: number, newItem: ProgramItem) => void;
type ItemRemoveHandler = (index: number) => void;
@ -147,15 +148,18 @@ class ProgramSequenceView extends React.Component<{ @@ -147,15 +148,18 @@ class ProgramSequenceView extends React.Component<{
);
}
@action.bound
private changeItem: ItemChangeHandler = (index, newItem) => {
this.props.sequence[index] = newItem;
}
@action.bound
private removeItem: ItemRemoveHandler = (index) => {
this.props.sequence.splice(index, 1);
}
private addItem = () => {
@action.bound
private addItem() {
let sectionId = 0;
for (const section of this.props.sections) {
const sectionNotIncluded = this.props.sequence
@ -173,7 +177,8 @@ class ProgramSequenceView extends React.Component<{ @@ -173,7 +177,8 @@ class ProgramSequenceView extends React.Component<{
this.props.sequence.push(item);
}
private onSortEnd = ({oldIndex, newIndex}: SortEnd) => {
@action.bound
private onSortEnd({oldIndex, newIndex}: SortEnd) {
const { sequence: array } = this.props;
if (newIndex >= array.length) {
return;

12
client/components/ProgramTable.tsx

@ -6,22 +6,25 @@ import { Button, ButtonProps, Form, Icon, Table } from "semantic-ui-react"; @@ -6,22 +6,25 @@ import { Button, ButtonProps, Form, Icon, Table } from "semantic-ui-react";
import { ProgramSequenceView, ScheduleView } from "@client/components";
import * as route from "@client/routePaths";
import { ISprinklersDevice } from "@common/httpApi";
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
@observer
class ProgramRows extends React.Component<{
program: Program, device: SprinklersDevice,
program: Program,
iDevice: ISprinklersDevice,
device: SprinklersDevice,
routerStore: RouterStore,
expanded: boolean, toggleExpanded: (program: Program) => void,
}> {
render() {
const { program, device, expanded } = this.props;
const { program, iDevice, device, expanded } = this.props;
const { sections } = device;
const { name, running, enabled, schedule, sequence } = program;
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 = (
<Button onClick={this.cancelOrRun} {...buttonStyle} positive={!running} negative={running}>
@ -81,7 +84,7 @@ class ProgramRows extends React.Component<{ @@ -81,7 +84,7 @@ class ProgramRows extends React.Component<{
@observer
export default class ProgramTable extends React.Component<{
device: SprinklersDevice, routerStore: RouterStore,
iDevice: ISprinklersDevice, device: SprinklersDevice, routerStore: RouterStore,
}, {
expandedPrograms: Program[],
}> {
@ -123,6 +126,7 @@ export default class ProgramTable extends React.Component<{ @@ -123,6 +126,7 @@ export default class ProgramTable extends React.Component<{
return (
<ProgramRows
program={program}
iDevice={this.props.iDevice}
device={this.props.device}
routerStore={this.props.routerStore}
expanded={expanded}

13
client/components/ScheduleView/index.tsx

@ -8,6 +8,7 @@ import ScheduleTimes from "./ScheduleTimes"; @@ -8,6 +8,7 @@ import ScheduleTimes from "./ScheduleTimes";
import WeekdaysView from "./WeekdaysView";
import "@client/styles/ScheduleView";
import { action } from "mobx";
export interface ScheduleViewProps {
label?: string | React.ReactNode | undefined;
@ -39,19 +40,23 @@ export default class ScheduleView extends React.Component<ScheduleViewProps> { @@ -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;
}
private updateWeekdays = (newWeekdays: Weekday[]) => {
@action.bound
private updateWeekdays(newWeekdays: Weekday[]) {
this.props.schedule.weekdays = newWeekdays;
}
private updateFromDate = (newFromDate: DateOfYear | null) => {
@action.bound
private updateFromDate(newFromDate: DateOfYear | null) {
this.props.schedule.from = newFromDate;
}
private updateToDate = (newToDate: DateOfYear | null) => {
@action.bound
private updateToDate(newToDate: DateOfYear | null) {
this.props.schedule.to = newToDate;
}
}

17
client/pages/LoginPage.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { computed, observable } from "mobx";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { Container, Dimmer, Form, Header, InputOnChangeData, Loader, Message, Segment } from "semantic-ui-react";
@ -19,28 +19,31 @@ class LoginPageState { @@ -19,28 +19,31 @@ class LoginPageState {
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;
}
onPasswordChange = (e: any, data: InputOnChangeData) => {
@action.bound
onPasswordChange(e: any, data: InputOnChangeData) {
this.password = data.value;
}
@action.bound
login(appState: AppState) {
this.loading = true;
this.error = null;
appState.httpApi.grantPassword(this.username, this.password)
.then(() => {
.then(action("loginSuccess", () => {
this.loading = false;
log.info("logged in");
appState.history.push("/");
})
.catch((err) => {
}))
.catch(action("loginError", (err: any) => {
this.loading = false;
this.error = err.message;
log.error({ err }, "login error");
});
}));
}
}

35
client/pages/ProgramPage.tsx

@ -8,8 +8,10 @@ import { Button, CheckboxProps, Form, Icon, Input, InputOnChangeData, Menu, Moda @@ -8,8 +8,10 @@ import { Button, CheckboxProps, Form, Icon, Input, InputOnChangeData, Menu, Moda
import { ProgramSequenceView, ScheduleView } from "@client/components";
import * as route from "@client/routePaths";
import { AppState, injectState } from "@client/state";
import { ISprinklersDevice } from "@common/httpApi";
import log from "@common/logger";
import { Program, SprinklersDevice } from "@common/sprinklersRpc";
import { action } from "mobx";
interface ProgramPageProps extends RouteComponentProps<{ deviceId: string, programId: string }> {
appState: AppState;
@ -21,6 +23,7 @@ class ProgramPage extends React.Component<ProgramPageProps> { @@ -21,6 +23,7 @@ class ProgramPage extends React.Component<ProgramPageProps> {
return qs.parse(this.props.location.search).editing != null;
}
iDevice!: ISprinklersDevice;
device!: SprinklersDevice;
program!: Program;
programView: Program | null = null;
@ -95,11 +98,16 @@ class ProgramPage extends React.Component<ProgramPageProps> { @@ -95,11 +98,16 @@ class ProgramPage extends React.Component<ProgramPageProps> {
}
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);
// tslint:disable-next-line:prefer-conditional-expression
if (!this.device || this.device.id !== deviceId) {
this.device = this.props.appState.sprinklersRpc.getDevice(deviceId);
if (!this.iDevice || this.iDevice.id !== 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
if (!this.program || this.program.id !== programId) {
@ -158,18 +166,21 @@ class ProgramPage extends React.Component<ProgramPageProps> { @@ -158,18 +166,21 @@ class ProgramPage extends React.Component<ProgramPageProps> {
);
}
private cancelOrRun = () => {
@action.bound
private cancelOrRun() {
if (!this.program) {
return;
}
this.program.running ? this.program.cancel() : this.program.run();
}
private startEditing = () => {
@action.bound
private startEditing() {
this.props.history.push({ search: qs.stringify({ editing: true }) });
}
private save = () => {
@action.bound
private save() {
if (!this.programView || !this.program) {
return;
}
@ -183,22 +194,26 @@ class ProgramPage extends React.Component<ProgramPageProps> { @@ -183,22 +194,26 @@ class ProgramPage extends React.Component<ProgramPageProps> {
this.stopEditing();
}
private stopEditing = () => {
@action.bound
private stopEditing() {
this.props.history.push({ search: "" });
}
private close = () => {
@action.bound
private close() {
const { deviceId } = this.props.match.params;
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) {
this.programView.name = p.value;
}
}
private onEnabledChange = (e: any, p: CheckboxProps) => {
@action.bound
private onEnabledChange(e: any, p: CheckboxProps) {
if (this.programView) {
this.programView.enabled = p.checked!;
}

54
client/sprinklersRpc/WebSocketRpcClient.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { action, autorun, observable, when } from "mobx";
import { action, autorun, observable, runInAction, when } from "mobx";
import { update } from "serializr";
import { TokenStore } from "@client/state/TokenStore";
@ -35,10 +35,7 @@ export class WSSprinklersDevice extends s.SprinklersDevice { @@ -35,10 +35,7 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
this.api = api;
this._id = id;
autorun(() => {
this.connectionState.clientToServer = this.api.connectionState.clientToServer;
this.connectionState.serverToBroker = this.api.connectionState.serverToBroker;
});
autorun(this.updateConnectionState);
this.waitSubscribe();
}
@ -46,20 +43,31 @@ export class WSSprinklersDevice extends s.SprinklersDevice { @@ -46,20 +43,31 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
return this._id;
}
private updateConnectionState = () => {
const { clientToServer, serverToBroker } = this.api.connectionState;
runInAction("updateConnectionState", () => {
Object.assign(this.connectionState, { clientToServer, serverToBroker });
});
}
async subscribe() {
const subscribeRequest: ws.IDeviceSubscribeRequest = {
deviceId: this.id,
};
try {
await this.api.makeRequest("deviceSubscribe", subscribeRequest);
runInAction("deviceSubscribeSuccess", () => {
this.connectionState.brokerToDevice = true;
});
} catch (err) {
runInAction("deviceSubscribeError", () => {
this.connectionState.brokerToDevice = false;
if ((err as ws.IError).code === ErrorCode.NoPermission) {
this.connectionState.hasPermission = false;
} else {
log.error({ err });
}
});
}
}
@ -143,7 +151,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC { @@ -143,7 +151,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
const res = await this.authenticate(this.tokenStore.accessToken.token!);
this.authenticated = res.authenticated;
logger.info({ user: res.user }, "authenticated websocket connection");
this.userStore.userData = res.user;
this.userStore.receiveUserData(res.user);
} catch (err) {
logger.error({ err }, "error authenticating websocket connection");
// TODO message?
@ -288,7 +296,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC { @@ -288,7 +296,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
try {
rpc.handleNotification(this.notificationHandlers, data);
} 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 { @@ -300,19 +308,31 @@ export class WebSocketRpcClient implements s.SprinklersRPC {
}
}
private notificationHandlers: ws.ServerNotificationHandlers = {
brokerConnectionUpdate: (data: ws.IBrokerConnectionUpdate) => {
this.connectionState.serverToBroker = data.brokerConnected;
},
deviceUpdate: (data: ws.IDeviceUpdate) => {
const device = this.devices.get(data.deviceId);
private notificationHandlers = new WSClientNotificationHandlers(this);
}
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) => {
}
error(data: ws.IError) {
log.warn({ err: data }, "server error");
},
};
}
};

6
client/state/AppState.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { createBrowserHistory, History } from "history";
import { computed } from "mobx";
import { computed, configure } from "mobx";
import { RouterStore, syncHistoryWithStore } from "mobx-react-router";
import { WebSocketRpcClient } from "@client/sprinklersRpc/WebSocketRpcClient";
@ -24,6 +24,10 @@ export default class AppState { @@ -24,6 +24,10 @@ export default class AppState {
}
async start() {
configure({
enforceActions: true,
});
syncHistoryWithStore(this.history, this.routerStore);
this.tokenStore.loadLocalStorage();

5
client/state/HttpApi.ts

@ -3,6 +3,7 @@ import ApiError from "@common/ApiError"; @@ -3,6 +3,7 @@ import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/httpApi";
import log from "@common/logger";
import { runInAction } from "mobx";
export { ApiError };
@ -58,9 +59,11 @@ export default class HttpApi { @@ -58,9 +59,11 @@ export default class HttpApi {
const response: TokenGrantResponse = await this.makeRequest("/token/grant", {
method: "POST",
}, request);
runInAction("grantPasswordSuccess", () => {
this.tokenStore.accessToken.token = response.access_token;
this.tokenStore.refreshToken.token = response.refresh_token;
this.tokenStore.saveLocalStorage();
});
const { accessToken } = this.tokenStore;
log.debug({ aud: accessToken.claims!.aud }, "got password grant tokens");
}
@ -76,9 +79,11 @@ export default class HttpApi { @@ -76,9 +79,11 @@ export default class HttpApi {
const response: TokenGrantResponse = await this.makeRequest("/token/grant", {
method: "POST",
}, request);
runInAction("grantRefreshSuccess", () => {
this.tokenStore.accessToken.token = response.access_token;
this.tokenStore.refreshToken.token = response.refresh_token;
this.tokenStore.saveLocalStorage();
});
const { accessToken } = this.tokenStore;
log.debug({ aud: accessToken.claims!.aud }, "got refresh grant tokens");
}

6
client/state/TokenStore.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { observable } from "mobx";
import { action, observable } from "mobx";
import { Token } from "@client/state/Token";
import { AccessToken, RefreshToken } from "@common/TokenClaims";
@ -9,16 +9,19 @@ export class TokenStore { @@ -9,16 +9,19 @@ export class TokenStore {
@observable accessToken: Token<AccessToken> = new Token();
@observable refreshToken: Token<RefreshToken> = new Token();
@action
clear() {
this.accessToken.token = null;
this.refreshToken.token = null;
this.saveLocalStorage();
}
@action
saveLocalStorage() {
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.toJSON()));
}
@action
loadLocalStorage() {
const data = window.localStorage.getItem(LOCAL_STORAGE_KEY);
if (data) {
@ -31,6 +34,7 @@ export class TokenStore { @@ -31,6 +34,7 @@ export class TokenStore {
return { accessToken: this.accessToken.toJSON(), refreshToken: this.refreshToken.toJSON() };
}
@action
updateFromJson(json: any) {
this.accessToken.token = json.accessToken;
this.refreshToken.token = json.refreshToken;

15
client/state/UiStore.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { IObservableArray, observable } from "mobx";
import { action, IObservableArray, observable } from "mobx";
import { MessageProps } from "semantic-ui-react";
import { getRandomId } from "@common/utils";
@ -14,7 +14,8 @@ export interface UiMessageProps extends MessageProps { @@ -14,7 +14,8 @@ export interface UiMessageProps extends MessageProps {
export class UiStore {
messages: IObservableArray<UiMessage> = observable.array();
addMessage(message: UiMessageProps) {
@action
addMessage(message: UiMessageProps): UiMessage {
const { timeout, ...otherProps } = message;
const msg = observable({
...otherProps,
@ -22,7 +23,15 @@ export class UiStore { @@ -22,7 +23,15 @@ export class UiStore {
});
this.messages.push(msg);
if (timeout) {
setTimeout(() => this.messages.remove(msg), timeout);
setTimeout(() => {
this.removeMessage(msg);
}, timeout);
}
return msg;
}
@action
removeMessage(message: UiMessage) {
return this.messages.remove(message);
}
}

15
client/state/UserStore.ts

@ -1,6 +1,17 @@ @@ -1,6 +1,17 @@
import { IUser } from "@common/httpApi";
import { observable } from "mobx";
import { ISprinklersDevice, IUser } from "@common/httpApi";
import { action, observable } from "mobx";
export class UserStore {
@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;
}
}

3
common/logger.ts

@ -107,7 +107,8 @@ function formatLevel(value: any): ColoredString { @@ -107,7 +107,8 @@ function formatLevel(value: any): ColoredString {
}
const logger: pino.Logger = pino({
browser: { write },
serializers: pino.stdSerializers,
browser: { serialize: true, write },
level: "trace",
});

Loading…
Cancel
Save