Browse Source

Lots of improvements

develop
Alex Mikhalev 6 years ago
parent
commit
f328e5c2e2
  1. 6
      client/App.tsx
  2. 6
      client/components/DeviceView.tsx
  3. 2
      client/components/MessagesView.tsx
  4. 2
      client/components/RunSectionForm.tsx
  5. 6
      client/pages/DevicesPage.tsx
  6. 8
      client/pages/ProgramPage.tsx
  7. 9
      client/sprinklersRpc/WebSocketRpcClient.ts
  8. 2
      client/state/AppState.ts
  9. 18
      client/state/UserStore.ts
  10. 10
      client/state/reactContext.tsx
  11. 7
      client/styles/DeviceView.scss
  12. 2
      client/styles/DurationView.scss
  13. 6
      client/styles/ProgramSequenceView.scss
  14. 2
      common/jsonRpc/index.ts
  15. 2
      common/sprinklersRpc/mqtt/index.ts
  16. 4
      common/sprinklersRpc/schema/requests.ts
  17. 7
      server/Database.ts
  18. 6
      server/commands/device.ts
  19. 6
      server/express/api/devices.ts

6
client/App.tsx

@ -1,6 +1,6 @@
// import DevTools from "mobx-react-devtools"; // import DevTools from "mobx-react-devtools";
import * as React from "react"; import * as React from "react";
import { Redirect, Route, Switch } from "react-router"; import { Redirect, Route, Switch, withRouter } from "react-router";
import { Container } from "semantic-ui-react"; import { Container } from "semantic-ui-react";
import { MessagesView, NavBar } from "@client/components"; import { MessagesView, NavBar } from "@client/components";
@ -30,7 +30,7 @@ function NavContainer() {
); );
} }
export default function App() { function App() {
return ( return (
<Switch> <Switch>
<Route path={route.login} component={p.LoginPage} /> <Route path={route.login} component={p.LoginPage} />
@ -39,3 +39,5 @@ export default function App() {
</Switch> </Switch>
); );
} }
export default withRouter(App);

6
client/components/DeviceView.tsx

@ -2,7 +2,7 @@ import * as classNames from "classnames";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Grid, Header, Icon, Item, SemanticICONS, Dimmer, Segment } from "semantic-ui-react"; import { Dimmer, Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react";
import { DeviceImage } from "@client/components"; import { DeviceImage } from "@client/components";
import * as p from "@client/pages"; import * as p from "@client/pages";
@ -62,7 +62,7 @@ const ConnectionState = observer(
} }
); );
interface DeviceViewProps { interface DeviceViewProps extends RouteComponentProps {
deviceId: number; deviceId: number;
appState: AppState; appState: AppState;
inList?: boolean; inList?: boolean;
@ -189,4 +189,4 @@ class DeviceView extends React.Component<DeviceViewProps> {
} }
} }
export default injectState(observer(DeviceView)); export default withRouter(injectState(observer(DeviceView)));

2
client/components/MessagesView.tsx

@ -30,7 +30,7 @@ class MessageView extends React.Component<{
if (message.onDismiss) { if (message.onDismiss) {
message.onDismiss(event, data); message.onDismiss(event, data);
} }
uiStore.messages.remove(message); uiStore.removeMessage(message);
}; };
} }

2
client/components/RunSectionForm.tsx

@ -1,6 +1,6 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Form, Header, Icon, Segment, Popup } from "semantic-ui-react"; import { Form, Header, Icon, Popup, Segment } from "semantic-ui-react";
import { DurationView, SectionChooser } from "@client/components"; import { DurationView, SectionChooser } from "@client/components";
import { UiStore } from "@client/state"; import { UiStore } from "@client/state";

6
client/pages/DevicesPage.tsx

@ -1,6 +1,6 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Item, Button } from "semantic-ui-react"; import { Button, Item } from "semantic-ui-react";
import { DeviceView } from "@client/components"; import { DeviceView } from "@client/components";
import { AppState, injectState } from "@client/state"; import { AppState, injectState } from "@client/state";
@ -12,7 +12,7 @@ class DevicesPage extends React.Component<{ appState: AppState }> {
render() { render() {
const { appState } = this.props; const { appState } = this.props;
const { userData } = appState.userStore; const userData = appState.userStore.getUserData();
let deviceNodes: React.ReactNode; let deviceNodes: React.ReactNode;
if (!userData) { if (!userData) {
deviceNodes = <span>Not logged in</span>; deviceNodes = <span>Not logged in</span>;
@ -25,7 +25,7 @@ class DevicesPage extends React.Component<{ appState: AppState }> {
} }
return ( return (
<React.Fragment> <React.Fragment>
<h1 className="devices-header">Devices <Button icon="refresh" onClick={this.refreshDevices}></Button></h1> <h1 className="devices-header">Devices <Button icon="refresh" onClick={this.refreshDevices} /></h1>
<Item.Group>{deviceNodes}</Item.Group> <Item.Group>{deviceNodes}</Item.Group>
</React.Fragment> </React.Fragment>
); );

8
client/pages/ProgramPage.tsx

@ -2,7 +2,7 @@ import { assign } from "lodash";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import * as qs from "query-string"; import * as qs from "query-string";
import * as React from "react"; import * as React from "react";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps, withRouter } from "react-router";
import { import {
Button, Button,
CheckboxProps, CheckboxProps,
@ -252,8 +252,8 @@ class ProgramPage extends React.Component<ProgramPageProps> {
@action.bound @action.bound
private close() { private close() {
const { deviceId } = this.props.match.params; // this.props.history.goBack();
this.props.history.push({ pathname: route.device(deviceId), search: "" }); this.props.appState.history.goBack();
} }
@action.bound @action.bound
@ -271,5 +271,5 @@ class ProgramPage extends React.Component<ProgramPageProps> {
} }
} }
const DecoratedProgramPage = injectState(observer(ProgramPage)); const DecoratedProgramPage = injectState(withRouter(observer(ProgramPage)));
export default DecoratedProgramPage; export default DecoratedProgramPage;

9
client/sprinklersRpc/WebSocketRpcClient.ts

@ -191,11 +191,16 @@ export class WebSocketRpcClient extends s.SprinklersRPC {
const id = this.nextRequestId++; const id = this.nextRequestId++;
return new Promise<ws.IServerResponseTypes[Method]>((resolve, reject) => { return new Promise<ws.IServerResponseTypes[Method]>((resolve, reject) => {
let timeoutHandle: number; let timeoutHandle: number;
this.responseCallbacks[id] = response => { this.responseCallbacks[id] = (response: ws.ServerResponse) => {
clearTimeout(timeoutHandle); clearTimeout(timeoutHandle);
delete this.responseCallbacks[id]; delete this.responseCallbacks[id];
if (response.result === "success") { if (response.result === "success") {
resolve(response.data); if (response.method === method) {
resolve(response.data as ws.IServerResponseTypes[Method]);
} else {
reject(new s.RpcError("Response method does not match request method", ErrorCode.Internal,
{ requestMethod: method, responseMethod: response.method }));
}
} else { } else {
const { error } = response; const { error } = response;
reject(new s.RpcError(error.message, error.code, error.data)); reject(new s.RpcError(error.message, error.code, error.data));

2
client/state/AppState.ts

@ -47,7 +47,7 @@ export default class AppState extends TypedEventEmitter<AppEvents> {
async start() { async start() {
configure({ configure({
enforceActions: true enforceActions: "observed"
}); });
syncHistoryWithStore(this.history, this.routerStore); syncHistoryWithStore(this.history, this.routerStore);

18
client/state/UserStore.ts

@ -1,20 +1,24 @@
import { ISprinklersDevice, IUser } from "@common/httpApi"; import { ISprinklersDevice, IUser } from "@common/httpApi";
import { action, observable } from "mobx"; import { action, IObservableValue, observable } from "mobx";
export class UserStore { export class UserStore {
@observable userData: IObservableValue<IUser | null> = observable.box(null);
userData: IUser | null = null;
@action.bound @action.bound
receiveUserData(userData: IUser) { receiveUserData(userData: IUser) {
this.userData = userData; this.userData.set(userData);
}
getUserData(): IUser | null {
return this.userData.get();
} }
findDevice(id: number): ISprinklersDevice | null { findDevice(id: number): ISprinklersDevice | null {
const userData = this.userData.get();
return ( return (
(this.userData && (userData &&
this.userData.devices && userData.devices &&
this.userData.devices.find(dev => dev.id === id)) || userData.devices.find(dev => dev.id === id)) ||
null null
); );
} }

10
client/state/reactContext.tsx

@ -31,12 +31,6 @@ export function ConsumeState({ children }: ConsumeStateProps) {
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>; 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 { appState: AppState }>( export function injectState<P extends { appState: AppState }>(
Component: React.ComponentType<P> Component: React.ComponentType<P>
): React.ComponentClass<Omit<P, "appState">> { ): React.ComponentClass<Omit<P, "appState">> {
@ -48,7 +42,9 @@ export function injectState<P extends { appState: AppState }>(
"Component with injectState must be mounted inside ProvideState" "Component with injectState must be mounted inside ProvideState"
); );
} }
return <Component {...this.props} appState={state} />; // tslint:disable-next-line:no-object-literal-type-assertion
const allProps: Readonly<P> = {...this.props, appState: state} as Readonly<P>;
return <Component {...allProps} />;
}; };
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>; return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
} }

7
client/styles/DeviceView.scss

@ -81,3 +81,10 @@ $connected-color: #13d213;
.ui.modal.programEditor > .header > .header.item .inline.fields { .ui.modal.programEditor > .header > .header.item .inline.fields {
margin-bottom: 0; margin-bottom: 0;
} }
.runSectionForm-runButton {
display: inline-block;
&, .ui.disabled.button {
pointer-events: auto !important;
}
}

2
client/styles/DurationView.scss

@ -4,7 +4,7 @@ $durationInput-labelWidth: 2.5em;
.field .durationInputs { .field .durationInputs {
display: flex; // max-width: 100%; display: flex; // max-width: 100%;
justify-content: start; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
margin: -$durationInput-spacing / 2; margin: -$durationInput-spacing / 2;

6
client/styles/ProgramSequenceView.scss

@ -8,12 +8,16 @@
.programSequence-item { .programSequence-item {
list-style-type: none; list-style-type: none;
display: flex; display: flex;
align-items: center;
margin-bottom: 0.5em; margin-bottom: 0.5em;
&.dragging { &.dragging {
z-index: 1010; z-index: 1010;
} }
.fields { .fields {
margin: 0em 0em 1em !important; display: flex;
align-items: center;
margin: 0em !important;
padding: 0em !important;
} }
.ui.icon.button { .ui.icon.button {
height: fit-content; height: fit-content;

2
common/jsonRpc/index.ts

@ -133,7 +133,7 @@ export type IResponseHandler<
ResponseTypes, ResponseTypes,
ErrorType, ErrorType,
Method extends keyof ResponseTypes = keyof ResponseTypes Method extends keyof ResponseTypes = keyof ResponseTypes
> = (response: ResponseData<ResponseTypes, ErrorType, Method>) => void; > = (response: Response<ResponseTypes, ErrorType, Method>) => void;
export interface ResponseHandlers< export interface ResponseHandlers<
ResponseTypes = DefaultResponseTypes, ResponseTypes = DefaultResponseTypes,

2
common/sprinklersRpc/mqtt/index.ts

@ -218,7 +218,7 @@ class MqttSprinklersDevice extends s.SprinklersDevice {
} }
doUnsubscribe() { doUnsubscribe() {
this.apiClient.client.unsubscribe(this.subscriptions, err => { this.apiClient.client.unsubscribe(this.subscriptions, (err: Error | undefined) => {
if (err) { if (err) {
log.error({ err, id: this.id }, "error unsubscribing to device"); log.error({ err, id: this.id }, "error unsubscribing to device");
} else { } else {

4
common/sprinklersRpc/schema/requests.ts

@ -36,8 +36,8 @@ export const updateProgram: ModelSchema<
deserializer: (json, done) => { deserializer: (json, done) => {
done(null, json); done(null, json);
}, },
beforeDeserialize: () => {}, beforeDeserialize: undefined as any,
afterDeserialize: () => {}, afterDeserialize: undefined as any,
} }
}); });

7
server/Database.ts

@ -47,7 +47,7 @@ export class Database {
const users: User[] = []; const users: User[] = [];
for (let i = 0; i < NUM; i++) { for (let i = 0; i < NUM; i++) {
const username = "alex" + i; const username = "alex" + i;
let user = await this.users.findByUsername(username); let user = await this.users.findByUsername(username, { devices: true });
if (!user) { if (!user) {
user = await this.users.create({ user = await this.users.create({
name: "Alex Mikhalev" + i, name: "Alex Mikhalev" + i,
@ -74,7 +74,10 @@ export class Database {
for (let j = 0; j < 5; j++) { for (let j = 0; j < 5; j++) {
const userIdx = (i + j * 10) % NUM; const userIdx = (i + j * 10) % NUM;
const user = users[userIdx]; const user = users[userIdx];
user.devices = (user.devices || []).concat([device]); if (!user.devices) {
user.devices = [];
}
user.devices.push(device);
} }
} }
await this.sprinklersDevices.save(devices); await this.sprinklersDevices.save(devices);

6
server/commands/device.ts

@ -70,7 +70,7 @@ export default class DeviceCommand extends ManageCommand {
} }
getFindConditions(flags: DeviceFlags, action: Action): FindConditions<SprinklersDevice> { getFindConditions(flags: DeviceFlags, action: Action): FindConditions<SprinklersDevice> {
let whereClause: FindConditions<SprinklersDevice> = {}; const whereClause: FindConditions<SprinklersDevice> = {};
if (flags.id) { if (flags.id) {
whereClause.id = flags.id; whereClause.id = flags.id;
} }
@ -125,7 +125,7 @@ export default class DeviceCommand extends ManageCommand {
query = query.where("user.username = :username", { username: flags.username }); query = query.where("user.username = :username", { username: flags.username });
} }
const devices = await query.getMany(); const devices = await query.getMany();
if (devices.length == 0) { if (devices.length === 0) {
this.log("No sprinklers devices found"); this.log("No sprinklers devices found");
return 1; return 1;
} }
@ -146,7 +146,7 @@ export default class DeviceCommand extends ManageCommand {
if (!user) { if (!user) {
return this.error(`Could not find user with username '${flags.username}'`); return this.error(`Could not find user with username '${flags.username}'`);
} }
let query = this.database.sprinklersDevices.createQueryBuilder() const query = this.database.sprinklersDevices.createQueryBuilder()
.relation("users") .relation("users")
.of(device); .of(device);
if (flags["add-user"]) { if (flags["add-user"]) {

6
server/express/api/devices.ts

@ -108,6 +108,12 @@ export function devices(state: ServerState) {
async (req, res) => { async (req, res) => {
const token: DeviceToken = req.token! as any; const token: DeviceToken = req.token! as any;
const deviceId = token.aud; const deviceId = token.aud;
const devs = await state.database.sprinklersDevices.count({
deviceId
});
if (devs === 0) {
throw new ApiError("deviceId not found", ErrorCode.NotFound);
}
const clientId = `device-${deviceId}`; const clientId = `device-${deviceId}`;
res.send({ res.send({
mqttUrl: state.mqttUrl, mqttUrl: state.mqttUrl,

Loading…
Cancel
Save