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 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);
|
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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} />
|
||||||
));
|
));
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
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 { 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 */
|
||||||
|
@ -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,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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
|
||||||
}
|
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user