Added login page and lots of related improvments
This commit is contained in:
parent
41ece40a84
commit
2baca5fdd0
@ -1,47 +1,37 @@
|
||||
import { observer } from "mobx-react";
|
||||
// import DevTools from "mobx-react-devtools";
|
||||
import * as React from "react";
|
||||
import { Redirect, Route, RouteComponentProps, Switch } from "react-router";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Redirect, Route, Switch } from "react-router";
|
||||
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
|
||||
import "font-awesome/css/font-awesome.css";
|
||||
import "semantic-ui-css/semantic.css";
|
||||
import "@app/styles/app.scss";
|
||||
|
||||
function DevicePage({match}: RouteComponentProps<{deviceId: string}>) {
|
||||
function NavContainer() {
|
||||
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 (
|
||||
<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);
|
||||
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react";
|
||||
import * as React from "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 { ProgramTable, RunSectionForm, SectionRunnerView, SectionTable } from ".";
|
||||
import "./DeviceView.scss";
|
||||
@ -45,13 +45,13 @@ const ConnectionState = observer(({ connectionState, className }:
|
||||
|
||||
interface DeviceViewProps {
|
||||
deviceId: string;
|
||||
state: StateBase;
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
class DeviceView extends React.Component<DeviceViewProps> {
|
||||
render() {
|
||||
const { uiStore, sprinklersApi } = this.props.state;
|
||||
const device = sprinklersApi.getDevice(this.props.deviceId);
|
||||
const { uiStore, sprinklersRpc } = this.props.appState;
|
||||
const device = sprinklersRpc.getDevice(this.props.deviceId);
|
||||
const { id, connectionState, sections, programs, sectionRunner } = device;
|
||||
const deviceBody = connectionState.isAvailable && (
|
||||
<React.Fragment>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import * as React from "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";
|
||||
|
||||
class MessageTest extends React.Component<{ state: StateBase }> {
|
||||
class MessageTest extends React.Component<{ appState: AppState }> {
|
||||
render() {
|
||||
return (
|
||||
<Segment>
|
||||
@ -17,20 +17,20 @@ class MessageTest extends React.Component<{ state: StateBase }> {
|
||||
}
|
||||
|
||||
private test1 = () => {
|
||||
this.props.state.uiStore.addMessage({
|
||||
this.props.appState.uiStore.addMessage({
|
||||
info: true, content: "Test Message! " + getRandomId(), header: "Header to test message",
|
||||
});
|
||||
}
|
||||
|
||||
private test2 = () => {
|
||||
this.props.state.uiStore.addMessage({
|
||||
this.props.appState.uiStore.addMessage({
|
||||
warning: true, content: "Im gonna dissapear in 5 seconds " + getRandomId(),
|
||||
header: "Header to test message", timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
private test3 = () => {
|
||||
this.props.state.uiStore.addMessage({
|
||||
this.props.appState.uiStore.addMessage({
|
||||
color: "brown", content: <div className="ui segment">I Have crazy content!</div>,
|
||||
header: "Header to test message", timeout: 5000,
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react";
|
||||
import * as React from "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
|
||||
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() {
|
||||
const { uiStore } = this.props.state;
|
||||
const { uiStore } = this.props.appState;
|
||||
const messages = uiStore.messages.map((message) => (
|
||||
<MessageView key={message.id} uiStore={uiStore} message={message} />
|
||||
));
|
||||
|
@ -18,6 +18,9 @@ function NavBar({ location }: { location: Location }) {
|
||||
<Menu>
|
||||
<NavItem to="/devices/grinklers">Device grinklers</NavItem>
|
||||
<NavItem to="/messagesTest">Messages test</NavItem>
|
||||
<Menu.Menu position="right">
|
||||
<NavItem to="/login">Login</NavItem>
|
||||
</Menu.Menu>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,16 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { AppContainer } from "react-hot-loader";
|
||||
import { Router } from "react-router-dom";
|
||||
|
||||
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";
|
||||
|
||||
const state: StateBase = new StateClass();
|
||||
const state = new AppState();
|
||||
state.start()
|
||||
.catch((err) => {
|
||||
logger.error({err}, "error starting state");
|
||||
.catch((err: any) => {
|
||||
logger.error({ err }, "error starting state");
|
||||
});
|
||||
|
||||
const rootElem = document.getElementById("app");
|
||||
@ -18,7 +19,9 @@ const doRender = (Component: React.ComponentType) => {
|
||||
ReactDOM.render((
|
||||
<AppContainer>
|
||||
<ProvideState state={state}>
|
||||
<Component />
|
||||
<Router history={state.history}>
|
||||
<Component/>
|
||||
</Router>
|
||||
</ProvideState>
|
||||
</AppContainer>
|
||||
), rootElem);
|
||||
|
75
app/pages/LoginPage.tsx
Normal file
75
app/pages/LoginPage.tsx
Normal file
@ -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
Normal file
18
app/pages/index.tsx
Normal file
@ -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/>
|
||||
);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { action, observable, when } from "mobx";
|
||||
import { update } from "serializr";
|
||||
|
||||
import { TokenStore } from "@app/state/TokenStore";
|
||||
import * as rpc from "@common/jsonRpc";
|
||||
import logger from "@common/logger";
|
||||
import * as deviceRequests from "@common/sprinklersRpc/deviceRequests";
|
||||
@ -18,17 +19,15 @@ const RECONNECT_TIMEOUT_MS = 5000;
|
||||
// tslint:disable:member-ordering
|
||||
|
||||
export class WSSprinklersDevice extends s.SprinklersDevice {
|
||||
readonly api: WebSocketApiClient;
|
||||
readonly api: WebSocketRpcClient;
|
||||
|
||||
private _id: string;
|
||||
|
||||
constructor(api: WebSocketApiClient, id: string) {
|
||||
constructor(api: WebSocketRpcClient, id: string) {
|
||||
super();
|
||||
this.api = api;
|
||||
this._id = id;
|
||||
when(() => api.connectionState.isConnected || false, () => {
|
||||
this.subscribe();
|
||||
});
|
||||
this.waitSubscribe();
|
||||
}
|
||||
|
||||
get id() {
|
||||
@ -36,9 +35,6 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
|
||||
}
|
||||
|
||||
async subscribe() {
|
||||
if (this.api.accessToken) {
|
||||
await this.api.authenticate(this.api.accessToken);
|
||||
}
|
||||
const subscribeRequest: ws.IDeviceSubscribeRequest = {
|
||||
deviceId: this.id,
|
||||
};
|
||||
@ -58,26 +54,35 @@ export class WSSprinklersDevice extends s.SprinklersDevice {
|
||||
makeRequest(request: deviceRequests.Request): Promise<deviceRequests.Response> {
|
||||
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;
|
||||
|
||||
devices: Map<string, WSSprinklersDevice> = new Map();
|
||||
@observable connectionState: s.ConnectionState = new s.ConnectionState();
|
||||
socket: WebSocket | null = null;
|
||||
|
||||
tokenStore: TokenStore;
|
||||
|
||||
private nextRequestId = Math.round(Math.random() * 1000000);
|
||||
private responseCallbacks: ws.ServerResponseHandlers = {};
|
||||
private reconnectTimer: number | null = null;
|
||||
accessToken: string | undefined;
|
||||
|
||||
get connected(): boolean {
|
||||
return this.connectionState.isConnected || false;
|
||||
}
|
||||
|
||||
constructor(webSocketUrl: string) {
|
||||
constructor(webSocketUrl: string, tokenStore: TokenStore) {
|
||||
this.webSocketUrl = webSocketUrl;
|
||||
this.tokenStore = tokenStore;
|
||||
this.connectionState.clientToServer = false;
|
||||
this.connectionState.serverToBroker = false;
|
||||
}
|
||||
@ -115,6 +120,12 @@ export class WebSocketApiClient implements s.SprinklersRPC {
|
||||
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
|
||||
async makeDeviceCall(deviceId: string, request: deviceRequests.Request): Promise<deviceRequests.Response> {
|
||||
if (this.socket == null) {
|
||||
@ -194,6 +205,7 @@ export class WebSocketApiClient implements s.SprinklersRPC {
|
||||
private onOpen() {
|
||||
log.info("established websocket connection");
|
||||
this.connectionState.clientToServer = true;
|
||||
this.tryAuthenticate();
|
||||
}
|
||||
|
||||
/* tslint:disable-next-line:member-ordering */
|
||||
|
@ -1,26 +1,28 @@
|
||||
import { WebSocketApiClient } from "@app/sprinklersRpc/websocketClient";
|
||||
import { WebSocketRpcClient } from "@app/sprinklersRpc/websocketClient";
|
||||
import HttpApi from "@app/state/HttpApi";
|
||||
import { UiStore } from "@app/state/UiStore";
|
||||
import { createBrowserHistory, History } from "history";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const websocketPort = isDev ? 8080 : location.port;
|
||||
|
||||
export default class ClientState {
|
||||
sprinklersApi = new WebSocketApiClient(`ws://${location.hostname}:${websocketPort}`);
|
||||
export default class AppState {
|
||||
history: History = createBrowserHistory();
|
||||
uiStore = new UiStore();
|
||||
httpApi = new HttpApi();
|
||||
tokenStore = this.httpApi.tokenStore;
|
||||
sprinklersRpc = new WebSocketRpcClient(`ws://${location.hostname}:${websocketPort}`,
|
||||
this.tokenStore);
|
||||
|
||||
async start() {
|
||||
if (!this.httpApi.tokenStore.accessToken.isValid) {
|
||||
if (this.httpApi.tokenStore.refreshToken.isValid) {
|
||||
await this.httpApi.tokenStore.grantRefresh();
|
||||
} else {
|
||||
await this.httpApi.tokenStore.grantPassword("alex", "kakashka");
|
||||
this.history.push("/login");
|
||||
}
|
||||
}
|
||||
|
||||
this.sprinklersApi.accessToken = this.httpApi.tokenStore.accessToken.token!;
|
||||
|
||||
this.sprinklersApi.start();
|
||||
this.sprinklersRpc.start();
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import { Token } from "@app/state/Token";
|
||||
import { TokenStore } from "@app/state/TokenStore";
|
||||
|
||||
export class HttpApiError extends Error {
|
||||
|
@ -62,4 +62,4 @@ export class Token {
|
||||
@computed get isValid() {
|
||||
return this.token != null && !this.isExpired;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ import { Token } from "@app/state/Token";
|
||||
import { TokenGrantPasswordRequest, TokenGrantRefreshRequest, TokenGrantResponse } from "@common/http";
|
||||
import logger from "@common/logger";
|
||||
|
||||
const log = logger.child({ source: "TokenStore"});
|
||||
|
||||
export class TokenStore {
|
||||
@observable accessToken: Token = new Token();
|
||||
@observable refreshToken: Token = new Token();
|
||||
@ -24,7 +26,7 @@ export class TokenStore {
|
||||
}, request);
|
||||
this.accessToken.token = response.access_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() {
|
||||
@ -39,7 +41,6 @@ export class TokenStore {
|
||||
}, request);
|
||||
this.accessToken.token = response.access_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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,3 @@
|
||||
export { UiMessage, UiStore } from "./UiStore";
|
||||
export * from "./reactContext";
|
||||
export { ClientState as StateBase } from "./ClientState";
|
||||
|
||||
import ClientState from "./ClientState";
|
||||
|
||||
|
||||
export class WebApiState extends ClientState {
|
||||
}
|
||||
export { default as AppState } from "./AppState";
|
||||
|
@ -1,15 +1,15 @@
|
||||
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 {
|
||||
state: StateBase;
|
||||
state: AppState;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ProvideState({state, children}: ProvideStateProps) {
|
||||
export function ProvideState({ state, children }: ProvideStateProps) {
|
||||
return (
|
||||
<StateContext.Provider value={state}>
|
||||
{children}
|
||||
@ -18,11 +18,11 @@ export function ProvideState({state, children}: ProvideStateProps) {
|
||||
}
|
||||
|
||||
export interface ConsumeStateProps {
|
||||
children: (state: StateBase) => React.ReactNode;
|
||||
children: (state: AppState) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function ConsumeState({children}: ConsumeStateProps) {
|
||||
const consumeState = (state: StateBase | null) => {
|
||||
export function ConsumeState({ children }: ConsumeStateProps) {
|
||||
const consumeState = (state: AppState | null) => {
|
||||
if (state == null) {
|
||||
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];
|
||||
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>) {
|
||||
return class extends React.Component<Omit<P, "state">> {
|
||||
export function injectState<P extends { appState: AppState }>(Component: React.ComponentType<P>):
|
||||
React.ComponentClass<Omit<P, "appState">> {
|
||||
return class extends React.Component<Omit<P, "appState">> {
|
||||
render() {
|
||||
const consumeState = (state: StateBase | null) => {
|
||||
const consumeState = (state: AppState | null) => {
|
||||
if (state == null) {
|
||||
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>;
|
||||
}
|
||||
|
@ -1,45 +1,46 @@
|
||||
.app {
|
||||
margin-top: 1em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.sectionRunner--pausedState {
|
||||
padding-left: .75em;
|
||||
font-size: .75em;
|
||||
font-weight: lighter;
|
||||
padding-left: .75em;
|
||||
font-size: .75em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
.sectionRunner--pausedState > .fa {
|
||||
padding-right: .2em;
|
||||
padding-right: .2em;
|
||||
}
|
||||
|
||||
.sectionRunner--pausedState-unpaused {
|
||||
}
|
||||
|
||||
.flex-horizontal-space-between {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sectionRun .progress {
|
||||
margin: 1em 0 0 !important;
|
||||
margin: 1em 0 0 !important;
|
||||
}
|
||||
|
||||
.sectionRun .ui.progress .bar {
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
min-width: 0 !important;
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.section--number,
|
||||
.program--number {
|
||||
width: 2em
|
||||
width: 2em
|
||||
}
|
||||
|
||||
.section--name /*,
|
||||
.program--name*/ {
|
||||
width: 10em;
|
||||
white-space: nowrap;
|
||||
.program--name*/
|
||||
{
|
||||
width: 10em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.section--state {
|
||||
@ -47,17 +48,16 @@
|
||||
}
|
||||
|
||||
.ui.table {
|
||||
tr > td.program--running {
|
||||
display: flex !important;
|
||||
@media only screen and (min-width: 768px) {
|
||||
//line-height: 36px;
|
||||
}
|
||||
tr > td.program--running {
|
||||
display: flex !important;
|
||||
@media only screen and (min-width: 768px) {
|
||||
//line-height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.section--state-true {
|
||||
color: green;
|
||||
color: green;
|
||||
}
|
||||
|
||||
.section--state-false {
|
||||
@ -66,25 +66,55 @@
|
||||
|
||||
.durationInput--minutes,
|
||||
.durationInput--seconds {
|
||||
min-width: 6em !important;
|
||||
min-width: 6em !important;
|
||||
}
|
||||
|
||||
.durationInput .ui.labeled.input > .label {
|
||||
width: 3em;
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
.messages {
|
||||
position: fixed;
|
||||
/* top: 12px; */
|
||||
bottom: 1em;
|
||||
left: 1em;
|
||||
right: 1em;
|
||||
padding-left: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
/* top: 12px; */
|
||||
bottom: 1em;
|
||||
left: 1em;
|
||||
right: 1em;
|
||||
padding-left: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
@ -14,4 +14,4 @@ export type TokenGrantRequest = TokenGrantPasswordRequest | TokenGrantRefreshReq
|
||||
export interface TokenGrantResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,6 @@ export class WebSocketClient {
|
||||
}
|
||||
|
||||
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("close", this.stop);
|
||||
}
|
||||
@ -48,6 +42,15 @@ export class WebSocketClient {
|
||||
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() {
|
||||
if (!this.userId) {
|
||||
throw new ws.RpcError("this WebSocket session has not been authenticated",
|
||||
@ -68,6 +71,7 @@ export class WebSocketClient {
|
||||
}
|
||||
this.userId = decoded.aud;
|
||||
log.info({ userId: decoded.aud, name: decoded.name }, "authenticated websocket client");
|
||||
this.subscribeBrokerConnection();
|
||||
return {
|
||||
result: "success",
|
||||
data: { authenticated: true, message: "authenticated" },
|
||||
|
Loading…
x
Reference in New Issue
Block a user