Browse Source

Added login page and lots of related improvments

update-deps
Alex Mikhalev 7 years ago
parent
commit
2baca5fdd0
  1. 50
      app/components/App.tsx
  2. 8
      app/components/DeviceView.tsx
  3. 10
      app/components/MessageTest.tsx
  4. 6
      app/components/MessagesView.tsx
  5. 3
      app/components/NavBar.tsx
  6. 13
      app/index.tsx
  7. 75
      app/pages/LoginPage.tsx
  8. 18
      app/pages/index.tsx
  9. 34
      app/sprinklersRpc/websocketClient.ts
  10. 16
      app/state/AppState.ts
  11. 1
      app/state/HttpApi.ts
  12. 2
      app/state/Token.ts
  13. 7
      app/state/TokenStore.ts
  14. 8
      app/state/index.ts
  15. 23
      app/state/reactContext.tsx
  16. 100
      app/styles/app.scss
  17. 2
      common/http.ts
  18. 0
      server/express/expressLogger.ts
  19. 16
      server/sprinklersRpc/websocketServer.ts

50
app/components/App.tsx

@ -1,47 +1,37 @@
import { observer } from "mobx-react";
// 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, RouteComponentProps, Switch } from "react-router"; import { Redirect, Route, Switch } from "react-router";
import { BrowserRouter as Router } from "react-router-dom";
import { Container } from "semantic-ui-react"; import { Container } from "semantic-ui-react";
import { DevicesView, MessagesView, MessageTest, NavBar } from "@app/components"; import { MessagesView, NavBar } from "@app/components";
import * as p from "@app/pages";
// tslint:disable:ordered-imports // tslint:disable:ordered-imports
import "font-awesome/css/font-awesome.css"; import "font-awesome/css/font-awesome.css";
import "semantic-ui-css/semantic.css"; import "semantic-ui-css/semantic.css";
import "@app/styles/app.scss"; import "@app/styles/app.scss";
function DevicePage({match}: RouteComponentProps<{deviceId: string}>) { function NavContainer() {
return ( return (
<DevicesView deviceId={match.params.deviceId}/> <Container className="app">
<NavBar/>
<Switch>
<Route path="/devices/:deviceId" component={p.DevicePage}/>
<Route path="/messagesTest" component={p.MessagesTestPage}/>
<Redirect to="/"/>
</Switch>
<MessagesView/>
</Container>
); );
} }
function MessagesTestPage() { export default function App() {
return ( return (
<MessageTest/> <Switch>
<Route path="/login" component={p.LoginPage}/>
<NavContainer/>
</Switch>
); );
} }
class App extends React.Component {
render() {
return (
<Router>
<Container className="app">
<NavBar/>
<Switch>
<Route path="/devices/:deviceId" component={DevicePage}/>
<Route path="/messagesTest" component={MessagesTestPage}/>
<Redirect to="/"/>
</Switch>
<MessagesView/>
</Container>
</Router>
);
}
}
export default observer(App);

8
app/components/DeviceView.tsx

@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react"; import { Grid, Header, Icon, Item, SemanticICONS } from "semantic-ui-react";
import { injectState, StateBase } from "@app/state"; import { AppState, injectState } from "@app/state";
import { ConnectionState as ConState } from "@common/sprinklersRpc"; import { ConnectionState as ConState } from "@common/sprinklersRpc";
import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from "."; import { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from ".";
import "./DeviceView.scss"; import "./DeviceView.scss";
@ -45,13 +45,13 @@ const ConnectionState = observer(({ connectionState, className }:
interface DeviceViewProps { interface DeviceViewProps {
deviceId: string; deviceId: string;
state: StateBase; appState: AppState;
} }
class DeviceView extends React.Component<DeviceViewProps> { class DeviceView extends React.Component<DeviceViewProps> {
render() { render() {
const { uiStore, sprinklersApi } = this.props.state; const { uiStore, sprinklersRpc } = this.props.appState;
const device = sprinklersApi.getDevice(this.props.deviceId); const device = sprinklersRpc.getDevice(this.props.deviceId);
const { id, connectionState, sections, programs, sectionRunner } = device; const { id, connectionState, sections, programs, sectionRunner } = device;
const deviceBody = connectionState.isAvailable && ( const deviceBody = connectionState.isAvailable && (
<React.Fragment> <React.Fragment>

10
app/components/MessageTest.tsx

@ -1,10 +1,10 @@
import * as React from "react"; import * as React from "react";
import { Button, Segment } from "semantic-ui-react"; import { Button, Segment } from "semantic-ui-react";
import { injectState, StateBase } from "@app/state"; import { AppState, injectState } from "@app/state";
import { getRandomId } from "@common/utils"; import { getRandomId } from "@common/utils";
class MessageTest extends React.Component<{ state: StateBase }> { class MessageTest extends React.Component<{ appState: AppState }> {
render() { render() {
return ( return (
<Segment> <Segment>
@ -17,20 +17,20 @@ class MessageTest extends React.Component<{ state: StateBase }> {
} }
private test1 = () => { private test1 = () => {
this.props.state.uiStore.addMessage({ this.props.appState.uiStore.addMessage({
info: true, content: "Test Message! " + getRandomId(), header: "Header to test message", info: true, content: "Test Message! " + getRandomId(), header: "Header to test message",
}); });
} }
private test2 = () => { private test2 = () => {
this.props.state.uiStore.addMessage({ this.props.appState.uiStore.addMessage({
warning: true, content: "Im gonna dissapear in 5 seconds " + getRandomId(), warning: true, content: "Im gonna dissapear in 5 seconds " + getRandomId(),
header: "Header to test message", timeout: 5000, header: "Header to test message", timeout: 5000,
}); });
} }
private test3 = () => { private test3 = () => {
this.props.state.uiStore.addMessage({ this.props.appState.uiStore.addMessage({
color: "brown", content: <div className="ui segment">I Have crazy content!</div>, color: "brown", content: <div className="ui segment">I Have crazy content!</div>,
header: "Header to test message", timeout: 5000, header: "Header to test message", timeout: 5000,
}); });

6
app/components/MessagesView.tsx

@ -3,7 +3,7 @@ import { observer } from "mobx-react";
import * as React from "react"; import * as React from "react";
import { Message, MessageProps, TransitionGroup } from "semantic-ui-react"; import { Message, MessageProps, TransitionGroup } from "semantic-ui-react";
import { injectState, StateBase, UiMessage, UiStore } from "@app/state/"; import { AppState, injectState, UiMessage, UiStore } from "@app/state/";
@observer @observer
class MessageView extends React.Component<{ class MessageView extends React.Component<{
@ -33,9 +33,9 @@ class MessageView extends React.Component<{
} }
} }
class MessagesView extends React.Component<{ state: StateBase }> { class MessagesView extends React.Component<{ appState: AppState }> {
render() { render() {
const { uiStore } = this.props.state; const { uiStore } = this.props.appState;
const messages = uiStore.messages.map((message) => ( const messages = uiStore.messages.map((message) => (
<MessageView key={message.id} uiStore={uiStore} message={message} /> <MessageView key={message.id} uiStore={uiStore} message={message} />
)); ));

3
app/components/NavBar.tsx

@ -18,6 +18,9 @@ function NavBar({ location }: { location: Location }) {
<Menu> <Menu>
<NavItem to="/devices/grinklers">Device grinklers</NavItem> <NavItem to="/devices/grinklers">Device grinklers</NavItem>
<NavItem to="/messagesTest">Messages test</NavItem> <NavItem to="/messagesTest">Messages test</NavItem>
<Menu.Menu position="right">
<NavItem to="/login">Login</NavItem>
</Menu.Menu>
</Menu> </Menu>
); );
} }

13
app/index.tsx

@ -1,15 +1,16 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import { AppContainer } from "react-hot-loader"; import { AppContainer } from "react-hot-loader";
import { Router } from "react-router-dom";
import App from "@app/components/App"; import App from "@app/components/App";
import { ProvideState, StateBase, WebApiState as StateClass } from "@app/state"; import { AppState, ProvideState } from "@app/state";
import logger from "@common/logger"; import logger from "@common/logger";
const state: StateBase = new StateClass(); const state = new AppState();
state.start() state.start()
.catch((err) => { .catch((err: any) => {
logger.error({err}, "error starting state"); logger.error({ err }, "error starting state");
}); });
const rootElem = document.getElementById("app"); const rootElem = document.getElementById("app");
@ -18,7 +19,9 @@ const doRender = (Component: React.ComponentType) => {
ReactDOM.render(( ReactDOM.render((
<AppContainer> <AppContainer>
<ProvideState state={state}> <ProvideState state={state}>
<Component /> <Router history={state.history}>
<Component/>
</Router>
</ProvideState> </ProvideState>
</AppContainer> </AppContainer>
), rootElem); ), rootElem);

75
app/pages/LoginPage.tsx

@ -0,0 +1,75 @@
import { AppState, injectState } from "@app/state";
import log from "@common/logger";
import { computed, observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import { Container, Dimmer, Form, Header, InputOnChangeData, Loader, Segment } from "semantic-ui-react";
class LoginPageState {
@observable username = "";
@observable password = "";
@observable loading: boolean = false;
@computed get canLogin() {
return this.username.length > 0 && this.password.length > 0;
}
onUsernameChange = (e: any, data: InputOnChangeData) => {
this.username = data.value;
}
onPasswordChange = (e: any, data: InputOnChangeData) => {
this.password = data.value;
}
login(appState: AppState) {
this.loading = true;
appState.httpApi.tokenStore.grantPassword(this.username, this.password)
.then(() => {
this.loading = false;
log.info("logged in");
appState.history.push("/");
})
.catch((err) => {
this.loading = false;
log.error({ err }, "login error");
});
}
}
class LoginPage extends React.Component<{ appState: AppState }> {
pageState = new LoginPageState();
render() {
const { username, password, canLogin, loading } = this.pageState;
return (
<Container className="loginPage">
<Segment>
<Dimmer inverted active={loading}>
<Loader/>
</Dimmer>
<Header as="h1">Login</Header>
<Form>
<Form.Input label="Username" value={username} onChange={this.pageState.onUsernameChange}/>
<Form.Input
label="Password"
value={password}
type="password"
onChange={this.pageState.onPasswordChange}
/>
<Form.Button disabled={!canLogin} onClick={this.login}>Login</Form.Button>
</Form>
</Segment>
</Container>
);
}
login = () => {
this.pageState.login(this.props.appState);
}
}
const DecoratedLoginPage = injectState(observer(LoginPage));
export { DecoratedLoginPage as LoginPage };

18
app/pages/index.tsx

@ -0,0 +1,18 @@
import * as React from "react";
import { RouteComponentProps } from "react-router";
import { DevicesView, MessageTest} from "@app/components";
export { LoginPage } from "./LoginPage";
export function DevicePage({ match }: RouteComponentProps<{ deviceId: string }>) {
return (
<DevicesView deviceId={match.params.deviceId}/>
);
}
export function MessagesTestPage() {
return (
<MessageTest/>
);
}

34
app/sprinklersRpc/websocketClient.ts

@ -1,6 +1,7 @@
import { action, observable, when } from "mobx"; import { action, observable, when } from "mobx";
import { update } from "serializr"; import { update } from "serializr";
import { TokenStore } from "@app/state/TokenStore";
import * as rpc from "@common/jsonRpc"; import * as rpc from "@common/jsonRpc";
import logger from "@common/logger"; import logger from "@common/logger";
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests"; import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
@ -18,17 +19,15 @@ const RECONNECT_TIMEOUT_MS = 5000;
// tslint:disable:member-ordering // tslint:disable:member-ordering
export class WSSprinklersDevice extends s.SprinklersDevice { export class WSSprinklersDevice extends s.SprinklersDevice {
readonly api: WebSocketApiClient; readonly api: WebSocketRpcClient;
private _id: string; private _id: string;
constructor(api: WebSocketApiClient, id: string) { constructor(api: WebSocketRpcClient, id: string) {
super(); super();
this.api = api; this.api = api;
this._id = id; this._id = id;
when(() => api.connectionState.isConnected || false, () => { this.waitSubscribe();
this.subscribe();
});
} }
get id() { get id() {
@ -36,9 +35,6 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
} }
async subscribe() { async subscribe() {
if (this.api.accessToken) {
await this.api.authenticate(this.api.accessToken);
}
const subscribeRequest: ws.IDeviceSubscribeRequest = { const subscribeRequest: ws.IDeviceSubscribeRequest = {
deviceId: this.id, deviceId: this.id,
}; };
@ -58,26 +54,35 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
makeRequest(request: deviceRequests.Request): Promise<deviceRequests.Response> { makeRequest(request: deviceRequests.Request): Promise<deviceRequests.Response> {
return this.api.makeDeviceCall(this.id, request); return this.api.makeDeviceCall(this.id, request);
} }
waitSubscribe = () => {
when(() => this.api.connected, () => {
this.subscribe();
when(() => !this.api.connected, this.waitSubscribe);
});
}
} }
export class WebSocketApiClient implements s.SprinklersRPC { export class WebSocketRpcClient implements s.SprinklersRPC {
readonly webSocketUrl: string; readonly webSocketUrl: string;
devices: Map<string, WSSprinklersDevice> = new Map(); devices: Map<string, WSSprinklersDevice> = new Map();
@observable connectionState: s.ConnectionState = new s.ConnectionState(); @observable connectionState: s.ConnectionState = new s.ConnectionState();
socket: WebSocket | null = null; socket: WebSocket | null = null;
tokenStore: TokenStore;
private nextRequestId = Math.round(Math.random() * 1000000); private nextRequestId = Math.round(Math.random() * 1000000);
private responseCallbacks: ws.ServerResponseHandlers = {}; private responseCallbacks: ws.ServerResponseHandlers = {};
private reconnectTimer: number | null = null; private reconnectTimer: number | null = null;
accessToken: string | undefined;
get connected(): boolean { get connected(): boolean {
return this.connectionState.isConnected || false; return this.connectionState.isConnected || false;
} }
constructor(webSocketUrl: string) { constructor(webSocketUrl: string, tokenStore: TokenStore) {
this.webSocketUrl = webSocketUrl; this.webSocketUrl = webSocketUrl;
this.tokenStore = tokenStore;
this.connectionState.clientToServer = false; this.connectionState.clientToServer = false;
this.connectionState.serverToBroker = false; this.connectionState.serverToBroker = false;
} }
@ -115,6 +120,12 @@ export class WebSocketApiClient implements s.SprinklersRPC {
return this.makeRequest("authenticate", { accessToken }); return this.makeRequest("authenticate", { accessToken });
} }
async tryAuthenticate() {
when(() => this.tokenStore.accessToken.isValid, () => {
return this.authenticate(this.tokenStore.accessToken.token!);
});
}
// args must all be JSON serializable // args must all be JSON serializable
async makeDeviceCall(deviceId: string, request: deviceRequests.Request): Promise<deviceRequests.Response> { async makeDeviceCall(deviceId: string, request: deviceRequests.Request): Promise<deviceRequests.Response> {
if (this.socket == null) { if (this.socket == null) {
@ -194,6 +205,7 @@ export class WebSocketApiClient implements s.SprinklersRPC {
private onOpen() { private onOpen() {
log.info("established websocket connection"); log.info("established websocket connection");
this.connectionState.clientToServer = true; this.connectionState.clientToServer = true;
this.tryAuthenticate();
} }
/* tslint:disable-next-line:member-ordering */ /* tslint:disable-next-line:member-ordering */

16
app/state/ClientState.ts → app/state/AppState.ts

@ -1,26 +1,28 @@
import { WebSocketApiClient } from "@app/sprinklersRpc/websocketClient"; import { WebSocketRpcClient } from "@app/sprinklersRpc/websocketClient";
import HttpApi from "@app/state/HttpApi"; import HttpApi from "@app/state/HttpApi";
import { UiStore } from "@app/state/UiStore"; import { UiStore } from "@app/state/UiStore";
import { createBrowserHistory, History } from "history";
const isDev = process.env.NODE_ENV === "development"; const isDev = process.env.NODE_ENV === "development";
const websocketPort = isDev ? 8080 : location.port; const websocketPort = isDev ? 8080 : location.port;
export default class ClientState { export default class AppState {
sprinklersApi = new WebSocketApiClient(`ws://${location.hostname}:${websocketPort}`); history: History = createBrowserHistory();
uiStore = new UiStore(); uiStore = new UiStore();
httpApi = new HttpApi(); httpApi = new HttpApi();
tokenStore = this.httpApi.tokenStore;
sprinklersRpc = new WebSocketRpcClient(`ws://${location.hostname}:${websocketPort}`,
this.tokenStore);
async start() { async start() {
if (!this.httpApi.tokenStore.accessToken.isValid) { if (!this.httpApi.tokenStore.accessToken.isValid) {
if (this.httpApi.tokenStore.refreshToken.isValid) { if (this.httpApi.tokenStore.refreshToken.isValid) {
await this.httpApi.tokenStore.grantRefresh(); await this.httpApi.tokenStore.grantRefresh();
} else { } else {
await this.httpApi.tokenStore.grantPassword("alex", "kakashka"); this.history.push("/login");
} }
} }
this.sprinklersApi.accessToken = this.httpApi.tokenStore.accessToken.token!; this.sprinklersRpc.start();
this.sprinklersApi.start();
} }
} }

1
app/state/HttpApi.ts

@ -1,4 +1,3 @@
import { Token } from "@app/state/Token";
import { TokenStore } from "@app/state/TokenStore"; import { TokenStore } from "@app/state/TokenStore";
export class HttpApiError extends Error { export class HttpApiError extends Error {

2
app/state/Token.ts

@ -62,4 +62,4 @@ export class Token {
@computed get isValid() { @computed get isValid() {
return this.token != null && !this.isExpired; return this.token != null && !this.isExpired;
} }
} }

7
app/state/TokenStore.ts

@ -5,6 +5,8 @@ import { Token } from "@app/state/Token";
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/http"; import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/http";
import logger from "@common/logger"; import logger from "@common/logger";
const log = logger.child({ source: "TokenStore"});
export class TokenStore { export class TokenStore {
@observable accessToken: Token = new Token(); @observable accessToken: Token = new Token();
@observable refreshToken: Token = new Token(); @observable refreshToken: Token = new Token();
@ -24,7 +26,7 @@ export class TokenStore {
}, request); }, request);
this.accessToken.token = response.access_token; this.accessToken.token = response.access_token;
this.refreshToken.token = response.refresh_token; this.refreshToken.token = response.refresh_token;
logger.debug({ aud: this.accessToken.claims!.aud }, "got password grant tokens"); log.debug({ aud: this.accessToken.claims!.aud }, "got password grant tokens");
} }
async grantRefresh() { async grantRefresh() {
@ -39,7 +41,6 @@ export class TokenStore {
}, request); }, request);
this.accessToken.token = response.access_token; this.accessToken.token = response.access_token;
this.refreshToken.token = response.refresh_token; this.refreshToken.token = response.refresh_token;
logger.debug({ aud: this.accessToken.claims!.aud }, "got refresh grant tokens"); log.debug({ aud: this.accessToken.claims!.aud }, "got refresh grant tokens");
} }
} }

8
app/state/index.ts

@ -1,9 +1,3 @@
export { UiMessage, UiStore } from "./UiStore"; export { UiMessage, UiStore } from "./UiStore";
export * from "./reactContext"; export * from "./reactContext";
export { ClientState as StateBase } from "./ClientState"; export { default as AppState } from "./AppState";
import ClientState from "./ClientState";
export class WebApiState extends ClientState {
}

23
app/state/reactContext.tsx

@ -1,15 +1,15 @@
import * as React from "react"; import * as React from "react";
import { StateBase } from "@app/state"; import { AppState } from "@app/state";
const StateContext = React.createContext<StateBase | null>(null); const StateContext = React.createContext<AppState | null>(null);
export interface ProvideStateProps { export interface ProvideStateProps {
state: StateBase; state: AppState;
children: React.ReactNode; children: React.ReactNode;
} }
export function ProvideState({state, children}: ProvideStateProps) { export function ProvideState({ state, children }: ProvideStateProps) {
return ( return (
<StateContext.Provider value={state}> <StateContext.Provider value={state}>
{children} {children}
@ -18,11 +18,11 @@ export function ProvideState({state, children}: ProvideStateProps) {
} }
export interface ConsumeStateProps { export interface ConsumeStateProps {
children: (state: StateBase) => React.ReactNode; children: (state: AppState) => React.ReactNode;
} }
export function ConsumeState({children}: ConsumeStateProps) { export function ConsumeState({ children }: ConsumeStateProps) {
const consumeState = (state: StateBase | null) => { const consumeState = (state: AppState | null) => {
if (state == null) { if (state == null) {
throw new Error("Component with ConsumeState must be mounted inside ProvideState"); throw new Error("Component with ConsumeState must be mounted inside ProvideState");
} }
@ -35,14 +35,15 @@ type Diff<T extends string | number | symbol, U extends string | number | symbol
({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]; ({[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]}; 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>) { export function injectState<P extends { appState: AppState }>(Component: React.ComponentType<P>):
return class extends React.Component<Omit<P, "state">> { React.ComponentClass<Omit<P, "appState">> {
return class extends React.Component<Omit<P, "appState">> {
render() { render() {
const consumeState = (state: StateBase | null) => { const consumeState = (state: AppState | null) => {
if (state == null) { if (state == null) {
throw new Error("Component with injectState must be mounted inside ProvideState"); throw new Error("Component with injectState must be mounted inside ProvideState");
} }
return <Component {...this.props} state={state}/>; return <Component {...this.props} appState={state}/>;
}; };
return <StateContext.Consumer>{consumeState}</StateContext.Consumer>; return <StateContext.Consumer>{consumeState}</StateContext.Consumer>;
} }

100
app/styles/app.scss

@ -1,45 +1,46 @@
.app { .app {
margin-top: 1em; margin-top: 1em;
} }
.sectionRunner--pausedState { .sectionRunner--pausedState {
padding-left: .75em; padding-left: .75em;
font-size: .75em; font-size: .75em;
font-weight: lighter; font-weight: lighter;
} }
.sectionRunner--pausedState > .fa { .sectionRunner--pausedState > .fa {
padding-right: .2em; padding-right: .2em;
} }
.sectionRunner--pausedState-unpaused { .sectionRunner--pausedState-unpaused {
} }
.flex-horizontal-space-between { .flex-horizontal-space-between {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
} }
.sectionRun .progress { .sectionRun .progress {
margin: 1em 0 0 !important; margin: 1em 0 0 !important;
} }
.sectionRun .ui.progress .bar { .sectionRun .ui.progress .bar {
-webkit-transition: none; -webkit-transition: none;
transition: none; transition: none;
min-width: 0 !important; min-width: 0 !important;
} }
.section--number, .section--number,
.program--number { .program--number {
width: 2em width: 2em
} }
.section--name /*, .section--name /*,
.program--name*/ { .program--name*/
width: 10em; {
white-space: nowrap; width: 10em;
white-space: nowrap;
} }
.section--state { .section--state {
@ -47,17 +48,16 @@
} }
.ui.table { .ui.table {
tr > td.program--running { tr > td.program--running {
display: flex !important; display: flex !important;
@media only screen and (min-width: 768px) { @media only screen and (min-width: 768px) {
//line-height: 36px; //line-height: 36px;
}
} }
}
} }
.section--state-true { .section--state-true {
color: green; color: green;
} }
.section--state-false { .section--state-false {
@ -66,25 +66,55 @@
.durationInput--minutes, .durationInput--minutes,
.durationInput--seconds { .durationInput--seconds {
min-width: 6em !important; min-width: 6em !important;
} }
.durationInput .ui.labeled.input > .label { .durationInput .ui.labeled.input > .label {
width: 3em; width: 3em;
} }
.messages { .messages {
position: fixed; position: fixed;
/* top: 12px; */ /* top: 12px; */
bottom: 1em; bottom: 1em;
left: 1em; left: 1em;
right: 1em; right: 1em;
padding-left: 0; padding-left: 0;
z-index: 1000; z-index: 1000;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.flex-spacer { .flex-spacer {
flex: 1; flex: 1;
}
.ui.container.loginPage {
margin-top: 1em;
.ui.header {
text-align: center;
}
/* Mobile */
@media only screen and (max-width: 767px) {
width: auto !important;
margin-left: 1em !important;
margin-right: 1em !important;
}
/* Tablet */
@media only screen and (min-width: 768px) and (max-width: 991px) {
width: 600px;
}
/* Small Monitor */
@media only screen and (min-width: 992px) and (max-width: 1199px) {
width: 600px;
}
/* Large Monitor */
@media only screen and (min-width: 1200px) {
width: 800px;
}
} }

2
common/http.ts

@ -14,4 +14,4 @@ export type TokenGrantRequest = TokenGrantPasswordRequest | TokenGrantRefreshReq
export interface TokenGrantResponse { export interface TokenGrantResponse {
access_token: string; access_token: string;
refresh_token: string; refresh_token: string;
} }

0
server/express/logger.ts → server/express/expressLogger.ts

16
server/sprinklersRpc/websocketServer.ts

@ -33,12 +33,6 @@ export class WebSocketClient {
} }
start() { start() {
this.disposers.push(autorun(() => {
const updateData: ws.IBrokerConnectionUpdate = {
brokerConnected: this.state.mqttClient.connected,
};
this.sendNotification("brokerConnectionUpdate", updateData);
}));
this.socket.on("message", this.handleSocketMessage); this.socket.on("message", this.handleSocketMessage);
this.socket.on("close", this.stop); this.socket.on("close", this.stop);
} }
@ -48,6 +42,15 @@ export class WebSocketClient {
this.api.removeClient(this); this.api.removeClient(this);
} }
private subscribeBrokerConnection() {
this.disposers.push(autorun(() => {
const updateData: ws.IBrokerConnectionUpdate = {
brokerConnected: this.state.mqttClient.connected,
};
this.sendNotification("brokerConnectionUpdate", updateData);
}));
}
private checkAuthorization() { private checkAuthorization() {
if (!this.userId) { if (!this.userId) {
throw new ws.RpcError("this WebSocket session has not been authenticated", throw new ws.RpcError("this WebSocket session has not been authenticated",
@ -68,6 +71,7 @@ export class WebSocketClient {
} }
this.userId = decoded.aud; this.userId = decoded.aud;
log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client"); log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client");
this.subscribeBrokerConnection();
return { return {
result: "success", result: "success",
data: { authenticated: true, message: "authenticated" }, data: { authenticated: true, message: "authenticated" },

Loading…
Cancel
Save