Re-engineer UI in TypeScript (#89)
* Re-engineer UI in TypeScript * Switch to named imports where possible * Restructure file system layout * Update depencencies * Update README.md * Change explicit colors for better support for dark theme
This commit is contained in:
parent
ea6aa78d60
commit
260e9a18d0
33
README.md
33
README.md
@ -213,21 +213,36 @@ The framework, and MaterialUI allows for a reasonable degree of customization wi
|
|||||||
|
|
||||||
### Theming the app
|
### Theming the app
|
||||||
|
|
||||||
The app can be easily themed by editing the [MaterialUI theme](https://material-ui.com/customization/themes/). Edit the theme in ['interface/src/App.js'](interface/src/App.js) as you desire:
|
The app can be easily themed by editing the [MaterialUI theme](https://material-ui.com/customization/theming/). Edit the theme in ['interface/src/CustomMuiTheme.tsx'](interface/src/CustomMuiTheme.tsx) as you desire. For example, here is a dark theme:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const theme = createMuiTheme({
|
const theme = createMuiTheme({
|
||||||
palette: {
|
palette: {
|
||||||
primary: red,
|
type:"dark",
|
||||||
secondary: deepOrange,
|
primary: {
|
||||||
highlight_idle: blueGrey[900],
|
main: '#222',
|
||||||
highlight_warn: orange[500],
|
},
|
||||||
highlight_error: red[500],
|
secondary: {
|
||||||
highlight_success: green[500],
|
main: '#666',
|
||||||
},
|
},
|
||||||
|
info: {
|
||||||
|
main: blueGrey[900]
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
main: orange[500]
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
main: red[500]
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: green[500]
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
![Dark Theme](/media/dark.png?raw=true "Dark Theme")
|
||||||
|
|
||||||
### Changing the app icon
|
### Changing the app icon
|
||||||
|
|
||||||
You can replace the app icon is located at ['interface/public/app/icon.png'](interface/public/app/icon.png) with one of your preference. A 256 x 256 PNG is recommended for best compatibility.
|
You can replace the app icon is located at ['interface/public/app/icon.png'](interface/public/app/icon.png) with one of your preference. A 256 x 256 PNG is recommended for best compatibility.
|
||||||
@ -448,7 +463,7 @@ Serial.println(wifiSettings.ssid);
|
|||||||
Configure the SSID and password:
|
Configure the SSID and password:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
WiFiSettings wifiSettings = esp8266React->getWiFiSettingsService()->fetch();
|
WiFiSettings wifiSettings = esp8266React.getWiFiSettingsService()->fetch();
|
||||||
wifiSettings.ssid = "MyNetworkSSID";
|
wifiSettings.ssid = "MyNetworkSSID";
|
||||||
wifiSettings.password = "MySuperSecretPassword";
|
wifiSettings.password = "MySuperSecretPassword";
|
||||||
esp8266React.getWiFiSettingsService()->update(wifiSettings);
|
esp8266React.getWiFiSettingsService()->update(wifiSettings);
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# Change the IP address to that of your ESP device to enable local development of the UI.
|
# Change the IP address to that of your ESP device to enable local development of the UI.
|
||||||
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
|
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
|
||||||
REACT_APP_ENDPOINT_ROOT=http://192.168.0.29/rest/
|
REACT_APP_ENDPOINT_ROOT=http://192.168.0.21/rest/
|
||||||
|
7965
interface/package-lock.json
generated
7965
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,37 +3,51 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.7.0",
|
"@material-ui/core": "^4.9.1",
|
||||||
"@material-ui/icons": "^4.5.1",
|
"@material-ui/icons": "^4.9.1",
|
||||||
"compression-webpack-plugin": "^2.0.0",
|
"@types/jwt-decode": "^2.2.1",
|
||||||
|
"@types/node": "^12.12.22",
|
||||||
|
"@types/react": "^16.9.17",
|
||||||
|
"@types/react-dom": "^16.9.4",
|
||||||
|
"@types/react-material-ui-form-validator": "^2.0.5",
|
||||||
|
"@types/react-router": "^5.1.3",
|
||||||
|
"@types/react-router-dom": "^5.1.3",
|
||||||
|
"compression-webpack-plugin": "^3.0.1",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"mime-types": "^2.1.25",
|
"mime-types": "^2.1.25",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"notistack": "^0.9.6",
|
"notistack": "^0.9.7",
|
||||||
"prop-types": "^15.7.2",
|
"react": "^16.12.0",
|
||||||
"react": "^16.10.1",
|
"react-dom": "^16.12.0",
|
||||||
"react-dom": "^16.10.1",
|
|
||||||
"react-form-validator-core": "^0.6.4",
|
"react-form-validator-core": "^0.6.4",
|
||||||
"react-jss": "^10.0.0",
|
"react-material-ui-form-validator": "^2.0.10",
|
||||||
"react-material-ui-form-validator": "^2.0.9",
|
"react-router": "^5.1.2",
|
||||||
"react-router": "^5.1.1",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-router-dom": "^5.1.1",
|
"react-scripts": "3.3.1",
|
||||||
"react-scripts": "3.0.1",
|
"typescript": "^3.7.5",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
"build": "react-app-rewired build",
|
"build": "react-app-rewired build",
|
||||||
"test": "react-app-rewired test --env=jsdom",
|
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"eslintConfig": {
|
||||||
"react-app-rewired": "^2.1.3"
|
"extends": "react-app"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": {
|
||||||
">0.2%",
|
"production": [
|
||||||
"not dead",
|
">0.2%",
|
||||||
"not ie <= 11",
|
"not dead",
|
||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
]
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"react-app-rewired": "^2.1.5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Redirect, Route, Switch } from 'react-router';
|
|
||||||
|
|
||||||
import AppRouting from './AppRouting';
|
|
||||||
import { PROJECT_NAME } from './constants/Env';
|
|
||||||
|
|
||||||
import { SnackbarProvider } from 'notistack';
|
|
||||||
import { create } from 'jss';
|
|
||||||
|
|
||||||
import { CssBaseline, IconButton, MuiThemeProvider, createMuiTheme } from '@material-ui/core';
|
|
||||||
import { StylesProvider, jssPreset } from '@material-ui/styles';
|
|
||||||
import { blueGrey, indigo, orange, red, green } from '@material-ui/core/colors';
|
|
||||||
import CloseIcon from '@material-ui/icons/Close';
|
|
||||||
|
|
||||||
|
|
||||||
// Our theme
|
|
||||||
const theme = createMuiTheme({
|
|
||||||
palette: {
|
|
||||||
primary: indigo,
|
|
||||||
secondary: blueGrey,
|
|
||||||
highlight_idle: blueGrey[900],
|
|
||||||
highlight_warn: orange[500],
|
|
||||||
highlight_error: red[500],
|
|
||||||
highlight_success: green[500],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// JSS instance
|
|
||||||
const jss = create(jssPreset());
|
|
||||||
|
|
||||||
// this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid.
|
|
||||||
const unauthorizedRedirect = () => <Redirect to="/" />;
|
|
||||||
|
|
||||||
class App extends Component {
|
|
||||||
|
|
||||||
notistackRef = React.createRef();
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
document.title = PROJECT_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickDismiss = (key) => () => {
|
|
||||||
this.notistackRef.current.closeSnackbar(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<StylesProvider jss={jss}>
|
|
||||||
<MuiThemeProvider theme={theme}>
|
|
||||||
<SnackbarProvider maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
||||||
ref={this.notistackRef}
|
|
||||||
action={(key) => (
|
|
||||||
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
|
||||||
<CloseIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}>
|
|
||||||
<CssBaseline />
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
|
||||||
<Route component={AppRouting} />
|
|
||||||
</Switch>
|
|
||||||
</SnackbarProvider>
|
|
||||||
</MuiThemeProvider>
|
|
||||||
</StylesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
47
interface/src/App.tsx
Normal file
47
interface/src/App.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, { Component, RefObject } from 'react';
|
||||||
|
import { Redirect, Route, Switch } from 'react-router';
|
||||||
|
import { SnackbarProvider } from 'notistack';
|
||||||
|
|
||||||
|
import { IconButton } from '@material-ui/core';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
|
||||||
|
import AppRouting from './AppRouting';
|
||||||
|
import CustomMuiTheme from './CustomMuiTheme';
|
||||||
|
import { PROJECT_NAME } from './api';
|
||||||
|
|
||||||
|
// this redirect forces a call to authenticationContext.refresh() which invalidates the JWT if it is invalid.
|
||||||
|
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
|
||||||
|
notistackRef: RefObject<any> = React.createRef();
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.title = PROJECT_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickDismiss = (key: string | number | undefined) => () => {
|
||||||
|
this.notistackRef.current.closeSnackbar(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CustomMuiTheme>
|
||||||
|
<SnackbarProvider maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
|
ref={this.notistackRef}
|
||||||
|
action={(key) => (
|
||||||
|
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
)}>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
||||||
|
<Route component={AppRouting} />
|
||||||
|
</Switch>
|
||||||
|
</SnackbarProvider>
|
||||||
|
</CustomMuiTheme>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
@ -1,23 +1,24 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { Switch, Redirect } from 'react-router';
|
||||||
|
|
||||||
import { Redirect, Switch } from 'react-router';
|
|
||||||
|
|
||||||
import { PROJECT_PATH } from './constants/Env';
|
|
||||||
import * as Authentication from './authentication/Authentication';
|
import * as Authentication from './authentication/Authentication';
|
||||||
import AuthenticationWrapper from './authentication/AuthenticationWrapper';
|
import AuthenticationWrapper from './authentication/AuthenticationWrapper';
|
||||||
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
|
|
||||||
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
|
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
|
||||||
import SignInPage from './containers/SignInPage';
|
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
|
||||||
import WiFiConnection from './sections/WiFiConnection';
|
|
||||||
import AccessPoint from './sections/AccessPoint';
|
import SignIn from './SignIn';
|
||||||
import NetworkTime from './sections/NetworkTime';
|
|
||||||
import Security from './sections/Security';
|
|
||||||
import System from './sections/System';
|
|
||||||
import ProjectRouting from './project/ProjectRouting';
|
import ProjectRouting from './project/ProjectRouting';
|
||||||
|
import WiFiConnection from './wifi/WiFiConnection';
|
||||||
|
import AccessPoint from './ap/AccessPoint';
|
||||||
|
import NetworkTime from './ntp/NetworkTime';
|
||||||
|
import Security from './security/Security';
|
||||||
|
import System from './system/System';
|
||||||
|
|
||||||
|
import { PROJECT_PATH } from './api';
|
||||||
|
|
||||||
class AppRouting extends Component {
|
class AppRouting extends Component {
|
||||||
|
|
||||||
componentWillMount() {
|
componentDidMount() {
|
||||||
Authentication.clearLoginRedirect();
|
Authentication.clearLoginRedirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,13 +26,13 @@ class AppRouting extends Component {
|
|||||||
return (
|
return (
|
||||||
<AuthenticationWrapper>
|
<AuthenticationWrapper>
|
||||||
<Switch>
|
<Switch>
|
||||||
<UnauthenticatedRoute exact path="/" component={SignInPage} />
|
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||||
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
|
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
||||||
|
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
|
||||||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||||
<AuthenticatedRoute exact path="/security/*" component={Security} />
|
<AuthenticatedRoute exact path="/security/*" component={Security} />
|
||||||
<AuthenticatedRoute exact path="/system/*" component={System} />
|
<AuthenticatedRoute exact path="/system/*" component={System} />
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
|
||||||
<Redirect to="/" />
|
<Redirect to="/" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</AuthenticationWrapper>
|
</AuthenticationWrapper>
|
39
interface/src/CustomMuiTheme.tsx
Normal file
39
interface/src/CustomMuiTheme.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { CssBaseline } from '@material-ui/core';
|
||||||
|
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
|
||||||
|
import { blueGrey, indigo, orange, red, green } from '@material-ui/core/colors';
|
||||||
|
|
||||||
|
const theme = createMuiTheme({
|
||||||
|
palette: {
|
||||||
|
primary: indigo,
|
||||||
|
secondary: blueGrey,
|
||||||
|
info: {
|
||||||
|
main: blueGrey[900]
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
main: orange[500]
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
main: red[500]
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: green[500]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default class CustomMuiTheme extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<StylesProvider>
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{this.props.children}
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</StylesProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,54 +1,55 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
import Paper from '@material-ui/core/Paper';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
import Fab from '@material-ui/core/Fab';
|
|
||||||
import { PROJECT_NAME } from '../constants/Env';
|
|
||||||
import ForwardIcon from '@material-ui/icons/Forward';
|
|
||||||
import { withSnackbar } from 'notistack';
|
|
||||||
import { SIGN_IN_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { withAuthenticationContext } from '../authentication/Context';
|
|
||||||
import PasswordValidator from '../components/PasswordValidator';
|
|
||||||
|
|
||||||
const styles = theme => {
|
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
|
||||||
return {
|
import { Paper, Typography, Fab } from '@material-ui/core';
|
||||||
loginPage: {
|
import ForwardIcon from '@material-ui/icons/Forward';
|
||||||
display: "flex",
|
|
||||||
height: "100vh",
|
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
||||||
margin: "auto",
|
import {PasswordValidator} from './components';
|
||||||
padding: theme.spacing(2),
|
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||||
justifyContent: "center",
|
|
||||||
flexDirection: "column",
|
const styles = (theme: Theme) => createStyles({
|
||||||
maxWidth: theme.breakpoints.values.sm
|
loginPage: {
|
||||||
},
|
display: "flex",
|
||||||
loginPanel: {
|
height: "100vh",
|
||||||
textAlign: "center",
|
margin: "auto",
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
paddingTop: "200px",
|
justifyContent: "center",
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
flexDirection: "column",
|
||||||
backgroundRepeat: "no-repeat",
|
maxWidth: theme.breakpoints.values.sm
|
||||||
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
},
|
||||||
backgroundSize: "auto 150px",
|
loginPanel: {
|
||||||
width: "100%"
|
textAlign: "center",
|
||||||
},
|
padding: theme.spacing(2),
|
||||||
extendedIcon: {
|
paddingTop: "200px",
|
||||||
marginRight: theme.spacing(0.5),
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
},
|
backgroundRepeat: "no-repeat",
|
||||||
textField: {
|
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
||||||
width: "100%"
|
backgroundSize: "auto 150px",
|
||||||
},
|
width: "100%"
|
||||||
button: {
|
},
|
||||||
marginRight: theme.spacing(2),
|
extendedIcon: {
|
||||||
marginTop: theme.spacing(2),
|
marginRight: theme.spacing(0.5),
|
||||||
}
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type SignInPageProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
|
||||||
|
|
||||||
|
interface SignInPageState {
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
processing: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SignInPage extends Component<SignInPageProps, SignInPageState> {
|
||||||
|
|
||||||
class SignInPage extends Component {
|
constructor(props: SignInPageProps) {
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
username: '',
|
username: '',
|
||||||
@ -57,8 +58,12 @@ class SignInPage extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleValueChange = name => event => {
|
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ [name]: event.target.value });
|
const { name, value } = event.currentTarget;
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: value,
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
@ -105,9 +110,10 @@ class SignInPage extends Component {
|
|||||||
errorMessages={['Username is required']}
|
errorMessages={['Username is required']}
|
||||||
name="username"
|
name="username"
|
||||||
label="Username"
|
label="Username"
|
||||||
className={classes.textField}
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={this.handleValueChange('username')}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
@ -116,9 +122,10 @@ class SignInPage extends Component {
|
|||||||
errorMessages={['Password is required']}
|
errorMessages={['Password is required']}
|
||||||
name="password"
|
name="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
className={classes.textField}
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={this.handleValueChange('password')}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
||||||
@ -133,6 +140,4 @@ class SignInPage extends Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticationContext(
|
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignInPage)));
|
||||||
withSnackbar(withStyles(styles)(SignInPage))
|
|
||||||
);
|
|
7
interface/src/ap/APModes.ts
Normal file
7
interface/src/ap/APModes.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { APSettings } from "./types";
|
||||||
|
|
||||||
|
export const AP_MODE_ALWAYS = 0;
|
||||||
|
export const AP_MODE_DISCONNECTED = 1;
|
||||||
|
export const AP_NEVER = 2;
|
||||||
|
|
||||||
|
export const isAPEnabled = ({ provision_mode }: APSettings) => provision_mode === AP_MODE_ALWAYS || provision_mode === AP_MODE_DISCONNECTED;
|
30
interface/src/ap/APSettingsController.tsx
Normal file
30
interface/src/ap/APSettingsController.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { AP_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
|
||||||
|
import APSettingsForm from './APSettingsForm';
|
||||||
|
import { APSettings } from './types';
|
||||||
|
|
||||||
|
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
||||||
|
|
||||||
|
class APSettingsController extends Component<APSettingsControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="Access Point Settings" titleGutter>
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <APSettingsForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
71
interface/src/ap/APSettingsForm.tsx
Normal file
71
interface/src/ap/APSettingsForm.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
|
import {PasswordValidator, RestFormProps, FormActions, FormButton} from '../components';
|
||||||
|
|
||||||
|
import { isAPEnabled, AP_MODE_ALWAYS, AP_MODE_DISCONNECTED, AP_NEVER } from './APModes';
|
||||||
|
import { APSettings } from './types';
|
||||||
|
|
||||||
|
type APSettingsFormProps = RestFormProps<APSettings>;
|
||||||
|
|
||||||
|
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data, handleValueChange, saveData, loadData } = this.props;
|
||||||
|
return (
|
||||||
|
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
||||||
|
<SelectValidator name="provision_mode"
|
||||||
|
label="Provide Access Point..."
|
||||||
|
value={data.provision_mode}
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onChange={handleValueChange('provision_mode')}
|
||||||
|
margin="normal">
|
||||||
|
<MenuItem value={AP_MODE_ALWAYS}>Always</MenuItem>
|
||||||
|
<MenuItem value={AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
|
||||||
|
<MenuItem value={AP_NEVER}>Never</MenuItem>
|
||||||
|
</SelectValidator>
|
||||||
|
{
|
||||||
|
isAPEnabled(data) &&
|
||||||
|
<Fragment>
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'matchRegexp:^.{1,32}$']}
|
||||||
|
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
|
||||||
|
name="ssid"
|
||||||
|
label="Access Point SSID"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.ssid}
|
||||||
|
onChange={handleValueChange('ssid')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<PasswordValidator
|
||||||
|
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||||
|
errorMessages={['Access Point Password is required', 'Access Point Password must be 64 characters or less']}
|
||||||
|
name="password"
|
||||||
|
label="Access Point Password"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.password}
|
||||||
|
onChange={handleValueChange('password')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
<FormActions>
|
||||||
|
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||||
|
Save
|
||||||
|
</FormButton>
|
||||||
|
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||||
|
Reset
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</ValidatorForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APSettingsForm;
|
10
interface/src/ap/APStatus.ts
Normal file
10
interface/src/ap/APStatus.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Theme } from "@material-ui/core";
|
||||||
|
import { APStatus } from "./types";
|
||||||
|
|
||||||
|
export const apStatusHighlight = ({ active }: APStatus, theme: Theme) => {
|
||||||
|
return active ? theme.palette.success.main : theme.palette.info.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apStatus = ({ active }: APStatus) => {
|
||||||
|
return active ? "Active" : "Inactive";
|
||||||
|
};
|
29
interface/src/ap/APStatusController.tsx
Normal file
29
interface/src/ap/APStatusController.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
import { AP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
|
import APStatusForm from './APStatusForm';
|
||||||
|
import { APStatus } from './types';
|
||||||
|
|
||||||
|
type APStatusControllerProps = RestControllerProps<APStatus>;
|
||||||
|
|
||||||
|
class APStatusController extends Component<APStatusControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="Access Point Status">
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <APStatusForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(AP_STATUS_ENDPOINT, APStatusController);
|
78
interface/src/ap/APStatusForm.tsx
Normal file
78
interface/src/ap/APStatusForm.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
|
||||||
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
|
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||||
|
|
||||||
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
|
import ComputerIcon from '@material-ui/icons/Computer';
|
||||||
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
|
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
||||||
|
import { apStatusHighlight, apStatus } from './APStatus';
|
||||||
|
import { APStatus } from './types';
|
||||||
|
|
||||||
|
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
||||||
|
|
||||||
|
class APStatusForm extends Component<APStatusFormProps> {
|
||||||
|
|
||||||
|
createListItems() {
|
||||||
|
const { data, theme } = this.props
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<HighlightAvatar color={apStatusHighlight(data, theme)}>
|
||||||
|
<SettingsInputAntennaIcon />
|
||||||
|
</HighlightAvatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="Status" secondary={apStatus(data)} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>IP</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="IP Address" secondary={data.ip_address} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<DeviceHubIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<ComputerIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="AP Clients" secondary={data.station_num} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<List>
|
||||||
|
{this.createListItems()}
|
||||||
|
</List>
|
||||||
|
<FormActions>
|
||||||
|
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||||
|
Refresh
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTheme(APStatusForm);
|
38
interface/src/ap/AccessPoint.tsx
Normal file
38
interface/src/ap/AccessPoint.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
||||||
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
|
import APSettingsController from './APSettingsController';
|
||||||
|
import APStatusController from './APStatusController';
|
||||||
|
|
||||||
|
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
|
class AccessPoint extends Component<AccessPointProps> {
|
||||||
|
|
||||||
|
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||||
|
this.props.history.push(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { authenticatedContext } = this.props;
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="Access Point">
|
||||||
|
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||||
|
<Tab value="/ap/status" label="Access Point Status" />
|
||||||
|
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
|
||||||
|
</Tabs>
|
||||||
|
<Switch>
|
||||||
|
<AuthenticatedRoute exact={true} path="/ap/status" component={APStatusController} />
|
||||||
|
<AuthenticatedRoute exact={true} path="/ap/settings" component={APSettingsController} />
|
||||||
|
<Redirect to="/ap/status" />
|
||||||
|
</Switch>
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticatedContext(AccessPoint);
|
12
interface/src/ap/types.ts
Normal file
12
interface/src/ap/types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface APStatus {
|
||||||
|
active: boolean;
|
||||||
|
ip_address: string;
|
||||||
|
mac_address: string;
|
||||||
|
station_num: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APSettings {
|
||||||
|
provision_mode: number;
|
||||||
|
ssid: string;
|
||||||
|
password: string;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { ENDPOINT_ROOT } from '../constants/Env';
|
import { ENDPOINT_ROOT } from './Env';
|
||||||
|
|
||||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
|
||||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";
|
3
interface/src/api/Env.ts
Normal file
3
interface/src/api/Env.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
||||||
|
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||||
|
export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT!;
|
2
interface/src/api/index.ts
Normal file
2
interface/src/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Env'
|
||||||
|
export * from './Endpoints'
|
@ -1,36 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import {
|
|
||||||
Redirect, Route
|
|
||||||
} from "react-router-dom";
|
|
||||||
|
|
||||||
import { withAuthenticationContext } from './Context.js';
|
|
||||||
import * as Authentication from './Authentication';
|
|
||||||
import { withSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
export class AuthenticatedRoute extends React.Component {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
|
||||||
const { location } = this.props;
|
|
||||||
const renderComponent = (props) => {
|
|
||||||
if (authenticationContext.isAuthenticated()) {
|
|
||||||
return (
|
|
||||||
<Component {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Authentication.storeLoginRedirect(location);
|
|
||||||
enqueueSnackbar("Please log in to continue.", {
|
|
||||||
variant: 'info',
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<Redirect to='/' />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Route {...rest} render={renderComponent} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
|
42
interface/src/authentication/AuthenticatedRoute.tsx
Normal file
42
interface/src/authentication/AuthenticatedRoute.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||||
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
|
import * as Authentication from './Authentication';
|
||||||
|
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext } from './AuthenticationContext';
|
||||||
|
|
||||||
|
type ChildComponent = React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||||
|
|
||||||
|
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
|
||||||
|
component: ChildComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
|
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
||||||
|
const { location } = this.props;
|
||||||
|
const renderComponent: RenderComponent = (props) => {
|
||||||
|
if (authenticationContext.me) {
|
||||||
|
return (
|
||||||
|
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContext}>
|
||||||
|
<Component {...props} />
|
||||||
|
</AuthenticatedContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Authentication.storeLoginRedirect(location);
|
||||||
|
enqueueSnackbar("Please log in to continue.", { variant: 'info' });
|
||||||
|
return (
|
||||||
|
<Redirect to='/' />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Route {...rest} render={renderComponent} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
@ -1,11 +1,13 @@
|
|||||||
|
import * as H from 'history';
|
||||||
|
|
||||||
import history from '../history';
|
import history from '../history';
|
||||||
import { PROJECT_PATH } from '../constants/Env';
|
import { PROJECT_PATH } from '../api';
|
||||||
|
|
||||||
export const ACCESS_TOKEN = 'access_token';
|
export const ACCESS_TOKEN = 'access_token';
|
||||||
export const LOGIN_PATHNAME = 'loginPathname';
|
export const LOGIN_PATHNAME = 'loginPathname';
|
||||||
export const LOGIN_SEARCH = 'loginSearch';
|
export const LOGIN_SEARCH = 'loginSearch';
|
||||||
|
|
||||||
export function storeLoginRedirect(location) {
|
export function storeLoginRedirect(location?: H.Location) {
|
||||||
if (location) {
|
if (location) {
|
||||||
localStorage.setItem(LOGIN_PATHNAME, location.pathname);
|
localStorage.setItem(LOGIN_PATHNAME, location.pathname);
|
||||||
localStorage.setItem(LOGIN_SEARCH, location.search);
|
localStorage.setItem(LOGIN_SEARCH, location.search);
|
||||||
@ -17,7 +19,7 @@ export function clearLoginRedirect() {
|
|||||||
localStorage.removeItem(LOGIN_SEARCH);
|
localStorage.removeItem(LOGIN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect() {
|
export function fetchLoginRedirect(): H.LocationDescriptorObject {
|
||||||
const loginPathname = localStorage.getItem(LOGIN_PATHNAME);
|
const loginPathname = localStorage.getItem(LOGIN_PATHNAME);
|
||||||
const loginSearch = localStorage.getItem(LOGIN_SEARCH);
|
const loginSearch = localStorage.getItem(LOGIN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
@ -30,13 +32,15 @@ export function fetchLoginRedirect() {
|
|||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene with one with provides the access token if present.
|
* Wraps the normal fetch routene with one with provides the access token if present.
|
||||||
*/
|
*/
|
||||||
export function authorizedFetch(url, params) {
|
export function authorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||||
const accessToken = localStorage.getItem(ACCESS_TOKEN);
|
const accessToken = localStorage.getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
params.credentials = 'include';
|
params.credentials = 'include';
|
||||||
params.headers = params.headers || {};
|
params.headers = {
|
||||||
params.headers.Authorization = 'Bearer ' + accessToken;
|
...params.headers,
|
||||||
|
"Authorization": 'Bearer ' + accessToken
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return fetch(url, params);
|
return fetch(url, params);
|
||||||
}
|
}
|
||||||
@ -44,11 +48,11 @@ export function authorizedFetch(url, params) {
|
|||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene which redirects on 401 response.
|
* Wraps the normal fetch routene which redirects on 401 response.
|
||||||
*/
|
*/
|
||||||
export function redirectingAuthorizedFetch(url, params) {
|
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise<Response>((resolve, reject) => {
|
||||||
authorizedFetch(url, params).then(response => {
|
authorizedFetch(url, params).then(response => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
history.push("/unauthorized");
|
history.push("/unauthorized");
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
59
interface/src/authentication/AuthenticationContext.tsx
Normal file
59
interface/src/authentication/AuthenticationContext.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface Me {
|
||||||
|
username: string;
|
||||||
|
admin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationContext {
|
||||||
|
refresh: () => void;
|
||||||
|
signIn: (accessToken: string) => void;
|
||||||
|
signOut: () => void;
|
||||||
|
me?: Me;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticationContextDefaultValue = {} as AuthenticationContext
|
||||||
|
export const AuthenticationContext = React.createContext(
|
||||||
|
AuthenticationContextDefaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface AuthenticationContextProps {
|
||||||
|
authenticationContext: AuthenticationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
|
||||||
|
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AuthenticationContext.Consumer>
|
||||||
|
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
|
||||||
|
</AuthenticationContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticatedContext extends AuthenticationContext {
|
||||||
|
me: Me;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthenticatedContextDefaultValue = {} as AuthenticatedContext
|
||||||
|
export const AuthenticatedContext = React.createContext(
|
||||||
|
AuthenticatedContextDefaultValue
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface AuthenticatedContextProps {
|
||||||
|
authenticatedContext: AuthenticatedContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
|
||||||
|
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AuthenticatedContext.Consumer>
|
||||||
|
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
|
||||||
|
</AuthenticatedContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,15 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import history from '../history'
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import { withSnackbar } from 'notistack';
|
|
||||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { ACCESS_TOKEN, authorizedFetch } from './Authentication';
|
|
||||||
import { AuthenticationContext } from './Context';
|
|
||||||
import jwtDecode from 'jwt-decode';
|
import jwtDecode from 'jwt-decode';
|
||||||
|
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { withStyles, Theme, createStyles, WithStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
const styles = theme => ({
|
import history from '../history'
|
||||||
|
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||||
|
import { ACCESS_TOKEN, authorizedFetch } from './Authentication';
|
||||||
|
import { AuthenticationContext, Me } from './AuthenticationContext';
|
||||||
|
|
||||||
|
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken);
|
||||||
|
|
||||||
|
const styles = (theme: Theme) => createStyles({
|
||||||
loadingPanel: {
|
loadingPanel: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -23,17 +27,22 @@ const styles = theme => ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class AuthenticationWrapper extends React.Component {
|
interface AuthenticationWrapperState {
|
||||||
|
context: AuthenticationContext;
|
||||||
|
initialized: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props) {
|
type AuthenticationWrapperProps = WithSnackbarProps & WithStyles<typeof styles>;
|
||||||
|
|
||||||
|
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
||||||
|
|
||||||
|
constructor(props: AuthenticationWrapperProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
context: {
|
context: {
|
||||||
refresh: this.refresh,
|
refresh: this.refresh,
|
||||||
signIn: this.signIn,
|
signIn: this.signIn,
|
||||||
signOut: this.signOut,
|
signOut: this.signOut,
|
||||||
isAuthenticated: this.isAuthenticated,
|
|
||||||
isAdmin: this.isAdmin
|
|
||||||
},
|
},
|
||||||
initialized: false
|
initialized: false
|
||||||
};
|
};
|
||||||
@ -72,33 +81,31 @@ class AuthenticationWrapper extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refresh = () => {
|
refresh = () => {
|
||||||
var accessToken = localStorage.getItem(ACCESS_TOKEN);
|
const accessToken = localStorage.getItem(ACCESS_TOKEN)
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
const user = response.status === 200 ? jwtDecode(accessToken) : undefined;
|
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, user } });
|
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, user: undefined } });
|
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||||
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, user: undefined } });
|
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signIn = (accessToken) => {
|
signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(ACCESS_TOKEN, accessToken);
|
localStorage.setItem(ACCESS_TOKEN, accessToken);
|
||||||
const user = jwtDecode(accessToken);
|
const me: Me = decodeMeJWT(accessToken);
|
||||||
this.setState({ context: { ...this.state.context, user } });
|
this.setState({ context: { ...this.state.context, me } });
|
||||||
this.props.enqueueSnackbar(`Logged in as ${user.username}`, {
|
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
||||||
variant: 'success',
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, user: undefined } });
|
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
||||||
throw new Error("Failed to parse JWT " + err.message);
|
throw new Error("Failed to parse JWT " + err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,24 +115,13 @@ class AuthenticationWrapper extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
context: {
|
context: {
|
||||||
...this.state.context,
|
...this.state.context,
|
||||||
user: undefined
|
me: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.props.enqueueSnackbar("You have signed out.", {
|
this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
|
||||||
variant: 'success',
|
|
||||||
});
|
|
||||||
history.push('/');
|
history.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated = () => {
|
|
||||||
return this.state.context.user;
|
|
||||||
}
|
|
||||||
|
|
||||||
isAdmin = () => {
|
|
||||||
const { context } = this.state;
|
|
||||||
return context.user && context.user.admin;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(AuthenticationWrapper))
|
export default withStyles(styles)(withSnackbar(AuthenticationWrapper))
|
@ -1,15 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export const AuthenticationContext = React.createContext(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
export function withAuthenticationContext(Component) {
|
|
||||||
return function AuthenticationContextComponent(props) {
|
|
||||||
return (
|
|
||||||
<AuthenticationContext.Consumer>
|
|
||||||
{authenticationContext => <Component {...props} authenticationContext={authenticationContext} />}
|
|
||||||
</AuthenticationContext.Consumer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import {
|
|
||||||
Redirect, Route
|
|
||||||
} from "react-router-dom";
|
|
||||||
|
|
||||||
import { withAuthenticationContext } from './Context.js';
|
|
||||||
import * as Authentication from './Authentication';
|
|
||||||
|
|
||||||
class UnauthenticatedRoute extends React.Component {
|
|
||||||
render() {
|
|
||||||
const { authenticationContext, component:Component, ...rest } = this.props;
|
|
||||||
const renderComponent = (props) => {
|
|
||||||
if (authenticationContext.isAuthenticated()) {
|
|
||||||
return (<Redirect to={Authentication.fetchLoginRedirect()} />);
|
|
||||||
}
|
|
||||||
return (<Component {...props} />);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Route {...rest} render={renderComponent} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withAuthenticationContext(UnauthenticatedRoute);
|
|
28
interface/src/authentication/UnauthenticatedRoute.tsx
Normal file
28
interface/src/authentication/UnauthenticatedRoute.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
|
||||||
|
|
||||||
|
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
|
||||||
|
import * as Authentication from './Authentication';
|
||||||
|
|
||||||
|
interface UnauthenticatedRouteProps extends RouteProps {
|
||||||
|
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
|
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps & AuthenticationContextProps> {
|
||||||
|
public render() {
|
||||||
|
const { authenticationContext, component:Component, ...rest } = this.props;
|
||||||
|
const renderComponent: RenderComponent = (props) => {
|
||||||
|
if (authenticationContext.me) {
|
||||||
|
return (<Redirect to={Authentication.fetchLoginRedirect()} />);
|
||||||
|
}
|
||||||
|
return (<Component {...props} />);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Route {...rest} render={renderComponent} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticationContext(UnauthenticatedRoute);
|
6
interface/src/authentication/index.ts
Normal file
6
interface/src/authentication/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { default as AuthenticatedRoute } from './AuthenticatedRoute';
|
||||||
|
export { default as AuthenticationWrapper } from './AuthenticationWrapper';
|
||||||
|
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute';
|
||||||
|
|
||||||
|
export * from './Authentication';
|
||||||
|
export * from './AuthenticationContext';
|
10
interface/src/components/BlockFormControlLabel.tsx
Normal file
10
interface/src/components/BlockFormControlLabel.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
|
||||||
|
|
||||||
|
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||||
|
<div>
|
||||||
|
<FormControlLabel {...props} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default BlockFormControlLabel;
|
7
interface/src/components/FormActions.tsx
Normal file
7
interface/src/components/FormActions.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { styled, Box } from "@material-ui/core";
|
||||||
|
|
||||||
|
const FormActions = styled(Box)(({ theme }) => ({
|
||||||
|
marginTop: theme.spacing(1)
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default FormActions;
|
13
interface/src/components/FormButton.tsx
Normal file
13
interface/src/components/FormButton.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Button, styled } from "@material-ui/core";
|
||||||
|
|
||||||
|
const FormButton = styled(Button)(({ theme }) => ({
|
||||||
|
margin: theme.spacing(0, 1),
|
||||||
|
'&:last-child': {
|
||||||
|
marginRight: 0,
|
||||||
|
},
|
||||||
|
'&:first-child': {
|
||||||
|
marginLeft: 0,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default FormButton;
|
23
interface/src/components/HighlightAvatar.tsx
Normal file
23
interface/src/components/HighlightAvatar.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Avatar, makeStyles } from "@material-ui/core";
|
||||||
|
import React, { FC } from "react";
|
||||||
|
|
||||||
|
interface HighlightAvatarProps {
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
root: (props: HighlightAvatarProps) => ({
|
||||||
|
backgroundColor: props.color
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
||||||
|
const classes = useStyles(props);
|
||||||
|
return (
|
||||||
|
<Avatar className={classes.root}>
|
||||||
|
{props.children}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HighlightAvatar;
|
@ -1,58 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
|
||||||
loadingSettings: {
|
|
||||||
margin: theme.spacing(0.5),
|
|
||||||
},
|
|
||||||
loadingSettingsDetails: {
|
|
||||||
margin: theme.spacing(4),
|
|
||||||
textAlign: "center"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function LoadingNotification(props) {
|
|
||||||
const classes = useStyles();
|
|
||||||
const { fetched, errorMessage, onReset, render } = props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
fetched ?
|
|
||||||
errorMessage ?
|
|
||||||
<div className={classes.loadingSettings}>
|
|
||||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
|
||||||
{errorMessage}
|
|
||||||
</Typography>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
render()
|
|
||||||
:
|
|
||||||
<div className={classes.loadingSettings}>
|
|
||||||
<LinearProgress className={classes.loadingSettingsDetails} />
|
|
||||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
|
||||||
Loading...
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadingNotification.propTypes = {
|
|
||||||
fetched: PropTypes.bool.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
render: PropTypes.func.isRequired
|
|
||||||
};
|
|
@ -1,42 +1,28 @@
|
|||||||
import React from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
|
||||||
|
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, IconButton } from '@material-ui/core';
|
||||||
|
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
|
||||||
|
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
|
||||||
|
import { Card, CardContent, CardActions } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Drawer from '@material-ui/core/Drawer';
|
|
||||||
import AppBar from '@material-ui/core/AppBar';
|
|
||||||
import Toolbar from '@material-ui/core/Toolbar';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
|
||||||
import Hidden from '@material-ui/core/Hidden';
|
|
||||||
import Divider from '@material-ui/core/Divider';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import List from '@material-ui/core/List';
|
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
|
||||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
|
||||||
import Popper from '@material-ui/core/Popper';
|
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
import WifiIcon from '@material-ui/icons/Wifi';
|
||||||
import SettingsIcon from '@material-ui/icons/Settings';
|
import SettingsIcon from '@material-ui/icons/Settings';
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
||||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
import LockIcon from '@material-ui/icons/Lock';
|
import LockIcon from '@material-ui/icons/Lock';
|
||||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
import Card from '@material-ui/core/Card';
|
|
||||||
import CardContent from '@material-ui/core/CardContent';
|
|
||||||
import CardActions from '@material-ui/core/CardActions';
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
|
||||||
|
|
||||||
import ProjectMenu from '../project/ProjectMenu';
|
import ProjectMenu from '../project/ProjectMenu';
|
||||||
import { PROJECT_NAME } from '../constants/Env';
|
import { PROJECT_NAME } from '../api';
|
||||||
import { withAuthenticationContext } from '../authentication/Context.js';
|
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||||
|
|
||||||
const drawerWidth = 290;
|
const drawerWidth = 290;
|
||||||
|
|
||||||
const styles = theme => ({
|
const styles = (theme: Theme) => createStyles({
|
||||||
root: {
|
root: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
@ -77,26 +63,38 @@ const styles = theme => ({
|
|||||||
"& > * + *": {
|
"& > * + *": {
|
||||||
marginLeft: theme.spacing(2),
|
marginLeft: theme.spacing(2),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
class MenuAppBar extends React.Component {
|
interface MenuAppBarState {
|
||||||
state = {
|
mobileOpen: boolean;
|
||||||
mobileOpen: false,
|
authMenuOpen: boolean;
|
||||||
authMenuOpen: false
|
}
|
||||||
};
|
|
||||||
|
|
||||||
anchorRef = React.createRef();
|
interface MenuAppBarProps extends AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
|
||||||
|
sectionTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
|
||||||
|
|
||||||
|
constructor(props: MenuAppBarProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
mobileOpen: false,
|
||||||
|
authMenuOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
anchorRef: RefObject<HTMLButtonElement> = React.createRef();
|
||||||
|
|
||||||
handleToggle = () => {
|
handleToggle = () => {
|
||||||
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClose = (event) => {
|
handleClose = (event: React.MouseEvent<Document>) => {
|
||||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.target)) {
|
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ authMenuOpen: false });
|
this.setState({ authMenuOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,13 +103,13 @@ class MenuAppBar extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, theme, children, sectionTitle, authenticationContext } = this.props;
|
const { classes, theme, children, sectionTitle, authenticatedContext } = this.props;
|
||||||
const { mobileOpen, authMenuOpen } = this.state;
|
const { mobileOpen, authMenuOpen } = this.state;
|
||||||
const path = this.props.match.url;
|
const path = this.props.match.url;
|
||||||
const drawer = (
|
const drawer = (
|
||||||
<div>
|
<div>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Typography variant="h6" color="primary">
|
<Typography variant="h6" color="textPrimary">
|
||||||
{PROJECT_NAME}
|
{PROJECT_NAME}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider absolute />
|
<Divider absolute />
|
||||||
@ -138,7 +136,7 @@ class MenuAppBar extends React.Component {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Network Time" />
|
<ListItemText primary="Network Time" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticationContext.isAdmin()}>
|
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
@ -156,7 +154,7 @@ class MenuAppBar extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<AppBar position="fixed" className={classes.appBar}>
|
<AppBar position="fixed" className={classes.appBar} elevation={0}>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@ -191,13 +189,13 @@ class MenuAppBar extends React.Component {
|
|||||||
<AccountCircleIcon />
|
<AccountCircleIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={"Signed in as: " + authenticationContext.user.username} secondary={authenticationContext.isAdmin() ? "Admin User" : undefined} />
|
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<Divider />
|
<Divider />
|
||||||
<CardActions className={classes.authMenuActions}>
|
<CardActions className={classes.authMenuActions}>
|
||||||
<Button variant="contained" color="primary" onClick={authenticationContext.signOut}>Sign Out</Button>
|
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
@ -243,14 +241,10 @@ class MenuAppBar extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuAppBar.propTypes = {
|
export default withRouter(
|
||||||
classes: PropTypes.object.isRequired,
|
withTheme(
|
||||||
theme: PropTypes.object.isRequired,
|
withAuthenticatedContext(
|
||||||
sectionTitle: PropTypes.string.isRequired,
|
withStyles(styles)(MenuAppBar)
|
||||||
};
|
)
|
||||||
|
|
||||||
export default withAuthenticationContext(
|
|
||||||
withRouter(
|
|
||||||
withStyles(styles, { withTheme: true })(MenuAppBar)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
@ -1,21 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TextValidator } from 'react-material-ui-form-validator';
|
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import { InputAdornment } from '@material-ui/core';
|
|
||||||
import Visibility from '@material-ui/icons/Visibility';
|
|
||||||
import VisibilityOff from '@material-ui/icons/VisibilityOff';
|
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
|
||||||
|
|
||||||
const styles = theme => (
|
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||||
{
|
import { InputAdornment, IconButton } from '@material-ui/core';
|
||||||
input: {
|
import {Visibility,VisibilityOff } from '@material-ui/icons';
|
||||||
"&::-ms-reveal": {
|
|
||||||
display: "none"
|
const styles = createStyles({
|
||||||
}
|
input: {
|
||||||
|
"&::-ms-reveal": {
|
||||||
|
display: "none"
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
class PasswordValidator extends React.Component {
|
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
||||||
|
|
||||||
|
interface PasswordValidatorState {
|
||||||
|
showPassword: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
showPassword: false
|
showPassword: false
|
@ -1,125 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { withSnackbar } from 'notistack';
|
|
||||||
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* It is unlikely this application will grow complex enough to require redux.
|
|
||||||
*
|
|
||||||
* This HOC acts as an interface to a REST service, providing data and change
|
|
||||||
* event callbacks to the wrapped components along with a function to persist the
|
|
||||||
* changes.
|
|
||||||
*/
|
|
||||||
export const restComponent = (endpointUrl, FormComponent) => {
|
|
||||||
|
|
||||||
return withSnackbar(
|
|
||||||
class extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
data: null,
|
|
||||||
fetched: false,
|
|
||||||
errorMessage: null
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState = this.setState.bind(this);
|
|
||||||
this.loadData = this.loadData.bind(this);
|
|
||||||
this.saveData = this.saveData.bind(this);
|
|
||||||
this.setData = this.setData.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(data) {
|
|
||||||
this.setState({
|
|
||||||
data: data,
|
|
||||||
fetched: true,
|
|
||||||
errorMessage: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData() {
|
|
||||||
this.setState({
|
|
||||||
data: null,
|
|
||||||
fetched: false,
|
|
||||||
errorMessage: null
|
|
||||||
});
|
|
||||||
redirectingAuthorizedFetch(endpointUrl)
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Invalid status code: " + response.status);
|
|
||||||
})
|
|
||||||
.then(json => { this.setState({ data: json, fetched: true }) })
|
|
||||||
.catch(error => {
|
|
||||||
const errorMessage = error.message || "Unknown error";
|
|
||||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
this.setState({ data: null, fetched: true, errorMessage });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
saveData(e) {
|
|
||||||
this.setState({ fetched: false });
|
|
||||||
redirectingAuthorizedFetch(endpointUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(this.state.data),
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
throw Error("Invalid status code: " + response.status);
|
|
||||||
})
|
|
||||||
.then(json => {
|
|
||||||
this.props.enqueueSnackbar("Changes successfully applied.", {
|
|
||||||
variant: 'success',
|
|
||||||
});
|
|
||||||
this.setState({ data: json, fetched: true });
|
|
||||||
}).catch(error => {
|
|
||||||
const errorMessage = error.message || "Unknown error";
|
|
||||||
this.props.enqueueSnackbar("Problem saving: " + errorMessage, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
this.setState({ data: null, fetched: true, errorMessage });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleValueChange = name => (event) => {
|
|
||||||
const { data } = this.state;
|
|
||||||
data[name] = event.target.value;
|
|
||||||
this.setState({ data });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSliderChange = name => (event, newValue) => {
|
|
||||||
const { data } = this.state;
|
|
||||||
data[name] = newValue;
|
|
||||||
this.setState({ data });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleCheckboxChange = name => event => {
|
|
||||||
const { data } = this.state;
|
|
||||||
data[name] = event.target.checked;
|
|
||||||
this.setState({ data });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <FormComponent
|
|
||||||
handleValueChange={this.handleValueChange}
|
|
||||||
handleCheckboxChange={this.handleCheckboxChange}
|
|
||||||
handleSliderChange={this.handleSliderChange}
|
|
||||||
setData={this.setData}
|
|
||||||
saveData={this.saveData}
|
|
||||||
loadData={this.loadData}
|
|
||||||
{...this.state}
|
|
||||||
{...this.props}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
116
interface/src/components/RestController.tsx
Normal file
116
interface/src/components/RestController.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
|
|
||||||
|
export interface RestControllerProps<D> extends WithSnackbarProps {
|
||||||
|
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
handleCheckboxChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => void;
|
||||||
|
handleSliderChange: (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => void;
|
||||||
|
|
||||||
|
setData: (data: D) => void;
|
||||||
|
saveData: () => void;
|
||||||
|
loadData: () => void;
|
||||||
|
|
||||||
|
data?: D;
|
||||||
|
loading: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RestControllerState<D> {
|
||||||
|
data?: D;
|
||||||
|
loading: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
|
||||||
|
return withSnackbar(
|
||||||
|
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
|
||||||
|
|
||||||
|
state: RestControllerState<D> = {
|
||||||
|
data: undefined,
|
||||||
|
loading: false,
|
||||||
|
errorMessage: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
setData = (data: D) => {
|
||||||
|
this.setState({
|
||||||
|
data,
|
||||||
|
loading: false,
|
||||||
|
errorMessage: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData = () => {
|
||||||
|
this.setState({
|
||||||
|
data: undefined,
|
||||||
|
loading: true,
|
||||||
|
errorMessage: undefined
|
||||||
|
});
|
||||||
|
redirectingAuthorizedFetch(endpointUrl).then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error("Invalid status code: " + response.status);
|
||||||
|
}).then(json => {
|
||||||
|
this.setState({ data: json, loading: false })
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMessage = error.message || "Unknown error";
|
||||||
|
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
|
||||||
|
this.setState({ data: undefined, loading: false, errorMessage });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData = () => {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
redirectingAuthorizedFetch(endpointUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(this.state.data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
throw Error("Invalid status code: " + response.status);
|
||||||
|
}).then(json => {
|
||||||
|
this.props.enqueueSnackbar("Changes successfully applied.", { variant: 'success' });
|
||||||
|
this.setState({ data: json, loading: false });
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMessage = error.message || "Unknown error";
|
||||||
|
this.props.enqueueSnackbar("Problem saving: " + errorMessage, { variant: 'error' });
|
||||||
|
this.setState({ data: undefined, loading: false, errorMessage });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const data = { ...this.state.data!, [name]: event.target.value };
|
||||||
|
this.setState({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCheckboxChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const data = { ...this.state.data!, [name]: event.target.checked };
|
||||||
|
this.setState({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSliderChange = (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => {
|
||||||
|
const data = { ...this.state.data!, [name]: value };
|
||||||
|
this.setState({ data });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <RestController
|
||||||
|
handleValueChange={this.handleValueChange}
|
||||||
|
handleCheckboxChange={this.handleCheckboxChange}
|
||||||
|
handleSliderChange={this.handleSliderChange}
|
||||||
|
setData={this.setData}
|
||||||
|
saveData={this.saveData}
|
||||||
|
loadData={this.loadData}
|
||||||
|
{...this.state}
|
||||||
|
{...this.props as P}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
55
interface/src/components/RestFormLoader.tsx
Normal file
55
interface/src/components/RestFormLoader.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
|
import { Button, LinearProgress, Typography } from '@material-ui/core';
|
||||||
|
import { RestControllerProps } from './RestController';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
loadingSettings: {
|
||||||
|
margin: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
loadingSettingsDetails: {
|
||||||
|
margin: theme.spacing(4),
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
|
||||||
|
|
||||||
|
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
|
||||||
|
render: (props: RestFormProps<D>) => JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
|
||||||
|
const { loading, errorMessage, loadData, render, data, ...rest } = props;
|
||||||
|
const classes = useStyles();
|
||||||
|
if (loading || !data) {
|
||||||
|
return (
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<LinearProgress className={classes.loadingSettingsDetails} />
|
||||||
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (errorMessage) {
|
||||||
|
return (
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
|
{errorMessage}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return render({ ...rest, loadData, data });
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Paper from '@material-ui/core/Paper';
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
content: {
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
margin: theme.spacing(3),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function SectionContent(props) {
|
|
||||||
const { children, classes, title, titleGutter } = props;
|
|
||||||
return (
|
|
||||||
<Paper className={classes.content}>
|
|
||||||
<Typography variant="h6" gutterBottom={titleGutter}>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
{children}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SectionContent.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
children: PropTypes.oneOfType([
|
|
||||||
PropTypes.arrayOf(PropTypes.node),
|
|
||||||
PropTypes.node
|
|
||||||
]).isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
titleGutter: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withStyles(styles)(SectionContent);
|
|
33
interface/src/components/SectionContent.tsx
Normal file
33
interface/src/components/SectionContent.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Typography, Paper } from '@material-ui/core';
|
||||||
|
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
content: {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
margin: theme.spacing(3),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SectionContentProps {
|
||||||
|
title: string;
|
||||||
|
titleGutter?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SectionContent: React.FC<SectionContentProps> = (props) => {
|
||||||
|
const { children, title, titleGutter } = props;
|
||||||
|
const classes = useStyles();
|
||||||
|
return (
|
||||||
|
<Paper className={classes.content}>
|
||||||
|
<Typography variant="h6" gutterBottom={titleGutter}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionContent;
|
11
interface/src/components/index.ts
Normal file
11
interface/src/components/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export { default as BlockFormControlLabel } from './BlockFormControlLabel';
|
||||||
|
export { default as FormActions } from './FormActions';
|
||||||
|
export { default as FormButton } from './FormButton';
|
||||||
|
export { default as HighlightAvatar } from './HighlightAvatar';
|
||||||
|
export { default as MenuAppBar } from './MenuAppBar';
|
||||||
|
export { default as PasswordValidator } from './PasswordValidator';
|
||||||
|
export { default as RestFormLoader } from './RestFormLoader';
|
||||||
|
export { default as SectionContent } from './SectionContent';
|
||||||
|
|
||||||
|
export * from './RestFormLoader';
|
||||||
|
export * from './RestController';
|
@ -1,3 +0,0 @@
|
|||||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME;
|
|
||||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH;
|
|
||||||
export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT;
|
|
@ -1,4 +0,0 @@
|
|||||||
export const IDLE = "idle";
|
|
||||||
export const SUCCESS = "success";
|
|
||||||
export const ERROR = "error";
|
|
||||||
export const WARN = "warn";
|
|
@ -1,28 +0,0 @@
|
|||||||
import * as Highlight from '../constants/Highlight';
|
|
||||||
|
|
||||||
export const NTP_INACTIVE = 0;
|
|
||||||
export const NTP_ACTIVE = 1;
|
|
||||||
|
|
||||||
export const isNtpActive = ntpStatus => ntpStatus && ntpStatus.status === NTP_ACTIVE;
|
|
||||||
|
|
||||||
export const ntpStatusHighlight = ntpStatus => {
|
|
||||||
switch (ntpStatus.status) {
|
|
||||||
case NTP_INACTIVE:
|
|
||||||
return Highlight.IDLE;
|
|
||||||
case NTP_ACTIVE:
|
|
||||||
return Highlight.SUCCESS;
|
|
||||||
default:
|
|
||||||
return Highlight.ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ntpStatus = ntpStatus => {
|
|
||||||
switch (ntpStatus.status) {
|
|
||||||
case NTP_INACTIVE:
|
|
||||||
return "Inactive";
|
|
||||||
case NTP_ACTIVE:
|
|
||||||
return "Active";
|
|
||||||
default:
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
export const formatIsoDateTime = isoDateString => moment.parseZone(isoDateString).format('ll @ HH:mm:ss');
|
|
@ -1,5 +0,0 @@
|
|||||||
export const WIFI_AP_MODE_ALWAYS = 0;
|
|
||||||
export const WIFI_AP_MODE_DISCONNECTED = 1;
|
|
||||||
export const WIFI_AP_NEVER = 2;
|
|
||||||
|
|
||||||
export const isAPEnabled = apMode => apMode === WIFI_AP_MODE_ALWAYS || apMode === WIFI_AP_MODE_DISCONNECTED;
|
|
@ -1,38 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { AP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import APSettingsForm from '../forms/APSettingsForm';
|
|
||||||
|
|
||||||
class APSettings extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fetched, errorMessage, data, saveData, loadData, handleValueChange } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="AP Settings">
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={() =>
|
|
||||||
<APSettingsForm
|
|
||||||
apSettings={data}
|
|
||||||
onSubmit={saveData}
|
|
||||||
onReset={loadData}
|
|
||||||
handleValueChange={handleValueChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(AP_SETTINGS_ENDPOINT, APSettings);
|
|
@ -1,121 +0,0 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import List from '@material-ui/core/List';
|
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
|
||||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
|
||||||
import Divider from '@material-ui/core/Divider';
|
|
||||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
|
||||||
import ComputerIcon from '@material-ui/icons/Computer';
|
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
|
||||||
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SectionContent from '../components/SectionContent'
|
|
||||||
|
|
||||||
import * as Highlight from '../constants/Highlight';
|
|
||||||
import { AP_STATUS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
["apStatus_" + Highlight.SUCCESS]: {
|
|
||||||
backgroundColor: theme.palette.highlight_success
|
|
||||||
},
|
|
||||||
["apStatus_" + Highlight.IDLE]: {
|
|
||||||
backgroundColor: theme.palette.highlight_idle
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class APStatus extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
apStatusHighlight(data) {
|
|
||||||
return data.active ? Highlight.SUCCESS : Highlight.IDLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
apStatus(data) {
|
|
||||||
return data.active ? "Active" : "Inactive";
|
|
||||||
}
|
|
||||||
|
|
||||||
createListItems(data, classes) {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar className={classes["apStatus_" + this.apStatusHighlight(data)]}>
|
|
||||||
<SettingsInputAntennaIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="Status" secondary={this.apStatus(data)} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>IP</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="IP Address" secondary={data.ip_address} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<DeviceHubIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="MAC Address" secondary={data.mac_address} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<ComputerIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="AP Clients" secondary={data.station_num} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAPStatus(data, classes) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<List>
|
|
||||||
{this.createListItems(data, classes)}
|
|
||||||
</List>
|
|
||||||
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fetched, errorMessage, data, loadData, classes } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="AP Status">
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={
|
|
||||||
() => this.renderAPStatus(data, classes)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(AP_STATUS_ENDPOINT, withStyles(styles)(APStatus));
|
|
@ -1,39 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import ManageUsersForm from '../forms/ManageUsersForm';
|
|
||||||
|
|
||||||
class ManageUsers extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fetched, errorMessage, data, saveData, loadData, setData, handleValueChange } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="Manage Users" titleGutter>
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={() =>
|
|
||||||
<ManageUsersForm
|
|
||||||
userData={data}
|
|
||||||
onSubmit={saveData}
|
|
||||||
onReset={loadData}
|
|
||||||
setData={setData}
|
|
||||||
handleValueChange={handleValueChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(SECURITY_SETTINGS_ENDPOINT, ManageUsers);
|
|
@ -1,40 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import NTPSettingsForm from '../forms/NTPSettingsForm';
|
|
||||||
|
|
||||||
class NTPSettings extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fetched, errorMessage, data, saveData, setData, loadData, handleValueChange, handleCheckboxChange } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="NTP Settings">
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={() =>
|
|
||||||
<NTPSettingsForm
|
|
||||||
ntpSettings={data}
|
|
||||||
setData={setData}
|
|
||||||
onSubmit={saveData}
|
|
||||||
onReset={loadData}
|
|
||||||
handleValueChange={handleValueChange}
|
|
||||||
handleCheckboxChange={handleCheckboxChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(NTP_SETTINGS_ENDPOINT, NTPSettings);
|
|
@ -1,138 +0,0 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import List from '@material-ui/core/List';
|
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
|
||||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
|
||||||
import Divider from '@material-ui/core/Divider';
|
|
||||||
|
|
||||||
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
|
||||||
import DNSIcon from '@material-ui/icons/Dns';
|
|
||||||
import UpdateIcon from '@material-ui/icons/Update';
|
|
||||||
import AvTimerIcon from '@material-ui/icons/AvTimer';
|
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
|
||||||
|
|
||||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from '../constants/NTPStatus';
|
|
||||||
import * as Highlight from '../constants/Highlight';
|
|
||||||
import { formatIsoDateTime } from '../constants/TimeFormat';
|
|
||||||
import { NTP_STATUS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
["ntpStatus_" + Highlight.SUCCESS]: {
|
|
||||||
backgroundColor: theme.palette.highlight_success
|
|
||||||
},
|
|
||||||
["ntpStatus_" + Highlight.ERROR]: {
|
|
||||||
backgroundColor: theme.palette.highlight_error
|
|
||||||
},
|
|
||||||
["ntpStatus_" + Highlight.WARN]: {
|
|
||||||
backgroundColor: theme.palette.highlight_warn
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class NTPStatus extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
createListItems(data, classes) {
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<ListItem >
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar className={classes["ntpStatus_" + ntpStatusHighlight(data)]}>
|
|
||||||
<UpdateIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="Status" secondary={ntpStatus(data)} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
{
|
|
||||||
isNtpActive(data) && (
|
|
||||||
<Fragment>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<AccessTimeIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="Local Time" secondary={formatIsoDateTime(data.time_local)} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<AccessTimeIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="UTC Time" secondary={formatIsoDateTime(data.time_utc)} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<DNSIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="NTP Server" secondary={data.server} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<AvTimerIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="Uptime" secondary={moment.duration(data.uptime, 'seconds').humanize()} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider variant="inset" component="li" />
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNTPStatus(data, classes) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<List>
|
|
||||||
{this.createListItems(data, classes)}
|
|
||||||
</List>
|
|
||||||
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { data, fetched, errorMessage, loadData, classes } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="NTP Status">
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={
|
|
||||||
() => this.renderNTPStatus(data, classes)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(NTP_STATUS_ENDPOINT, withStyles(styles)(NTPStatus));
|
|
@ -1,39 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import OTASettingsForm from '../forms/OTASettingsForm';
|
|
||||||
|
|
||||||
class OTASettings extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { fetched, errorMessage, data, saveData, loadData, handleValueChange, handleCheckboxChange } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="OTA Settings">
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={() =>
|
|
||||||
<OTASettingsForm
|
|
||||||
otaSettings={data}
|
|
||||||
onSubmit={saveData}
|
|
||||||
onReset={loadData}
|
|
||||||
handleValueChange={handleValueChange}
|
|
||||||
handleCheckboxChange={handleCheckboxChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(OTA_SETTINGS_ENDPOINT, OTASettings);
|
|
@ -1,38 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SecuritySettingsForm from '../forms/SecuritySettingsForm';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
|
|
||||||
class SecuritySettings extends Component {
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { data, fetched, errorMessage, saveData, loadData, handleValueChange } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="Security Settings">
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={() =>
|
|
||||||
<SecuritySettingsForm
|
|
||||||
securitySettings={data}
|
|
||||||
onSubmit={saveData}
|
|
||||||
onReset={loadData}
|
|
||||||
handleValueChange={handleValueChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(SECURITY_SETTINGS_ENDPOINT, SecuritySettings);
|
|
@ -1,125 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import WiFiNetworkSelector from '../forms/WiFiNetworkSelector';
|
|
||||||
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
|
|
||||||
|
|
||||||
const NUM_POLLS = 10
|
|
||||||
const POLLING_FREQUENCY = 500
|
|
||||||
const RETRY_EXCEPTION_TYPE = "retry"
|
|
||||||
|
|
||||||
class WiFiNetworkScanner extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.pollCount = 0;
|
|
||||||
this.state = {
|
|
||||||
scanningForNetworks: true,
|
|
||||||
errorMessage: null,
|
|
||||||
networkList: null
|
|
||||||
};
|
|
||||||
this.pollNetworkList = this.pollNetworkList.bind(this);
|
|
||||||
this.requestNetworkScan = this.requestNetworkScan.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.scanNetworks();
|
|
||||||
}
|
|
||||||
|
|
||||||
requestNetworkScan() {
|
|
||||||
const { scanningForNetworks } = this.state;
|
|
||||||
if (!scanningForNetworks) {
|
|
||||||
this.scanNetworks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scanNetworks() {
|
|
||||||
this.pollCount = 0;
|
|
||||||
this.setState({ scanningForNetworks: true, networkList: null, errorMessage: null });
|
|
||||||
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
|
||||||
if (response.status === 202) {
|
|
||||||
this.schedulePollTimeout();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw Error("Scanning for networks returned unexpected response code: " + response.status);
|
|
||||||
}).catch(error => {
|
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
this.setState({ scanningForNetworks: false, networkList: null, errorMessage: error.message });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
schedulePollTimeout() {
|
|
||||||
setTimeout(this.pollNetworkList, POLLING_FREQUENCY);
|
|
||||||
}
|
|
||||||
|
|
||||||
retryError() {
|
|
||||||
return {
|
|
||||||
name: RETRY_EXCEPTION_TYPE,
|
|
||||||
message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
compareNetworks(network1, network2) {
|
|
||||||
if (network1.rssi < network2.rssi)
|
|
||||||
return 1;
|
|
||||||
if (network1.rssi > network2.rssi)
|
|
||||||
return -1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pollNetworkList() {
|
|
||||||
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
if (response.status === 202) {
|
|
||||||
if (++this.pollCount < NUM_POLLS) {
|
|
||||||
this.schedulePollTimeout();
|
|
||||||
throw this.retryError();
|
|
||||||
} else {
|
|
||||||
throw Error("Device did not return network list in timely manner.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw Error("Device returned unexpected response code: " + response.status);
|
|
||||||
})
|
|
||||||
.then(json => {
|
|
||||||
json.networks.sort(this.compareNetworks)
|
|
||||||
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: null })
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
this.setState({ scanningForNetworks: false, networkList: null, errorMessage: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { scanningForNetworks, networkList, errorMessage } = this.state;
|
|
||||||
return (
|
|
||||||
<SectionContent title="Network Scanner">
|
|
||||||
<WiFiNetworkSelector scanningForNetworks={scanningForNetworks}
|
|
||||||
networkList={networkList}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
requestNetworkScan={this.requestNetworkScan}
|
|
||||||
selectNetwork={this.props.selectNetwork}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiNetworkScanner.propTypes = {
|
|
||||||
selectNetwork: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withSnackbar(WiFiNetworkScanner);
|
|
@ -1,69 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { WIFI_SETTINGS_ENDPOINT } from '../constants/Endpoints';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import WiFiSettingsForm from '../forms/WiFiSettingsForm';
|
|
||||||
|
|
||||||
class WiFiSettings extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.deselectNetworkAndLoadData = this.deselectNetworkAndLoadData.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { selectedNetwork } = this.props;
|
|
||||||
if (selectedNetwork) {
|
|
||||||
var wifiSettings = {
|
|
||||||
ssid: selectedNetwork.ssid,
|
|
||||||
password: "",
|
|
||||||
hostname: "esp8266-react",
|
|
||||||
static_ip_config: false,
|
|
||||||
}
|
|
||||||
this.props.setData(wifiSettings);
|
|
||||||
} else {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectNetworkAndLoadData() {
|
|
||||||
this.props.deselectNetwork();
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { data, fetched, errorMessage, saveData, loadData, handleValueChange, handleCheckboxChange, selectedNetwork, deselectNetwork } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="WiFi Settings">
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={() =>
|
|
||||||
<WiFiSettingsForm
|
|
||||||
wifiSettings={data}
|
|
||||||
selectedNetwork={selectedNetwork}
|
|
||||||
deselectNetwork={deselectNetwork}
|
|
||||||
onSubmit={saveData}
|
|
||||||
onReset={this.deselectNetworkAndLoadData}
|
|
||||||
handleValueChange={handleValueChange}
|
|
||||||
handleCheckboxChange={handleCheckboxChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiSettings.propTypes = {
|
|
||||||
deselectNetwork: PropTypes.func,
|
|
||||||
selectedNetwork: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default restComponent(WIFI_SETTINGS_ENDPOINT, WiFiSettings);
|
|
@ -1,84 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
|
||||||
|
|
||||||
import { isAPEnabled } from '../constants/WiFiAPModes';
|
|
||||||
import PasswordValidator from '../components/PasswordValidator';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
textField: {
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
selectField: {
|
|
||||||
width: "100%",
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(0.5)
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class APSettingsForm extends React.Component {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, apSettings, handleValueChange, onSubmit, onReset } = this.props;
|
|
||||||
return (
|
|
||||||
<ValidatorForm onSubmit={onSubmit} ref="APSettingsForm">
|
|
||||||
<SelectValidator name="provision_mode" label="Provide Access Point..." value={apSettings.provision_mode} className={classes.selectField}
|
|
||||||
onChange={handleValueChange('provision_mode')}>
|
|
||||||
<MenuItem value={0}>Always</MenuItem>
|
|
||||||
<MenuItem value={1}>When WiFi Disconnected</MenuItem>
|
|
||||||
<MenuItem value={2}>Never</MenuItem>
|
|
||||||
</SelectValidator>
|
|
||||||
{
|
|
||||||
isAPEnabled(apSettings.provision_mode) &&
|
|
||||||
<Fragment>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'matchRegexp:^.{1,32}$']}
|
|
||||||
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
|
|
||||||
name="ssid"
|
|
||||||
label="Access Point SSID"
|
|
||||||
className={classes.textField}
|
|
||||||
value={apSettings.ssid}
|
|
||||||
onChange={handleValueChange('ssid')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<PasswordValidator
|
|
||||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
|
||||||
errorMessages={['Access Point Password is required', 'Access Point Password must be 64 characters or less']}
|
|
||||||
name="password"
|
|
||||||
label="Access Point Password"
|
|
||||||
className={classes.textField}
|
|
||||||
value={apSettings.password}
|
|
||||||
onChange={handleValueChange('password')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
APSettingsForm.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
apSettings: PropTypes.object,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
handleValueChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withStyles(styles)(APSettingsForm);
|
|
@ -1,105 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
|
||||||
import Switch from '@material-ui/core/Switch';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
|
||||||
|
|
||||||
import isIP from '../validators/isIP';
|
|
||||||
import isHostname from '../validators/isHostname';
|
|
||||||
import or from '../validators/or';
|
|
||||||
import { timeZoneSelectItems, selectedTimeZone, TIME_ZONES } from '../constants/TZ';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
switchControl: {
|
|
||||||
width: "100%",
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(0.5)
|
|
||||||
},
|
|
||||||
textField: {
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class NTPSettingsForm extends React.Component {
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
|
||||||
}
|
|
||||||
|
|
||||||
changeTimeZone = (event) => {
|
|
||||||
const { ntpSettings, setData } = this.props;
|
|
||||||
setData({
|
|
||||||
...ntpSettings,
|
|
||||||
tz_label: event.target.value,
|
|
||||||
tz_format: TIME_ZONES[event.target.value]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, ntpSettings, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
|
|
||||||
return (
|
|
||||||
<ValidatorForm onSubmit={onSubmit}>
|
|
||||||
<FormControlLabel className={classes.switchControl}
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={ntpSettings.enabled}
|
|
||||||
onChange={handleCheckboxChange('enabled')}
|
|
||||||
value="enabled"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Enable NTP?"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isIPOrHostname']}
|
|
||||||
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
|
|
||||||
name="server"
|
|
||||||
label="Server"
|
|
||||||
className={classes.textField}
|
|
||||||
value={ntpSettings.server}
|
|
||||||
onChange={handleValueChange('server')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<SelectValidator
|
|
||||||
native
|
|
||||||
validators={['required']}
|
|
||||||
errorMessages={['Time zone is required']}
|
|
||||||
labelId="tz_label"
|
|
||||||
label="Time zone"
|
|
||||||
value={selectedTimeZone(ntpSettings.tz_label, ntpSettings.tz_format)}
|
|
||||||
onChange={this.changeTimeZone}
|
|
||||||
className={classes.textField}
|
|
||||||
margin="normal"
|
|
||||||
>
|
|
||||||
<MenuItem disabled={true}>Time zone...</MenuItem>
|
|
||||||
{timeZoneSelectItems()}
|
|
||||||
</SelectValidator>
|
|
||||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NTPSettingsForm.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
ntpSettings: PropTypes.object,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
handleValueChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withStyles(styles)(NTPSettingsForm);
|
|
@ -1,93 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import Switch from '@material-ui/core/Switch';
|
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
|
||||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
|
||||||
|
|
||||||
import isIP from '../validators/isIP';
|
|
||||||
import isHostname from '../validators/isHostname';
|
|
||||||
import or from '../validators/or';
|
|
||||||
import PasswordValidator from '../components/PasswordValidator';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
switchControl: {
|
|
||||||
width: "100%",
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(0.5)
|
|
||||||
},
|
|
||||||
textField: {
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class OTASettingsForm extends React.Component {
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, otaSettings, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
|
|
||||||
return (
|
|
||||||
<ValidatorForm onSubmit={onSubmit}>
|
|
||||||
<FormControlLabel className={classes.switchControl}
|
|
||||||
control={
|
|
||||||
<Switch
|
|
||||||
checked={otaSettings.enabled}
|
|
||||||
onChange={handleCheckboxChange('enabled')}
|
|
||||||
value="enabled"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Enable OTA Updates?"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']}
|
|
||||||
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]}
|
|
||||||
name="port"
|
|
||||||
label="Port"
|
|
||||||
className={classes.textField}
|
|
||||||
value={otaSettings.port}
|
|
||||||
type="number"
|
|
||||||
onChange={handleValueChange('port')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<PasswordValidator
|
|
||||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
|
||||||
errorMessages={['OTA Password is required', 'OTA Point Password must be 64 characters or less']}
|
|
||||||
name="password"
|
|
||||||
label="Password"
|
|
||||||
className={classes.textField}
|
|
||||||
value={otaSettings.password}
|
|
||||||
onChange={handleValueChange('password')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OTASettingsForm.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
otaSettings: PropTypes.object,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
handleValueChange: PropTypes.func.isRequired,
|
|
||||||
handleCheckboxChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withStyles(styles)(OTASettingsForm);
|
|
@ -1,70 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
import Box from '@material-ui/core/Box';
|
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
|
||||||
|
|
||||||
import PasswordValidator from '../components/PasswordValidator';
|
|
||||||
import { withAuthenticationContext } from '../authentication/Context';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
textField: {
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class SecuritySettingsForm extends React.Component {
|
|
||||||
|
|
||||||
onSubmit = () => {
|
|
||||||
this.props.onSubmit();
|
|
||||||
this.props.authenticationContext.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, securitySettings, handleValueChange, onReset } = this.props;
|
|
||||||
return (
|
|
||||||
<ValidatorForm onSubmit={this.onSubmit} ref="SecuritySettingsForm">
|
|
||||||
<PasswordValidator
|
|
||||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
|
||||||
errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']}
|
|
||||||
name="jwt_secret"
|
|
||||||
label="JWT Secret"
|
|
||||||
className={classes.textField}
|
|
||||||
value={securitySettings.jwt_secret}
|
|
||||||
onChange={handleValueChange('jwt_secret')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<Typography component="div" variant="body1">
|
|
||||||
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
|
|
||||||
If you modify the JWT Secret, all users will be logged out.
|
|
||||||
</Box>
|
|
||||||
</Typography>
|
|
||||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SecuritySettingsForm.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
securitySettings: PropTypes.object,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
handleValueChange: PropTypes.func.isRequired,
|
|
||||||
authenticationContext: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withAuthenticationContext(withStyles(styles)(SecuritySettingsForm));
|
|
@ -1,102 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
|
|
||||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
|
||||||
import Switch from '@material-ui/core/Switch';
|
|
||||||
import FormGroup from '@material-ui/core/FormGroup';
|
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
|
||||||
import Dialog from '@material-ui/core/Dialog';
|
|
||||||
import DialogContent from '@material-ui/core/DialogContent';
|
|
||||||
import DialogActions from '@material-ui/core/DialogActions';
|
|
||||||
|
|
||||||
import PasswordValidator from '../components/PasswordValidator';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
textField: {
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
margin: theme.spacing(0.5)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class UserForm extends React.Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.formRef = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
submit = () => {
|
|
||||||
this.formRef.current.submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, user, creating, handleValueChange, handleCheckboxChange, onDoneEditing, onCancelEditing } = this.props;
|
|
||||||
return (
|
|
||||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
|
||||||
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open={true}>
|
|
||||||
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
|
|
||||||
<DialogContent dividers={true}>
|
|
||||||
<TextValidator
|
|
||||||
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []}
|
|
||||||
errorMessages={creating ? ['Username is required', "Username already exists", "Must be 1-24 characters: alpha numeric, '_' or '.'"] : []}
|
|
||||||
name="username"
|
|
||||||
label="Username"
|
|
||||||
className={classes.textField}
|
|
||||||
value={user.username}
|
|
||||||
disabled={!creating}
|
|
||||||
onChange={handleValueChange('username')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<PasswordValidator
|
|
||||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
|
||||||
errorMessages={['Password is required', 'Password must be 64 characters or less']}
|
|
||||||
name="password"
|
|
||||||
label="Password"
|
|
||||||
className={classes.textField}
|
|
||||||
value={user.password}
|
|
||||||
onChange={handleValueChange('password')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<FormGroup>
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Switch checked={user.admin} onChange={handleCheckboxChange('admin')} id="admin" />}
|
|
||||||
label="Admin?"
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button variant="contained" color="primary" className={classes.button} type="submit" onClick={this.submit}>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} type="submit" onClick={onCancelEditing}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UserForm.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
user: PropTypes.object.isRequired,
|
|
||||||
creating: PropTypes.bool.isRequired,
|
|
||||||
onDoneEditing: PropTypes.func.isRequired,
|
|
||||||
onCancelEditing: PropTypes.func.isRequired,
|
|
||||||
uniqueUsername: PropTypes.func.isRequired,
|
|
||||||
handleValueChange: PropTypes.func.isRequired,
|
|
||||||
handleCheckboxChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withStyles(styles)(UserForm);
|
|
@ -1,107 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
|
|
||||||
import List from '@material-ui/core/List';
|
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
|
||||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
|
||||||
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
|
||||||
import Badge from '@material-ui/core/Badge';
|
|
||||||
|
|
||||||
import WifiIcon from '@material-ui/icons/Wifi';
|
|
||||||
import LockIcon from '@material-ui/icons/Lock';
|
|
||||||
import LockOpenIcon from '@material-ui/icons/LockOpen';
|
|
||||||
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
|
|
||||||
|
|
||||||
import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
scanningProgress: {
|
|
||||||
margin: theme.spacing(4),
|
|
||||||
textAlign: "center"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class WiFiNetworkSelector extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.renderNetwork = this.renderNetwork.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNetwork(network) {
|
|
||||||
return (
|
|
||||||
<ListItem key={network.bssid} button onClick={() => this.props.selectNetwork(network)}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={network.ssid}
|
|
||||||
secondary={"Security: "+ networkSecurityMode(network) + ", Ch: " + network.channel}
|
|
||||||
/>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Badge badgeContent={network.rssi + "db"}>
|
|
||||||
<WifiIcon />
|
|
||||||
</Badge>
|
|
||||||
</ListItemIcon>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, scanningForNetworks, networkList, errorMessage, requestNetworkScan } = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
scanningForNetworks ?
|
|
||||||
<div>
|
|
||||||
<LinearProgress className={classes.scanningProgress}/>
|
|
||||||
<Typography variant="h6" className={classes.scanningProgress}>
|
|
||||||
Scanning...
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
networkList ?
|
|
||||||
<List>
|
|
||||||
{networkList.networks.map(this.renderNetwork)}
|
|
||||||
</List>
|
|
||||||
:
|
|
||||||
<div>
|
|
||||||
<Typography variant="h6" className={classes.scanningProgress}>
|
|
||||||
{errorMessage}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button startIcon={<PermScanWifiIcon />} variant="contained" color="secondary" className={classes.button} onClick={requestNetworkScan} disabled={scanningForNetworks}>
|
|
||||||
Scan again...
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiNetworkSelector.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
selectNetwork: PropTypes.func.isRequired,
|
|
||||||
scanningForNetworks: PropTypes.bool.isRequired,
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
networkList: PropTypes.object,
|
|
||||||
requestNetworkScan: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withStyles(styles)(WiFiNetworkSelector);
|
|
@ -1,201 +0,0 @@
|
|||||||
import React, { Fragment } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import Checkbox from '@material-ui/core/Checkbox';
|
|
||||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
|
||||||
import List from '@material-ui/core/List';
|
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
|
||||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
|
||||||
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
|
|
||||||
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
|
||||||
import LockIcon from '@material-ui/icons/Lock';
|
|
||||||
import LockOpenIcon from '@material-ui/icons/LockOpen';
|
|
||||||
import DeleteIcon from '@material-ui/icons/Delete';
|
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
|
||||||
|
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
|
||||||
import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes';
|
|
||||||
|
|
||||||
import isIP from '../validators/isIP';
|
|
||||||
import isHostname from '../validators/isHostname';
|
|
||||||
import optional from '../validators/optional';
|
|
||||||
import PasswordValidator from '../components/PasswordValidator';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
textField: {
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
checkboxControl: {
|
|
||||||
width: "100%"
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class WiFiSettingsForm extends React.Component {
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
ValidatorForm.addValidationRule('isIP', isIP);
|
|
||||||
ValidatorForm.addValidationRule('isHostname', isHostname);
|
|
||||||
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSelectedNetwork() {
|
|
||||||
const { selectedNetwork, deselectNetwork } = this.props;
|
|
||||||
return (
|
|
||||||
<List>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary={selectedNetwork.ssid}
|
|
||||||
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
|
||||||
<DeleteIcon />
|
|
||||||
</IconButton>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes, wifiSettings, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
|
|
||||||
return (
|
|
||||||
<ValidatorForm onSubmit={onSubmit} ref="WiFiSettingsForm">
|
|
||||||
{
|
|
||||||
selectedNetwork ? this.renderSelectedNetwork() :
|
|
||||||
<TextValidator
|
|
||||||
validators={['matchRegexp:^.{0,32}$']}
|
|
||||||
errorMessages={['SSID must be 32 characters or less']}
|
|
||||||
name="ssid"
|
|
||||||
label="SSID"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.ssid}
|
|
||||||
onChange={handleValueChange('ssid')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!isNetworkOpen(selectedNetwork) &&
|
|
||||||
<PasswordValidator
|
|
||||||
validators={['matchRegexp:^.{0,64}$']}
|
|
||||||
errorMessages={['Password must be 64 characters or less']}
|
|
||||||
name="password"
|
|
||||||
label="Password"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.password}
|
|
||||||
onChange={handleValueChange('password')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isHostname']}
|
|
||||||
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
|
||||||
name="hostname"
|
|
||||||
label="Hostname"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.hostname}
|
|
||||||
onChange={handleValueChange('hostname')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<FormControlLabel className={classes.checkboxControl}
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
value="static_ip_config"
|
|
||||||
checked={wifiSettings.static_ip_config}
|
|
||||||
onChange={handleCheckboxChange("static_ip_config")}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Static IP Config?"
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
wifiSettings.static_ip_config &&
|
|
||||||
<Fragment>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isIP']}
|
|
||||||
errorMessages={['Local IP is required', 'Must be an IP address']}
|
|
||||||
name="local_ip"
|
|
||||||
label="Local IP"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.local_ip}
|
|
||||||
onChange={handleValueChange('local_ip')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isIP']}
|
|
||||||
errorMessages={['Gateway IP is required', 'Must be an IP address']}
|
|
||||||
name="gateway_ip"
|
|
||||||
label="Gateway"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.gateway_ip}
|
|
||||||
onChange={handleValueChange('gateway_ip')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['required', 'isIP']}
|
|
||||||
errorMessages={['Subnet mask is required', 'Must be an IP address']}
|
|
||||||
name="subnet_mask"
|
|
||||||
label="Subnet"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.subnet_mask}
|
|
||||||
onChange={handleValueChange('subnet_mask')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['isOptionalIP']}
|
|
||||||
errorMessages={['Must be an IP address']}
|
|
||||||
name="dns_ip_1"
|
|
||||||
label="DNS IP #1"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.dns_ip_1}
|
|
||||||
onChange={handleValueChange('dns_ip_1')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
<TextValidator
|
|
||||||
validators={['isOptionalIP']}
|
|
||||||
errorMessages={['Must be an IP address']}
|
|
||||||
name="dns_ip_2"
|
|
||||||
label="DNS IP #2"
|
|
||||||
className={classes.textField}
|
|
||||||
value={wifiSettings.dns_ip_2}
|
|
||||||
onChange={handleValueChange('dns_ip_2')}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiSettingsForm.propTypes = {
|
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
wifiSettings: PropTypes.object,
|
|
||||||
deselectNetwork: PropTypes.func,
|
|
||||||
selectedNetwork: PropTypes.object,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
handleValueChange: PropTypes.func.isRequired,
|
|
||||||
handleCheckboxChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withStyles(styles)(WiFiSettingsForm);
|
|
30
interface/src/ntp/NTPSettingsController.tsx
Normal file
30
interface/src/ntp/NTPSettingsController.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
import { NTP_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
|
import NTPSettingsForm from './NTPSettingsForm';
|
||||||
|
import { NTPSettings } from './types';
|
||||||
|
|
||||||
|
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
||||||
|
|
||||||
|
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="NTP Settings" titleGutter>
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <NTPSettingsForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
84
interface/src/ntp/NTPSettingsForm.tsx
Normal file
84
interface/src/ntp/NTPSettingsForm.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import { Checkbox, MenuItem } from '@material-ui/core';
|
||||||
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
|
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
|
||||||
|
import { isIP, isHostname, or } from '../validators';
|
||||||
|
|
||||||
|
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
||||||
|
import { NTPSettings } from './types';
|
||||||
|
|
||||||
|
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
||||||
|
|
||||||
|
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTimeZone = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const { data, setData } = this.props;
|
||||||
|
setData({
|
||||||
|
...data,
|
||||||
|
tz_label: event.target.value,
|
||||||
|
tz_format: TIME_ZONES[event.target.value]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data, handleValueChange, handleCheckboxChange, saveData, loadData } = this.props;
|
||||||
|
return (
|
||||||
|
<ValidatorForm onSubmit={saveData}>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.enabled}
|
||||||
|
onChange={handleCheckboxChange('enabled')}
|
||||||
|
value="enabled"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable NTP?"
|
||||||
|
/>
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'isIPOrHostname']}
|
||||||
|
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
|
||||||
|
name="server"
|
||||||
|
label="Server"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.server}
|
||||||
|
onChange={handleValueChange('server')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<SelectValidator
|
||||||
|
validators={['required']}
|
||||||
|
errorMessages={['Time zone is required']}
|
||||||
|
name="tz_label"
|
||||||
|
labelId="tz_label"
|
||||||
|
label="Time zone"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
native
|
||||||
|
value={selectedTimeZone(data.tz_label, data.tz_format)}
|
||||||
|
onChange={this.changeTimeZone}
|
||||||
|
margin="normal"
|
||||||
|
>
|
||||||
|
<MenuItem disabled={true}>Time zone...</MenuItem>
|
||||||
|
{timeZoneSelectItems()}
|
||||||
|
</SelectValidator>
|
||||||
|
<FormActions>
|
||||||
|
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||||
|
Save
|
||||||
|
</FormButton>
|
||||||
|
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||||
|
Reset
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</ValidatorForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NTPSettingsForm;
|
29
interface/src/ntp/NTPStatus.ts
Normal file
29
interface/src/ntp/NTPStatus.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Theme } from "@material-ui/core";
|
||||||
|
import { NTPStatus } from "./types";
|
||||||
|
|
||||||
|
export const NTP_INACTIVE = 0;
|
||||||
|
export const NTP_ACTIVE = 1;
|
||||||
|
|
||||||
|
export const isNtpActive = ({ status }: NTPStatus) => status === NTP_ACTIVE;
|
||||||
|
|
||||||
|
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||||
|
switch (status) {
|
||||||
|
case NTP_INACTIVE:
|
||||||
|
return theme.palette.info.main;
|
||||||
|
case NTP_ACTIVE:
|
||||||
|
return theme.palette.success.main;
|
||||||
|
default:
|
||||||
|
return theme.palette.error.main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ntpStatus = ({ status }: NTPStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case NTP_INACTIVE:
|
||||||
|
return "Inactive";
|
||||||
|
case NTP_ACTIVE:
|
||||||
|
return "Active";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
30
interface/src/ntp/NTPStatusController.tsx
Normal file
30
interface/src/ntp/NTPStatusController.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
import { NTP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
|
import NTPStatusForm from './NTPStatusForm';
|
||||||
|
import { NTPStatus } from './types';
|
||||||
|
|
||||||
|
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
||||||
|
|
||||||
|
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="NTP Status">
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <NTPStatusForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
89
interface/src/ntp/NTPStatusForm.tsx
Normal file
89
interface/src/ntp/NTPStatusForm.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { WithTheme, withTheme } from '@material-ui/core/styles';
|
||||||
|
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||||
|
|
||||||
|
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
||||||
|
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||||
|
import DNSIcon from '@material-ui/icons/Dns';
|
||||||
|
import UpdateIcon from '@material-ui/icons/Update';
|
||||||
|
import AvTimerIcon from '@material-ui/icons/AvTimer';
|
||||||
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
|
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
|
||||||
|
|
||||||
|
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
||||||
|
import { formatIsoDateTime } from './TimeFormat';
|
||||||
|
import { NTPStatus } from './types';
|
||||||
|
|
||||||
|
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme;
|
||||||
|
|
||||||
|
class NTPStatusForm extends Component<NTPStatusFormProps> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data, theme } = this.props
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<HighlightAvatar color={ntpStatusHighlight(data, theme)}>
|
||||||
|
<UpdateIcon />
|
||||||
|
</HighlightAvatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="Status" secondary={ntpStatus(data)} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
{isNtpActive(data) && (
|
||||||
|
<Fragment>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<AccessTimeIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="Local Time" secondary={formatIsoDateTime(data.time_local)} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<SwapVerticalCircleIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="UTC Time" secondary={formatIsoDateTime(data.time_utc)} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<DNSIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="NTP Server" secondary={data.server} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<AvTimerIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="Uptime" secondary={moment.duration(data.uptime, 'seconds').humanize()} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" component="li" />
|
||||||
|
</List>
|
||||||
|
<FormActions>
|
||||||
|
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||||
|
Refresh
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTheme(NTPStatusForm);
|
39
interface/src/ntp/NetworkTime.tsx
Normal file
39
interface/src/ntp/NetworkTime.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||||
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
|
import NTPStatusController from './NTPStatusController';
|
||||||
|
import NTPSettingsController from './NTPSettingsController';
|
||||||
|
|
||||||
|
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
|
class NetworkTime extends Component<NetworkTimeProps> {
|
||||||
|
|
||||||
|
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||||
|
this.props.history.push(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { authenticatedContext } = this.props;
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="Network Time">
|
||||||
|
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||||
|
<Tab value="/ntp/status" label="NTP Status" />
|
||||||
|
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
|
||||||
|
</Tabs>
|
||||||
|
<Switch>
|
||||||
|
<AuthenticatedRoute exact={true} path="/ntp/status" component={NTPStatusController} />
|
||||||
|
<AuthenticatedRoute exact={true} path="/ntp/settings" component={NTPSettingsController} />
|
||||||
|
<Redirect to="/ntp/status" />
|
||||||
|
</Switch>
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticatedContext(NetworkTime)
|
@ -1,7 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
export const TIME_ZONES = {
|
type TimeZones = {
|
||||||
|
[name: string]: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIME_ZONES: TimeZones = {
|
||||||
"Africa/Abidjan": "GMT0",
|
"Africa/Abidjan": "GMT0",
|
||||||
"Africa/Accra": "GMT0",
|
"Africa/Accra": "GMT0",
|
||||||
"Africa/Addis_Ababa": "EAT-3",
|
"Africa/Addis_Ababa": "EAT-3",
|
||||||
@ -464,7 +468,7 @@ export const TIME_ZONES = {
|
|||||||
"Etc/Zulu": "UTC0"
|
"Etc/Zulu": "UTC0"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectedTimeZone(label, format){
|
export function selectedTimeZone(label: string, format: string) {
|
||||||
return TIME_ZONES[label] === format ? label : undefined;
|
return TIME_ZONES[label] === format ? label : undefined;
|
||||||
}
|
}
|
||||||
|
|
3
interface/src/ntp/TimeFormat.ts
Normal file
3
interface/src/ntp/TimeFormat.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const formatIsoDateTime = (isoDateString: string) => moment.parseZone(isoDateString).format('ll @ HH:mm:ss');
|
14
interface/src/ntp/types.ts
Normal file
14
interface/src/ntp/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export interface NTPStatus {
|
||||||
|
status: number;
|
||||||
|
time_utc: string;
|
||||||
|
time_local: string;
|
||||||
|
server: string;
|
||||||
|
uptime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NTPSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
server: string;
|
||||||
|
tz_label: string;
|
||||||
|
tz_format: string;
|
||||||
|
}
|
@ -1,83 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
|
||||||
|
|
||||||
import { ENDPOINT_ROOT } from '../constants/Env';
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import { restComponent } from '../components/RestComponent';
|
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
|
||||||
|
|
||||||
import Button from '@material-ui/core/Button';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
import Slider from '@material-ui/core/Slider';
|
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
|
||||||
|
|
||||||
export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings";
|
|
||||||
|
|
||||||
const valueToPercentage = (value) => `${Math.round(value / 255 * 100)}%`;
|
|
||||||
|
|
||||||
class DemoController extends Component {
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { data, fetched, errorMessage, saveData, loadData, handleSliderChange } = this.props;
|
|
||||||
return (
|
|
||||||
<SectionContent title="Controller" titleGutter>
|
|
||||||
<LoadingNotification
|
|
||||||
onReset={loadData}
|
|
||||||
fetched={fetched}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
render={() =>
|
|
||||||
<DemoControllerForm
|
|
||||||
demoSettings={data}
|
|
||||||
onReset={loadData}
|
|
||||||
onSubmit={saveData}
|
|
||||||
handleSliderChange={handleSliderChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SectionContent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
},
|
|
||||||
blinkSpeedLabel: {
|
|
||||||
marginBottom: theme.spacing(5),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
function DemoControllerForm(props) {
|
|
||||||
const { demoSettings, onSubmit, onReset, handleSliderChange } = props;
|
|
||||||
const classes = useStyles();
|
|
||||||
return (
|
|
||||||
<ValidatorForm onSubmit={onSubmit}>
|
|
||||||
<Typography id="blink-speed-slider" className={classes.blinkSpeedLabel}>
|
|
||||||
Blink Speed
|
|
||||||
</Typography>
|
|
||||||
<Slider
|
|
||||||
value={demoSettings.blink_speed}
|
|
||||||
valueLabelFormat={valueToPercentage}
|
|
||||||
aria-labelledby="blink-speed-slider"
|
|
||||||
valueLabelDisplay="on"
|
|
||||||
min={0}
|
|
||||||
max={255}
|
|
||||||
onChange={handleSliderChange('blink_speed')}
|
|
||||||
/>
|
|
||||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
</ValidatorForm>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default restComponent(DEMO_SETTINGS_ENDPOINT, DemoController);
|
|
75
interface/src/project/DemoController.tsx
Normal file
75
interface/src/project/DemoController.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import { Typography, Slider, Box } from '@material-ui/core';
|
||||||
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
|
import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components';
|
||||||
|
|
||||||
|
export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings";
|
||||||
|
|
||||||
|
interface DemoSettings {
|
||||||
|
blink_speed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DemoControllerProps = RestControllerProps<DemoSettings>;
|
||||||
|
|
||||||
|
class DemoController extends Component<DemoControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title='Demo Controller' titleGutter>
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={props => (
|
||||||
|
<DemoControllerForm {...props} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(DEMO_SETTINGS_ENDPOINT, DemoController);
|
||||||
|
|
||||||
|
const valueToPercentage = (value: number) => `${Math.round(value / 255 * 100)}%`;
|
||||||
|
|
||||||
|
type DemoControllerFormProps = RestFormProps<DemoSettings>;
|
||||||
|
|
||||||
|
function DemoControllerForm(props: DemoControllerFormProps) {
|
||||||
|
const { data, saveData, loadData, handleSliderChange } = props;
|
||||||
|
return (
|
||||||
|
<ValidatorForm onSubmit={saveData}>
|
||||||
|
<Typography id="blink-speed-slider">
|
||||||
|
Blink Speed
|
||||||
|
</Typography>
|
||||||
|
<Box pt={5}>
|
||||||
|
<Slider
|
||||||
|
value={data.blink_speed}
|
||||||
|
valueLabelFormat={valueToPercentage}
|
||||||
|
aria-labelledby="blink-speed-slider"
|
||||||
|
valueLabelDisplay="on"
|
||||||
|
min={0}
|
||||||
|
max={255}
|
||||||
|
onChange={handleSliderChange('blink_speed')}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<FormActions>
|
||||||
|
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||||
|
Save
|
||||||
|
</FormButton>
|
||||||
|
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||||
|
Reset
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</ValidatorForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,29 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { Typography, TableRow, TableBody, TableCell, TableHead, Table, Box } from '@material-ui/core';
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { SectionContent } from '../components';
|
||||||
import Table from '@material-ui/core/Table';
|
|
||||||
import TableHead from '@material-ui/core/TableHead';
|
|
||||||
import TableCell from '@material-ui/core/TableCell';
|
|
||||||
import TableBody from '@material-ui/core/TableBody';
|
|
||||||
import TableRow from '@material-ui/core/TableRow';
|
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
|
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
fileTable: {
|
|
||||||
marginBottom: theme.spacing(2)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class DemoInformation extends Component {
|
class DemoInformation extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title="Demo Project - Blink Speed Controller" titleGutter>
|
<SectionContent title='Demo Information' titleGutter>
|
||||||
<Typography variant="body1" paragraph>
|
<Typography variant="body1" paragraph>
|
||||||
This simple demo project allows you to control the blink speed of the built-in LED.
|
This simple demo project allows you to control the blink speed of the built-in LED.
|
||||||
It demonstrates how the esp8266-react framework may be extended for your own IoT project.
|
It demonstrates how the esp8266-react framework may be extended for your own IoT project.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" paragraph>
|
<Typography variant="body1" paragraph>
|
||||||
@ -34,7 +19,7 @@ class DemoInformation extends Component {
|
|||||||
<Typography variant="body1" paragraph>
|
<Typography variant="body1" paragraph>
|
||||||
The demo project interface code stored in the interface/project directory:
|
The demo project interface code stored in the interface/project directory:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Table className={classes.fileTable}>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@ -48,7 +33,7 @@ class DemoInformation extends Component {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
ProjectMenu.js
|
ProjectMenu.tsx
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
You can add your project's screens to the side bar here.
|
You can add your project's screens to the side bar here.
|
||||||
@ -56,7 +41,7 @@ class DemoInformation extends Component {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
ProjectRouting.js
|
ProjectRouting.tsx
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
The routing which controls the screens of your project.
|
The routing which controls the screens of your project.
|
||||||
@ -64,7 +49,7 @@ class DemoInformation extends Component {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
DemoProject.js
|
DemoProject.tsx
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
This screen, with tabs and tab routing.
|
This screen, with tabs and tab routing.
|
||||||
@ -72,29 +57,31 @@ class DemoInformation extends Component {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
DemoInformation.js
|
DemoInformation.tsx
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
The demo information tab.
|
The demo information page.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
DemoController.js
|
DemoController.tsx
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
The demo controller tab, to control the built-in LED.
|
The demo controller tab, to control the built-in LED.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<Typography variant="body1" paragraph>
|
<Box mt={2}>
|
||||||
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.
|
<Typography variant="body1">
|
||||||
</Typography>
|
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(DemoInformation);
|
export default DemoInformation;
|
@ -1,36 +1,36 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Redirect, Switch } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { PROJECT_PATH } from '../api';
|
||||||
|
import { MenuAppBar } from '../components';
|
||||||
|
import { AuthenticatedRoute } from '../authentication';
|
||||||
|
|
||||||
import { PROJECT_PATH } from '../constants/Env';
|
|
||||||
import MenuAppBar from '../components/MenuAppBar';
|
|
||||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
|
||||||
import DemoInformation from './DemoInformation';
|
import DemoInformation from './DemoInformation';
|
||||||
import DemoController from './DemoController';
|
import DemoController from './DemoController';
|
||||||
|
|
||||||
import Tabs from '@material-ui/core/Tabs';
|
class DemoProject extends Component<RouteComponentProps> {
|
||||||
import Tab from '@material-ui/core/Tab';
|
|
||||||
|
|
||||||
class DemoProject extends Component {
|
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||||
|
|
||||||
handleTabChange = (event, path) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Demo Project">
|
<MenuAppBar sectionTitle="Demo Project">
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
|
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||||
<Tab value={`/${PROJECT_PATH}/demo/information`} label="Information" />
|
<Tab value={`/${PROJECT_PATH}/demo/information`} label="Demo Information" />
|
||||||
<Tab value={`/${PROJECT_PATH}/demo/controller`} label="Controller" />
|
<Tab value={`/${PROJECT_PATH}/demo/controller`} label="Demo Controller" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/information`} component={DemoInformation} />
|
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/information`} component={DemoInformation} />
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/controller`} component={DemoController} />
|
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/controller`} component={DemoController} />
|
||||||
<Redirect to={`/${PROJECT_PATH}/demo/information`} />
|
<Redirect to={`/${PROJECT_PATH}/demo/information`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { PROJECT_PATH } from '../constants/Env';
|
import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
|
||||||
|
|
||||||
import List from '@material-ui/core/List';
|
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
|
||||||
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
|
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
|
||||||
|
|
||||||
class ProjectMenu extends Component {
|
import { PROJECT_PATH } from '../api';
|
||||||
|
|
||||||
|
class ProjectMenu extends Component<RouteComponentProps> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const path = this.props.match.url;
|
const path = this.props.match.url;
|
@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Redirect, Switch } from 'react-router';
|
import { Redirect, Switch } from 'react-router';
|
||||||
|
|
||||||
import { PROJECT_PATH } from '../constants/Env';
|
import { PROJECT_PATH } from '../api';
|
||||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
import { AuthenticatedRoute } from '../authentication';
|
||||||
|
|
||||||
import DemoProject from './DemoProject';
|
import DemoProject from './DemoProject';
|
||||||
|
|
||||||
class ProjectRouting extends Component {
|
class ProjectRouting extends Component {
|
1
interface/src/react-app-env.d.ts
vendored
Normal file
1
interface/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
@ -1,37 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Redirect, Switch } from 'react-router-dom'
|
|
||||||
|
|
||||||
import Tabs from '@material-ui/core/Tabs';
|
|
||||||
import Tab from '@material-ui/core/Tab';
|
|
||||||
|
|
||||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
|
||||||
import MenuAppBar from '../components/MenuAppBar';
|
|
||||||
import APSettings from '../containers/APSettings';
|
|
||||||
import APStatus from '../containers/APStatus';
|
|
||||||
import { withAuthenticationContext } from '../authentication/Context.js';
|
|
||||||
|
|
||||||
class AccessPoint extends Component {
|
|
||||||
|
|
||||||
handleTabChange = (event, path) => {
|
|
||||||
this.props.history.push(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { authenticationContext } = this.props;
|
|
||||||
return (
|
|
||||||
<MenuAppBar sectionTitle="Access Point">
|
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
|
|
||||||
<Tab value="/ap/status" label="Access Point Status" />
|
|
||||||
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticationContext.isAdmin()} />
|
|
||||||
</Tabs>
|
|
||||||
<Switch>
|
|
||||||
<AuthenticatedRoute exact={true} path="/ap/status" component={APStatus} />
|
|
||||||
<AuthenticatedRoute exact={true} path="/ap/settings" component={APSettings} />
|
|
||||||
<Redirect to="/ap/status" />
|
|
||||||
</Switch>
|
|
||||||
</MenuAppBar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withAuthenticationContext(AccessPoint);
|
|
@ -1,38 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Redirect, Switch } from 'react-router-dom'
|
|
||||||
|
|
||||||
import Tabs from '@material-ui/core/Tabs';
|
|
||||||
import Tab from '@material-ui/core/Tab';
|
|
||||||
|
|
||||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
|
||||||
import MenuAppBar from '../components/MenuAppBar';
|
|
||||||
import NTPSettings from '../containers/NTPSettings';
|
|
||||||
import NTPStatus from '../containers/NTPStatus';
|
|
||||||
import { withAuthenticationContext } from '../authentication/Context.js';
|
|
||||||
|
|
||||||
class NetworkTime extends Component {
|
|
||||||
|
|
||||||
handleTabChange = (event, path) => {
|
|
||||||
this.props.history.push(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { authenticationContext } = this.props;
|
|
||||||
return (
|
|
||||||
<MenuAppBar sectionTitle="Network Time">
|
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
|
|
||||||
<Tab value="/ntp/status" label="NTP Status" />
|
|
||||||
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticationContext.isAdmin()} />
|
|
||||||
</Tabs>
|
|
||||||
<Switch>
|
|
||||||
<AuthenticatedRoute exact={true} path="/ntp/status" component={NTPStatus} />
|
|
||||||
<AuthenticatedRoute exact={true} path="/ntp/settings" component={NTPSettings} />
|
|
||||||
<Redirect to="/ntp/status" />
|
|
||||||
</Switch>
|
|
||||||
</MenuAppBar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withAuthenticationContext(NetworkTime)
|
|
@ -1,35 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Redirect, Switch } from 'react-router-dom'
|
|
||||||
|
|
||||||
import Tabs from '@material-ui/core/Tabs';
|
|
||||||
import Tab from '@material-ui/core/Tab';
|
|
||||||
|
|
||||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
|
||||||
import MenuAppBar from '../components/MenuAppBar';
|
|
||||||
import ManageUsers from '../containers/ManageUsers';
|
|
||||||
import SecuritySettings from '../containers/SecuritySettings';
|
|
||||||
|
|
||||||
class Security extends Component {
|
|
||||||
|
|
||||||
handleTabChange = (event, path) => {
|
|
||||||
this.props.history.push(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<MenuAppBar sectionTitle="Security">
|
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
|
|
||||||
<Tab value="/security/users" label="Manage Users" />
|
|
||||||
<Tab value="/security/settings" label="Security Settings" />
|
|
||||||
</Tabs>
|
|
||||||
<Switch>
|
|
||||||
<AuthenticatedRoute exact={true} path="/security/users" component={ManageUsers} />
|
|
||||||
<AuthenticatedRoute exact={true} path="/security/settings" component={SecuritySettings} />
|
|
||||||
<Redirect to="/security/users" />
|
|
||||||
</Switch>
|
|
||||||
</MenuAppBar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Security;
|
|
@ -1,37 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Redirect, Switch } from 'react-router-dom'
|
|
||||||
|
|
||||||
import Tabs from '@material-ui/core/Tabs';
|
|
||||||
import Tab from '@material-ui/core/Tab';
|
|
||||||
|
|
||||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
|
||||||
import MenuAppBar from '../components/MenuAppBar';
|
|
||||||
import OTASettings from '../containers/OTASettings';
|
|
||||||
import SystemStatus from '../containers/SystemStatus';
|
|
||||||
import { withAuthenticationContext } from '../authentication/Context.js';
|
|
||||||
|
|
||||||
class System extends Component {
|
|
||||||
|
|
||||||
handleTabChange = (event, path) => {
|
|
||||||
this.props.history.push(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { authenticationContext } = this.props;
|
|
||||||
return (
|
|
||||||
<MenuAppBar sectionTitle="System">
|
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
|
|
||||||
<Tab value="/system/status" label="System Status" />
|
|
||||||
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticationContext.isAdmin()} />
|
|
||||||
</Tabs>
|
|
||||||
<Switch>
|
|
||||||
<AuthenticatedRoute exact={true} path="/system/status" component={SystemStatus} />
|
|
||||||
<AuthenticatedRoute exact={true} path="/system/ota" component={OTASettings} />
|
|
||||||
<Redirect to="/system/status" />
|
|
||||||
</Switch>
|
|
||||||
</MenuAppBar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withAuthenticationContext(System);
|
|
@ -1,74 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Redirect, Switch } from 'react-router-dom'
|
|
||||||
|
|
||||||
import Tabs from '@material-ui/core/Tabs';
|
|
||||||
import Tab from '@material-ui/core/Tab';
|
|
||||||
|
|
||||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
|
||||||
import MenuAppBar from '../components/MenuAppBar';
|
|
||||||
import WiFiNetworkScanner from '../containers/WiFiNetworkScanner';
|
|
||||||
import WiFiSettings from '../containers/WiFiSettings';
|
|
||||||
import WiFiStatus from '../containers/WiFiStatus';
|
|
||||||
import { withAuthenticationContext } from '../authentication/Context.js';
|
|
||||||
|
|
||||||
class WiFiConnection extends Component {
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
selectedNetwork: null
|
|
||||||
};
|
|
||||||
this.selectNetwork = this.selectNetwork.bind(this);
|
|
||||||
this.deselectNetwork = this.deselectNetwork.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectNetwork(network) {
|
|
||||||
this.setState({ selectedNetwork: network });
|
|
||||||
this.props.history.push('/wifi/settings');
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectNetwork(network) {
|
|
||||||
this.setState({ selectedNetwork: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTabChange = (event, path) => {
|
|
||||||
this.props.history.push(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { authenticationContext } = this.props;
|
|
||||||
const ConfiguredWiFiNetworkScanner = (props) => {
|
|
||||||
return (
|
|
||||||
<WiFiNetworkScanner
|
|
||||||
selectNetwork={this.selectNetwork}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const ConfiguredWiFiSettings = (props) => {
|
|
||||||
return (
|
|
||||||
<WiFiSettings
|
|
||||||
deselectNetwork={this.deselectNetwork} selectedNetwork={this.state.selectedNetwork}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<MenuAppBar sectionTitle="WiFi Connection">
|
|
||||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
|
|
||||||
<Tab value="/wifi/status" label="WiFi Status" />
|
|
||||||
<Tab value="/wifi/scan" label="Scan Networks" disabled={!authenticationContext.isAdmin()} />
|
|
||||||
<Tab value="/wifi/settings" label="WiFi Settings" disabled={!authenticationContext.isAdmin()} />
|
|
||||||
</Tabs>
|
|
||||||
<Switch>
|
|
||||||
<AuthenticatedRoute exact={true} path="/wifi/status" component={WiFiStatus} />
|
|
||||||
<AuthenticatedRoute exact={true} path="/wifi/scan" component={ConfiguredWiFiNetworkScanner} />
|
|
||||||
<AuthenticatedRoute exact={true} path="/wifi/settings" component={ConfiguredWiFiSettings} />
|
|
||||||
<Redirect to="/wifi/status" />
|
|
||||||
</Switch>
|
|
||||||
</MenuAppBar>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withAuthenticationContext(WiFiConnection);
|
|
30
interface/src/security/ManageUsersController.tsx
Normal file
30
interface/src/security/ManageUsersController.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
|
import ManageUsersForm from './ManageUsersForm';
|
||||||
|
import { SecuritySettings } from './types';
|
||||||
|
|
||||||
|
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
|
||||||
|
|
||||||
|
class ManageUsersController extends Component<ManageUsersControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="Manage Users" titleGutter>
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <ManageUsersForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
|
@ -1,18 +1,9 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow } from '@material-ui/core';
|
||||||
import Button from '@material-ui/core/Button';
|
import { Box, Button, Typography, } from '@material-ui/core';
|
||||||
import Typography from '@material-ui/core/Typography';
|
|
||||||
import Table from '@material-ui/core/Table';
|
|
||||||
import TableBody from '@material-ui/core/TableBody';
|
|
||||||
import TableCell from '@material-ui/core/TableCell';
|
|
||||||
import TableFooter from '@material-ui/core/TableFooter';
|
|
||||||
import TableHead from '@material-ui/core/TableHead';
|
|
||||||
import TableRow from '@material-ui/core/TableRow';
|
|
||||||
import Box from '@material-ui/core/Box';
|
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
import DeleteIcon from '@material-ui/icons/Delete';
|
import DeleteIcon from '@material-ui/icons/Delete';
|
||||||
import CloseIcon from '@material-ui/icons/Close';
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
@ -21,20 +12,13 @@ import IconButton from '@material-ui/core/IconButton';
|
|||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||||
|
|
||||||
|
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||||
|
import { RestFormProps, FormActions, FormButton } from '../components';
|
||||||
|
|
||||||
import UserForm from './UserForm';
|
import UserForm from './UserForm';
|
||||||
import { withAuthenticationContext } from '../authentication/Context';
|
import { SecuritySettings, User } from './types';
|
||||||
|
|
||||||
const styles = theme => ({
|
function compareUsers(a: User, b: User) {
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
'& td, & th': { padding: theme.spacing(0.5) }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function compareUsers(a, b) {
|
|
||||||
if (a.username < b.username) {
|
if (a.username < b.username) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@ -44,12 +28,18 @@ function compareUsers(a, b) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ManageUsersForm extends React.Component {
|
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
||||||
|
|
||||||
constructor(props) {
|
type ManageUsersFormState = {
|
||||||
super(props);
|
creating: boolean;
|
||||||
this.state = {};
|
user?: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
||||||
|
|
||||||
|
state: ManageUsersFormState = {
|
||||||
|
creating: false
|
||||||
|
};
|
||||||
|
|
||||||
createUser = () => {
|
createUser = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -62,21 +52,21 @@ class ManageUsersForm extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
uniqueUsername = username => {
|
uniqueUsername = (username: string) => {
|
||||||
return !this.props.userData.users.find(u => u.username === username);
|
return !this.props.data.users.find(u => u.username === username);
|
||||||
}
|
}
|
||||||
|
|
||||||
noAdminConfigured = () => {
|
noAdminConfigured = () => {
|
||||||
return !this.props.userData.users.find(u => u.admin);
|
return !this.props.data.users.find(u => u.admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUser = user => {
|
removeUser = (user: User) => {
|
||||||
const { userData } = this.props;
|
const { data } = this.props;
|
||||||
const users = userData.users.filter(u => u.username !== user.username);
|
const users = data.users.filter(u => u.username !== user.username);
|
||||||
this.props.setData({ ...userData, users });
|
this.props.setData({ ...data, users });
|
||||||
}
|
}
|
||||||
|
|
||||||
startEditingUser = user => {
|
startEditingUser = (user: User) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
creating: false,
|
creating: false,
|
||||||
user
|
user
|
||||||
@ -91,45 +81,37 @@ class ManageUsersForm extends React.Component {
|
|||||||
|
|
||||||
doneEditingUser = () => {
|
doneEditingUser = () => {
|
||||||
const { user } = this.state;
|
const { user } = this.state;
|
||||||
const { userData } = this.props;
|
if (user) {
|
||||||
const users = userData.users.filter(u => u.username !== user.username);
|
const { data } = this.props;
|
||||||
users.push(user);
|
const users = data.users.filter(u => u.username !== user.username);
|
||||||
this.props.setData({ ...userData, users });
|
users.push(user);
|
||||||
this.setState({
|
this.props.setData({ ...data, users });
|
||||||
user: undefined
|
this.setState({
|
||||||
});
|
user: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUserValueChange = name => event => {
|
handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { user } = this.state;
|
this.setState({ user: { ...this.state.user!, [name]: event.target.value } });
|
||||||
this.setState({
|
|
||||||
user: {
|
|
||||||
...user, [name]: event.target.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUserCheckboxChange = name => event => {
|
handleUserCheckboxChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { user } = this.state;
|
this.setState({ user: { ...this.state.user!, [name]: event.target.checked } });
|
||||||
this.setState({
|
|
||||||
user: {
|
|
||||||
...user, [name]: event.target.checked
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
this.props.onSubmit();
|
this.props.saveData();
|
||||||
this.props.authenticationContext.refresh();
|
this.props.authenticatedContext.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, userData, onReset } = this.props;
|
const { data, loadData } = this.props;
|
||||||
const { user, creating } = this.state;
|
const { user, creating } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ValidatorForm onSubmit={this.onSubmit}>
|
<ValidatorForm onSubmit={this.onSubmit}>
|
||||||
<Table className={classes.table}>
|
<Table size="small">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Username</TableCell>
|
<TableCell>Username</TableCell>
|
||||||
@ -138,7 +120,7 @@ class ManageUsersForm extends React.Component {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{userData.users.sort(compareUsers).map(user => (
|
{data.users.sort(compareUsers).map(user => (
|
||||||
<TableRow key={user.username}>
|
<TableRow key={user.username}>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{user.username}
|
{user.username}
|
||||||
@ -178,12 +160,14 @@ class ManageUsersForm extends React.Component {
|
|||||||
</Box>
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
|
<FormActions>
|
||||||
Save
|
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}>
|
||||||
</Button>
|
Save
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
</FormButton>
|
||||||
Reset
|
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||||
</Button>
|
Reset
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
</ValidatorForm>
|
</ValidatorForm>
|
||||||
{
|
{
|
||||||
user &&
|
user &&
|
||||||
@ -203,14 +187,4 @@ class ManageUsersForm extends React.Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ManageUsersForm.propTypes = {
|
export default withAuthenticatedContext(ManageUsersForm);
|
||||||
classes: PropTypes.object.isRequired,
|
|
||||||
userData: PropTypes.object,
|
|
||||||
onSubmit: PropTypes.func.isRequired,
|
|
||||||
onReset: PropTypes.func.isRequired,
|
|
||||||
setData: PropTypes.func.isRequired,
|
|
||||||
handleValueChange: PropTypes.func.isRequired,
|
|
||||||
authenticationContext: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withAuthenticationContext(withStyles(styles)(ManageUsersForm));
|
|
37
interface/src/security/Security.tsx
Normal file
37
interface/src/security/Security.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||||
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
|
import ManageUsersController from './ManageUsersController';
|
||||||
|
import SecuritySettingsController from './SecuritySettingsController';
|
||||||
|
|
||||||
|
type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
|
class Security extends Component<SecurityProps> {
|
||||||
|
|
||||||
|
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||||
|
this.props.history.push(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="Security">
|
||||||
|
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||||
|
<Tab value="/security/users" label="Manage Users" />
|
||||||
|
<Tab value="/security/settings" label="Security Settings" />
|
||||||
|
</Tabs>
|
||||||
|
<Switch>
|
||||||
|
<AuthenticatedRoute exact={true} path="/security/users" component={ManageUsersController} />
|
||||||
|
<AuthenticatedRoute exact={true} path="/security/settings" component={SecuritySettingsController} />
|
||||||
|
<Redirect to="/security/users" />
|
||||||
|
</Switch>
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Security;
|
30
interface/src/security/SecuritySettingsController.tsx
Normal file
30
interface/src/security/SecuritySettingsController.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
|
import SecuritySettingsForm from './SecuritySettingsForm';
|
||||||
|
import { SecuritySettings } from './types';
|
||||||
|
|
||||||
|
type SecuritySettingsControllerProps = RestControllerProps<SecuritySettings>;
|
||||||
|
|
||||||
|
class SecuritySettingsController extends Component<SecuritySettingsControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="Security Settings" titleGutter>
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <SecuritySettingsForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(SECURITY_SETTINGS_ENDPOINT, SecuritySettingsController);
|
55
interface/src/security/SecuritySettingsForm.tsx
Normal file
55
interface/src/security/SecuritySettingsForm.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import { Box, Typography } from '@material-ui/core';
|
||||||
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
|
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||||
|
import { RestFormProps, PasswordValidator, FormActions, FormButton } from '../components';
|
||||||
|
|
||||||
|
import { SecuritySettings } from './types';
|
||||||
|
|
||||||
|
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
||||||
|
|
||||||
|
class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
this.props.saveData();
|
||||||
|
this.props.authenticatedContext.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data, handleValueChange, loadData } = this.props;
|
||||||
|
return (
|
||||||
|
<ValidatorForm onSubmit={this.onSubmit}>
|
||||||
|
<PasswordValidator
|
||||||
|
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||||
|
errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']}
|
||||||
|
name="jwt_secret"
|
||||||
|
label="JWT Secret"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.jwt_secret}
|
||||||
|
onChange={handleValueChange('jwt_secret')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<Typography component="div" variant="body1">
|
||||||
|
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
|
||||||
|
If you modify the JWT Secret, all users will be logged out.
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
<FormActions>
|
||||||
|
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||||
|
Save
|
||||||
|
</FormButton>
|
||||||
|
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||||
|
Reset
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</ValidatorForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticatedContext(SecuritySettingsForm);
|
87
interface/src/security/UserForm.tsx
Normal file
87
interface/src/security/UserForm.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React, { RefObject } from 'react';
|
||||||
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import { Dialog, DialogTitle, DialogContent, DialogActions, Checkbox } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { PasswordValidator, BlockFormControlLabel, FormButton } from '../components';
|
||||||
|
|
||||||
|
import { User } from './types';
|
||||||
|
|
||||||
|
interface UserFormProps {
|
||||||
|
creating: boolean;
|
||||||
|
user: User;
|
||||||
|
uniqueUsername: (value: any) => boolean;
|
||||||
|
handleValueChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
handleCheckboxChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => void;
|
||||||
|
onDoneEditing: () => void;
|
||||||
|
onCancelEditing: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserForm extends React.Component<UserFormProps> {
|
||||||
|
|
||||||
|
formRef: RefObject<any> = React.createRef();
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = () => {
|
||||||
|
this.formRef.current.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { user, creating, handleValueChange, handleCheckboxChange, onDoneEditing, onCancelEditing } = this.props;
|
||||||
|
return (
|
||||||
|
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||||
|
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open={true}>
|
||||||
|
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
|
||||||
|
<DialogContent dividers={true}>
|
||||||
|
<TextValidator
|
||||||
|
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []}
|
||||||
|
errorMessages={creating ? ['Username is required', "Username already exists", "Must be 1-24 characters: alpha numeric, '_' or '.'"] : []}
|
||||||
|
name="username"
|
||||||
|
label="Username"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={user.username}
|
||||||
|
disabled={!creating}
|
||||||
|
onChange={handleValueChange('username')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<PasswordValidator
|
||||||
|
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||||
|
errorMessages={['Password is required', 'Password must be 64 characters or less']}
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={user.password}
|
||||||
|
onChange={handleValueChange('password')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
value="admin"
|
||||||
|
checked={user.admin}
|
||||||
|
onChange={handleCheckboxChange('admin')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Admin?"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
|
||||||
|
Done
|
||||||
|
</FormButton>
|
||||||
|
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
|
||||||
|
Cancel
|
||||||
|
</FormButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</ValidatorForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserForm;
|
11
interface/src/security/types.ts
Normal file
11
interface/src/security/types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface User {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
admin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SecuritySettings {
|
||||||
|
users: User[];
|
||||||
|
jwt_secret: string;
|
||||||
|
}
|
||||||
|
|
145
interface/src/serviceWorker.ts
Normal file
145
interface/src/serviceWorker.ts
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
type Config = {
|
||||||
|
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||||
|
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function register(config?: Config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(
|
||||||
|
process.env.PUBLIC_URL,
|
||||||
|
window.location.href
|
||||||
|
);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl: string, config?: Config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { 'Service-Worker': 'script' }
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
30
interface/src/system/OTASettingsController.tsx
Normal file
30
interface/src/system/OTASettingsController.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
import { OTA_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
|
import OTASettingsForm from './OTASettingsForm';
|
||||||
|
import { OTASettings } from './types';
|
||||||
|
|
||||||
|
type OTASettingsControllerProps = RestControllerProps<OTASettings>;
|
||||||
|
|
||||||
|
class OTASettingsController extends Component<OTASettingsControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="OTA Settings" titleGutter>
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <OTASettingsForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(OTA_SETTINGS_ENDPOINT, OTASettingsController);
|
69
interface/src/system/OTASettingsForm.tsx
Normal file
69
interface/src/system/OTASettingsForm.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
|
import { Checkbox } from '@material-ui/core';
|
||||||
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
|
import { RestFormProps, BlockFormControlLabel, PasswordValidator, FormButton, FormActions } from '../components';
|
||||||
|
import {isIP,isHostname,or} from '../validators';
|
||||||
|
|
||||||
|
import { OTASettings } from './types';
|
||||||
|
|
||||||
|
type OTASettingsFormProps = RestFormProps<OTASettings>;
|
||||||
|
|
||||||
|
class OTASettingsForm extends React.Component<OTASettingsFormProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data, handleValueChange, handleCheckboxChange, saveData, loadData } = this.props;
|
||||||
|
return (
|
||||||
|
<ValidatorForm onSubmit={saveData}>
|
||||||
|
<BlockFormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={data.enabled}
|
||||||
|
onChange={handleCheckboxChange("enabled")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable OTA Updates?"
|
||||||
|
/>
|
||||||
|
<TextValidator
|
||||||
|
validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']}
|
||||||
|
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]}
|
||||||
|
name="port"
|
||||||
|
label="Port"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.port}
|
||||||
|
type="number"
|
||||||
|
onChange={handleValueChange('port')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<PasswordValidator
|
||||||
|
validators={['required', 'matchRegexp:^.{1,64}$']}
|
||||||
|
errorMessages={['OTA Password is required', 'OTA Point Password must be 64 characters or less']}
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={data.password}
|
||||||
|
onChange={handleValueChange('password')}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<FormActions>
|
||||||
|
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
||||||
|
Save
|
||||||
|
</FormButton>
|
||||||
|
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||||
|
Reset
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
|
</ValidatorForm>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OTASettingsForm;
|
38
interface/src/system/System.tsx
Normal file
38
interface/src/system/System.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
||||||
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
|
import SystemStatusController from './SystemStatusController';
|
||||||
|
import OTASettingsController from './OTASettingsController';
|
||||||
|
|
||||||
|
type SystemProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
|
class System extends Component<SystemProps> {
|
||||||
|
|
||||||
|
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||||
|
this.props.history.push(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { authenticatedContext } = this.props;
|
||||||
|
return (
|
||||||
|
<MenuAppBar sectionTitle="System">
|
||||||
|
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||||
|
<Tab value="/system/status" label="System Status" />
|
||||||
|
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
|
||||||
|
</Tabs>
|
||||||
|
<Switch>
|
||||||
|
<AuthenticatedRoute exact={true} path="/system/status" component={SystemStatusController} />
|
||||||
|
<AuthenticatedRoute exact={true} path="/system/ota" component={OTASettingsController} />
|
||||||
|
<Redirect to="/system/status" />
|
||||||
|
</Switch>
|
||||||
|
</MenuAppBar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withAuthenticatedContext(System);
|
30
interface/src/system/SystemStatusController.tsx
Normal file
30
interface/src/system/SystemStatusController.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
||||||
|
import { SYSTEM_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
|
import SystemStatusForm from './SystemStatusForm';
|
||||||
|
import { SystemStatus } from './types';
|
||||||
|
|
||||||
|
type SystemStatusControllerProps = RestControllerProps<SystemStatus>;
|
||||||
|
|
||||||
|
class SystemStatusController extends Component<SystemStatusControllerProps> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<SectionContent title="System Status">
|
||||||
|
<RestFormLoader
|
||||||
|
{...this.props}
|
||||||
|
render={formProps => <SystemStatusForm {...formProps} />}
|
||||||
|
/>
|
||||||
|
</SectionContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restController(SYSTEM_STATUS_ENDPOINT, SystemStatusController);
|
@ -1,18 +1,7 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { withSnackbar } from 'notistack';
|
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { Avatar, Button, Divider, Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core';
|
||||||
import Button from '@material-ui/core/Button';
|
import { List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||||
import List from '@material-ui/core/List';
|
|
||||||
import ListItem from '@material-ui/core/ListItem';
|
|
||||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
|
||||||
import Avatar from '@material-ui/core/Avatar';
|
|
||||||
import Divider from '@material-ui/core/Divider';
|
|
||||||
import Dialog from '@material-ui/core/Dialog';
|
|
||||||
import DialogActions from '@material-ui/core/DialogActions';
|
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
|
||||||
import DialogContent from '@material-ui/core/DialogContent';
|
|
||||||
|
|
||||||
import DevicesIcon from '@material-ui/icons/Devices';
|
import DevicesIcon from '@material-ui/icons/Devices';
|
||||||
import MemoryIcon from '@material-ui/icons/Memory';
|
import MemoryIcon from '@material-ui/icons/Memory';
|
||||||
@ -22,36 +11,28 @@ import DataUsageIcon from '@material-ui/icons/DataUsage';
|
|||||||
import AutorenewIcon from '@material-ui/icons/Autorenew';
|
import AutorenewIcon from '@material-ui/icons/Autorenew';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
import { SYSTEM_STATUS_ENDPOINT, RESTART_ENDPOINT } from '../constants/Endpoints';
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
import { restComponent } from '../components/RestComponent';
|
import { RestFormProps, FormButton, FormActions } from '../components';
|
||||||
import LoadingNotification from '../components/LoadingNotification';
|
import { RESTART_ENDPOINT } from '../api';
|
||||||
import SectionContent from '../components/SectionContent';
|
|
||||||
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
|
|
||||||
|
|
||||||
const styles = theme => ({
|
import { SystemStatus } from './types';
|
||||||
button: {
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class SystemStatus extends Component {
|
interface SystemStatusFormState {
|
||||||
|
confirmRestart: boolean;
|
||||||
|
processing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemStatusFormProps = RestFormProps<SystemStatus>;
|
||||||
|
|
||||||
constructor(props) {
|
class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusFormState> {
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
state: SystemStatusFormState = {
|
||||||
confirmRestart: false,
|
confirmRestart: false,
|
||||||
processing: false
|
processing: false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
createListItems() {
|
||||||
this.props.loadData();
|
const { data } = this.props
|
||||||
}
|
|
||||||
|
|
||||||
createListItems(data, classes) {
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem >
|
<ListItem >
|
||||||
@ -103,20 +84,26 @@ class SystemStatus extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSystemStatus(data, classes) {
|
renderRestartDialog() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Dialog
|
||||||
<List>
|
open={this.state.confirmRestart}
|
||||||
{this.createListItems(data, classes)}
|
onClose={this.onRestartRejected}
|
||||||
</List>
|
>
|
||||||
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
|
<DialogTitle>Confirm Restart</DialogTitle>
|
||||||
Refresh
|
<DialogContent dividers={true}>
|
||||||
</Button>
|
Are you sure you want to restart the device?
|
||||||
<Button startIcon={<AutorenewIcon />} variant="contained" color="secondary" className={classes.button} onClick={this.onRestart}>
|
</DialogContent>
|
||||||
Restart
|
<DialogActions>
|
||||||
</Button>
|
<Button startIcon={<AutorenewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
|
||||||
</div>
|
Restart
|
||||||
);
|
</Button>
|
||||||
|
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onRestart = () => {
|
onRestart = () => {
|
||||||
@ -144,45 +131,25 @@ class SystemStatus extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRestartDialog() {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={this.state.confirmRestart}
|
|
||||||
onClose={this.onRestartRejected}
|
|
||||||
>
|
|
||||||
<DialogTitle>Confirm Restart</DialogTitle>
|
|
||||||
<DialogContent dividers={true}>
|
|
||||||
Are you sure you want to restart the device?
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button startIcon={<AutorenewIcon />} variant="contained" onClick={this.onRestartConfirmed} disabled={this.state.processing} color="primary" autoFocus>
|
|
||||||
Restart
|
|
||||||
</Button>
|
|
||||||
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, fetched, errorMessage, loadData, classes } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title="System Status">
|
<Fragment>
|
||||||
<LoadingNotification
|
<List>
|
||||||
onRestart={loadData}
|
{this.createListItems()}
|
||||||
fetched={fetched}
|
</List>
|
||||||
errorMessage={errorMessage}
|
<FormActions>
|
||||||
render={
|
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||||
() => this.renderSystemStatus(data, classes)
|
Refresh
|
||||||
}
|
</FormButton>
|
||||||
/>
|
<FormButton startIcon={<AutorenewIcon />} variant="contained" color="primary" onClick={this.onRestart}>
|
||||||
|
Restart
|
||||||
|
</FormButton>
|
||||||
|
</FormActions>
|
||||||
{this.renderRestartDialog()}
|
{this.renderRestartDialog()}
|
||||||
</SectionContent>
|
</Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(restComponent(SYSTEM_STATUS_ENDPOINT, withStyles(styles)(SystemStatus)));
|
export default SystemStatusForm;
|
14
interface/src/system/types.ts
Normal file
14
interface/src/system/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export interface SystemStatus {
|
||||||
|
esp_platform: string;
|
||||||
|
cpu_freq_mhz: number;
|
||||||
|
free_heap: number;
|
||||||
|
sketch_size: number;
|
||||||
|
free_sketch_space: number;
|
||||||
|
flash_chip_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OTASettings {
|
||||||
|
enabled: boolean;
|
||||||
|
port: number;
|
||||||
|
password: string;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user