Lots of improvements
This commit is contained in:
parent
6e95d091ae
commit
f328e5c2e2
@ -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);
|
||||||
|
@ -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)));
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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));
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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"]) {
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user