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
31
README.md
31
README.md
@ -213,21 +213,36 @@ The framework, and MaterialUI allows for a reasonable degree of customization wi
|
||||
|
||||
### 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
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
primary: red,
|
||||
secondary: deepOrange,
|
||||
highlight_idle: blueGrey[900],
|
||||
highlight_warn: orange[500],
|
||||
highlight_error: red[500],
|
||||
highlight_success: green[500],
|
||||
type:"dark",
|
||||
primary: {
|
||||
main: '#222',
|
||||
},
|
||||
secondary: {
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```cpp
|
||||
WiFiSettings wifiSettings = esp8266React->getWiFiSettingsService()->fetch();
|
||||
WiFiSettings wifiSettings = esp8266React.getWiFiSettingsService()->fetch();
|
||||
wifiSettings.ssid = "MyNetworkSSID";
|
||||
wifiSettings.password = "MySuperSecretPassword";
|
||||
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.
|
||||
# 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/
|
||||
|
7967
interface/package-lock.json
generated
7967
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,37 +3,51 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.7.0",
|
||||
"@material-ui/icons": "^4.5.1",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@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",
|
||||
"mime-types": "^2.1.25",
|
||||
"moment": "^2.24.0",
|
||||
"notistack": "^0.9.6",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.10.1",
|
||||
"react-dom": "^16.10.1",
|
||||
"notistack": "^0.9.7",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-form-validator-core": "^0.6.4",
|
||||
"react-jss": "^10.0.0",
|
||||
"react-material-ui-form-validator": "^2.0.9",
|
||||
"react-router": "^5.1.1",
|
||||
"react-router-dom": "^5.1.1",
|
||||
"react-scripts": "3.0.1",
|
||||
"react-material-ui-form-validator": "^2.0.10",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.3.1",
|
||||
"typescript": "^3.7.5",
|
||||
"zlib": "^1.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-app-rewired start",
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-app-rewired": "^2.1.3"
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": [
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"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 { Switch, Redirect } from 'react-router';
|
||||
|
||||
import { Redirect, Switch } from 'react-router';
|
||||
|
||||
import { PROJECT_PATH } from './constants/Env';
|
||||
import * as Authentication from './authentication/Authentication';
|
||||
import AuthenticationWrapper from './authentication/AuthenticationWrapper';
|
||||
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
|
||||
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
|
||||
import SignInPage from './containers/SignInPage';
|
||||
import WiFiConnection from './sections/WiFiConnection';
|
||||
import AccessPoint from './sections/AccessPoint';
|
||||
import NetworkTime from './sections/NetworkTime';
|
||||
import Security from './sections/Security';
|
||||
import System from './sections/System';
|
||||
import AuthenticatedRoute from './authentication/AuthenticatedRoute';
|
||||
|
||||
import SignIn from './SignIn';
|
||||
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 {
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
Authentication.clearLoginRedirect();
|
||||
}
|
||||
|
||||
@ -25,13 +26,13 @@ class AppRouting extends Component {
|
||||
return (
|
||||
<AuthenticationWrapper>
|
||||
<Switch>
|
||||
<UnauthenticatedRoute exact path="/" component={SignInPage} />
|
||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
||||
<AuthenticatedRoute exact path="/wifi/*" component={WiFiConnection} />
|
||||
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||
<AuthenticatedRoute exact path="/security/*" component={Security} />
|
||||
<AuthenticatedRoute exact path="/system/*" component={System} />
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
|
||||
<Redirect to="/" />
|
||||
</Switch>
|
||||
</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,18 +1,16 @@
|
||||
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 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 => {
|
||||
return {
|
||||
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
|
||||
import { Paper, Typography, Fab } from '@material-ui/core';
|
||||
import ForwardIcon from '@material-ui/icons/Forward';
|
||||
|
||||
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
||||
import {PasswordValidator} from './components';
|
||||
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
loginPage: {
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
@ -35,20 +33,23 @@ const styles = theme => {
|
||||
extendedIcon: {
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
textField: {
|
||||
width: "100%"
|
||||
},
|
||||
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) {
|
||||
constructor(props: SignInPageProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
@ -57,8 +58,12 @@ class SignInPage extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
handleValueChange = name => event => {
|
||||
this.setState({ [name]: event.target.value });
|
||||
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const { name, value } = event.currentTarget;
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
[name]: value,
|
||||
}))
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
@ -105,9 +110,10 @@ class SignInPage extends Component {
|
||||
errorMessages={['Username is required']}
|
||||
name="username"
|
||||
label="Username"
|
||||
className={classes.textField}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={username}
|
||||
onChange={this.handleValueChange('username')}
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
/>
|
||||
<PasswordValidator
|
||||
@ -116,9 +122,10 @@ class SignInPage extends Component {
|
||||
errorMessages={['Password is required']}
|
||||
name="password"
|
||||
label="Password"
|
||||
className={classes.textField}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={password}
|
||||
onChange={this.handleValueChange('password')}
|
||||
onChange={this.updateInputElement}
|
||||
margin="normal"
|
||||
/>
|
||||
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
||||
@ -133,6 +140,4 @@ class SignInPage extends Component {
|
||||
|
||||
}
|
||||
|
||||
export default withAuthenticationContext(
|
||||
withSnackbar(withStyles(styles)(SignInPage))
|
||||
);
|
||||
export default withAuthenticationContext(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_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 { PROJECT_PATH } from '../constants/Env';
|
||||
import { PROJECT_PATH } from '../api';
|
||||
|
||||
export const ACCESS_TOKEN = 'access_token';
|
||||
export const LOGIN_PATHNAME = 'loginPathname';
|
||||
export const LOGIN_SEARCH = 'loginSearch';
|
||||
|
||||
export function storeLoginRedirect(location) {
|
||||
export function storeLoginRedirect(location?: H.Location) {
|
||||
if (location) {
|
||||
localStorage.setItem(LOGIN_PATHNAME, location.pathname);
|
||||
localStorage.setItem(LOGIN_SEARCH, location.search);
|
||||
@ -17,7 +19,7 @@ export function clearLoginRedirect() {
|
||||
localStorage.removeItem(LOGIN_SEARCH);
|
||||
}
|
||||
|
||||
export function fetchLoginRedirect() {
|
||||
export function fetchLoginRedirect(): H.LocationDescriptorObject {
|
||||
const loginPathname = localStorage.getItem(LOGIN_PATHNAME);
|
||||
const loginSearch = localStorage.getItem(LOGIN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
@ -30,13 +32,15 @@ export function fetchLoginRedirect() {
|
||||
/**
|
||||
* 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);
|
||||
if (accessToken) {
|
||||
params = params || {};
|
||||
params.credentials = 'include';
|
||||
params.headers = params.headers || {};
|
||||
params.headers.Authorization = 'Bearer ' + accessToken;
|
||||
params.headers = {
|
||||
...params.headers,
|
||||
"Authorization": 'Bearer ' + accessToken
|
||||
};
|
||||
}
|
||||
return fetch(url, params);
|
||||
}
|
||||
@ -44,8 +48,8 @@ export function authorizedFetch(url, params) {
|
||||
/**
|
||||
* Wraps the normal fetch routene which redirects on 401 response.
|
||||
*/
|
||||
export function redirectingAuthorizedFetch(url, params) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
export function redirectingAuthorizedFetch(url: RequestInfo, params?: RequestInit): Promise<Response> {
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
authorizedFetch(url, params).then(response => {
|
||||
if (response.status === 401) {
|
||||
history.push("/unauthorized");
|
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 history from '../history'
|
||||
import { withSnackbar } from 'notistack';
|
||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../constants/Endpoints';
|
||||
import { ACCESS_TOKEN, authorizedFetch } from './Authentication';
|
||||
import { AuthenticationContext } from './Context';
|
||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
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: {
|
||||
padding: theme.spacing(2),
|
||||
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);
|
||||
this.state = {
|
||||
context: {
|
||||
refresh: this.refresh,
|
||||
signIn: this.signIn,
|
||||
signOut: this.signOut,
|
||||
isAuthenticated: this.isAuthenticated,
|
||||
isAdmin: this.isAdmin
|
||||
},
|
||||
initialized: false
|
||||
};
|
||||
@ -72,33 +81,31 @@ class AuthenticationWrapper extends React.Component {
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
var accessToken = localStorage.getItem(ACCESS_TOKEN);
|
||||
const accessToken = localStorage.getItem(ACCESS_TOKEN)
|
||||
if (accessToken) {
|
||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||
.then(response => {
|
||||
const user = response.status === 200 ? jwtDecode(accessToken) : undefined;
|
||||
this.setState({ initialized: true, context: { ...this.state.context, user } });
|
||||
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
||||
}).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, {
|
||||
variant: 'error',
|
||||
});
|
||||
});
|
||||
} 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 {
|
||||
localStorage.setItem(ACCESS_TOKEN, accessToken);
|
||||
const user = jwtDecode(accessToken);
|
||||
this.setState({ context: { ...this.state.context, user } });
|
||||
this.props.enqueueSnackbar(`Logged in as ${user.username}`, {
|
||||
variant: 'success',
|
||||
});
|
||||
const me: Me = decodeMeJWT(accessToken);
|
||||
this.setState({ context: { ...this.state.context, me } });
|
||||
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@ -108,24 +115,13 @@ class AuthenticationWrapper extends React.Component {
|
||||
this.setState({
|
||||
context: {
|
||||
...this.state.context,
|
||||
user: undefined
|
||||
me: undefined
|
||||
}
|
||||
});
|
||||
this.props.enqueueSnackbar("You have signed out.", {
|
||||
variant: 'success',
|
||||
});
|
||||
this.props.enqueueSnackbar("You have signed out.", { variant: 'success', });
|
||||
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))
|
@ -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 PropTypes from 'prop-types';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import React, { RefObject } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } 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 SettingsIcon from '@material-ui/icons/Settings';
|
||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
||||
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||
import LockIcon from '@material-ui/icons/Lock';
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||
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 MenuIcon from '@material-ui/icons/Menu';
|
||||
|
||||
import ProjectMenu from '../project/ProjectMenu';
|
||||
import { PROJECT_NAME } from '../constants/Env';
|
||||
import { withAuthenticationContext } from '../authentication/Context.js';
|
||||
import { PROJECT_NAME } from '../api';
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
|
||||
const drawerWidth = 290;
|
||||
|
||||
const styles = theme => ({
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
@ -77,26 +63,38 @@ const styles = theme => ({
|
||||
"& > * + *": {
|
||||
marginLeft: theme.spacing(2),
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
class MenuAppBar extends React.Component {
|
||||
state = {
|
||||
interface MenuAppBarState {
|
||||
mobileOpen: boolean;
|
||||
authMenuOpen: boolean;
|
||||
}
|
||||
|
||||
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 = React.createRef();
|
||||
anchorRef: RefObject<HTMLButtonElement> = React.createRef();
|
||||
|
||||
handleToggle = () => {
|
||||
this.setState({ authMenuOpen: !this.state.authMenuOpen });
|
||||
}
|
||||
|
||||
handleClose = (event) => {
|
||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.target)) {
|
||||
handleClose = (event: React.MouseEvent<Document>) => {
|
||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ authMenuOpen: false });
|
||||
}
|
||||
|
||||
@ -105,13 +103,13 @@ class MenuAppBar extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, theme, children, sectionTitle, authenticationContext } = this.props;
|
||||
const { classes, theme, children, sectionTitle, authenticatedContext } = this.props;
|
||||
const { mobileOpen, authMenuOpen } = this.state;
|
||||
const path = this.props.match.url;
|
||||
const drawer = (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" color="primary">
|
||||
<Typography variant="h6" color="textPrimary">
|
||||
{PROJECT_NAME}
|
||||
</Typography>
|
||||
<Divider absolute />
|
||||
@ -138,7 +136,7 @@ class MenuAppBar extends React.Component {
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Network Time" />
|
||||
</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>
|
||||
<LockIcon />
|
||||
</ListItemIcon>
|
||||
@ -156,7 +154,7 @@ class MenuAppBar extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="fixed" className={classes.appBar}>
|
||||
<AppBar position="fixed" className={classes.appBar} elevation={0}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
@ -191,13 +189,13 @@ class MenuAppBar extends React.Component {
|
||||
<AccountCircleIcon />
|
||||
</Avatar>
|
||||
</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>
|
||||
</List>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<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>
|
||||
</Card>
|
||||
</ClickAwayListener>
|
||||
@ -243,14 +241,10 @@ class MenuAppBar extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
MenuAppBar.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
sectionTitle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withAuthenticationContext(
|
||||
withRouter(
|
||||
withStyles(styles, { withTheme: true })(MenuAppBar)
|
||||
export default withRouter(
|
||||
withTheme(
|
||||
withAuthenticatedContext(
|
||||
withStyles(styles)(MenuAppBar)
|
||||
)
|
||||
)
|
||||
);
|
@ -1,21 +1,25 @@
|
||||
import React from 'react';
|
||||
import { TextValidator } 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';
|
||||
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
|
||||
|
||||
const styles = theme => (
|
||||
{
|
||||
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||
import { InputAdornment, IconButton } from '@material-ui/core';
|
||||
import {Visibility,VisibilityOff } from '@material-ui/icons';
|
||||
|
||||
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 = {
|
||||
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 MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
export const TIME_ZONES = {
|
||||
type TimeZones = {
|
||||
[name: string]: string
|
||||
};
|
||||
|
||||
export const TIME_ZONES: TimeZones = {
|
||||
"Africa/Abidjan": "GMT0",
|
||||
"Africa/Accra": "GMT0",
|
||||
"Africa/Addis_Ababa": "EAT-3",
|
||||
@ -464,7 +468,7 @@ export const TIME_ZONES = {
|
||||
"Etc/Zulu": "UTC0"
|
||||
}
|
||||
|
||||
export function selectedTimeZone(label, format){
|
||||
export function selectedTimeZone(label: string, format: string) {
|
||||
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,27 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
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)
|
||||
}
|
||||
});
|
||||
import { Typography, TableRow, TableBody, TableCell, TableHead, Table, Box } from '@material-ui/core';
|
||||
import { SectionContent } from '../components';
|
||||
|
||||
class DemoInformation extends Component {
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<SectionContent title="Demo Project - Blink Speed Controller" titleGutter>
|
||||
<SectionContent title='Demo Information' titleGutter>
|
||||
<Typography variant="body1" paragraph>
|
||||
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.
|
||||
@ -34,7 +19,7 @@ class DemoInformation extends Component {
|
||||
<Typography variant="body1" paragraph>
|
||||
The demo project interface code stored in the interface/project directory:
|
||||
</Typography>
|
||||
<Table className={classes.fileTable}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
@ -48,7 +33,7 @@ class DemoInformation extends Component {
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
ProjectMenu.js
|
||||
ProjectMenu.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
You can add your project's screens to the side bar here.
|
||||
@ -56,7 +41,7 @@ class DemoInformation extends Component {
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
ProjectRouting.js
|
||||
ProjectRouting.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
The routing which controls the screens of your project.
|
||||
@ -64,7 +49,7 @@ class DemoInformation extends Component {
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
DemoProject.js
|
||||
DemoProject.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
This screen, with tabs and tab routing.
|
||||
@ -72,15 +57,15 @@ class DemoInformation extends Component {
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
DemoInformation.js
|
||||
DemoInformation.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
The demo information tab.
|
||||
The demo information page.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
DemoController.js
|
||||
DemoController.tsx
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
The demo controller tab, to control the built-in LED.
|
||||
@ -88,13 +73,15 @@ class DemoInformation extends Component {
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Typography variant="body1" paragraph>
|
||||
<Box mt={2}>
|
||||
<Typography variant="body1">
|
||||
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.
|
||||
</Typography>
|
||||
</Box>
|
||||
</SectionContent>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withStyles(styles)(DemoInformation);
|
||||
export default DemoInformation;
|
@ -1,27 +1,27 @@
|
||||
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 DemoController from './DemoController';
|
||||
|
||||
import Tabs from '@material-ui/core/Tabs';
|
||||
import Tab from '@material-ui/core/Tab';
|
||||
class DemoProject extends Component<RouteComponentProps> {
|
||||
|
||||
class DemoProject extends Component {
|
||||
|
||||
handleTabChange = (event, path) => {
|
||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
||||
this.props.history.push(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MenuAppBar sectionTitle="Demo Project">
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
|
||||
<Tab value={`/${PROJECT_PATH}/demo/information`} label="Information" />
|
||||
<Tab value={`/${PROJECT_PATH}/demo/controller`} label="Controller" />
|
||||
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
|
||||
<Tab value={`/${PROJECT_PATH}/demo/information`} label="Demo Information" />
|
||||
<Tab value={`/${PROJECT_PATH}/demo/controller`} label="Demo Controller" />
|
||||
</Tabs>
|
||||
<Switch>
|
||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/information`} component={DemoInformation} />
|
@ -1,15 +1,12 @@
|
||||
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 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 {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
|
||||
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
|
||||
|
||||
class ProjectMenu extends Component {
|
||||
import { PROJECT_PATH } from '../api';
|
||||
|
||||
class ProjectMenu extends Component<RouteComponentProps> {
|
||||
|
||||
render() {
|
||||
const path = this.props.match.url;
|
@ -1,8 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Redirect, Switch } from 'react-router';
|
||||
|
||||
import { PROJECT_PATH } from '../constants/Env';
|
||||
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
|
||||
import { PROJECT_PATH } from '../api';
|
||||
import { AuthenticatedRoute } from '../authentication';
|
||||
|
||||
import DemoProject from './DemoProject';
|
||||
|
||||
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 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 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 { Table, TableBody, TableCell, TableHead, TableFooter, TableRow } from '@material-ui/core';
|
||||
import { Box, Button, Typography, } from '@material-ui/core';
|
||||
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
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 PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
import { RestFormProps, FormActions, FormButton } from '../components';
|
||||
|
||||
import UserForm from './UserForm';
|
||||
import { withAuthenticationContext } from '../authentication/Context';
|
||||
import { SecuritySettings, User } from './types';
|
||||
|
||||
const styles = theme => ({
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
table: {
|
||||
'& td, & th': { padding: theme.spacing(0.5) }
|
||||
}
|
||||
});
|
||||
|
||||
function compareUsers(a, b) {
|
||||
function compareUsers(a: User, b: User) {
|
||||
if (a.username < b.username) {
|
||||
return -1;
|
||||
}
|
||||
@ -44,12 +28,18 @@ function compareUsers(a, b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
class ManageUsersForm extends React.Component {
|
||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
type ManageUsersFormState = {
|
||||
creating: boolean;
|
||||
user?: User;
|
||||
}
|
||||
|
||||
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
||||
|
||||
state: ManageUsersFormState = {
|
||||
creating: false
|
||||
};
|
||||
|
||||
createUser = () => {
|
||||
this.setState({
|
||||
@ -62,21 +52,21 @@ class ManageUsersForm extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
uniqueUsername = username => {
|
||||
return !this.props.userData.users.find(u => u.username === username);
|
||||
uniqueUsername = (username: string) => {
|
||||
return !this.props.data.users.find(u => u.username === username);
|
||||
}
|
||||
|
||||
noAdminConfigured = () => {
|
||||
return !this.props.userData.users.find(u => u.admin);
|
||||
return !this.props.data.users.find(u => u.admin);
|
||||
}
|
||||
|
||||
removeUser = user => {
|
||||
const { userData } = this.props;
|
||||
const users = userData.users.filter(u => u.username !== user.username);
|
||||
this.props.setData({ ...userData, users });
|
||||
removeUser = (user: User) => {
|
||||
const { data } = this.props;
|
||||
const users = data.users.filter(u => u.username !== user.username);
|
||||
this.props.setData({ ...data, users });
|
||||
}
|
||||
|
||||
startEditingUser = user => {
|
||||
startEditingUser = (user: User) => {
|
||||
this.setState({
|
||||
creating: false,
|
||||
user
|
||||
@ -91,45 +81,37 @@ class ManageUsersForm extends React.Component {
|
||||
|
||||
doneEditingUser = () => {
|
||||
const { user } = this.state;
|
||||
const { userData } = this.props;
|
||||
const users = userData.users.filter(u => u.username !== user.username);
|
||||
if (user) {
|
||||
const { data } = this.props;
|
||||
const users = data.users.filter(u => u.username !== user.username);
|
||||
users.push(user);
|
||||
this.props.setData({ ...userData, users });
|
||||
this.props.setData({ ...data, users });
|
||||
this.setState({
|
||||
user: undefined
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleUserValueChange = name => event => {
|
||||
const { user } = this.state;
|
||||
this.setState({
|
||||
user: {
|
||||
...user, [name]: event.target.value
|
||||
}
|
||||
});
|
||||
handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ user: { ...this.state.user!, [name]: event.target.value } });
|
||||
};
|
||||
|
||||
handleUserCheckboxChange = name => event => {
|
||||
const { user } = this.state;
|
||||
this.setState({
|
||||
user: {
|
||||
...user, [name]: event.target.checked
|
||||
}
|
||||
});
|
||||
handleUserCheckboxChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ user: { ...this.state.user!, [name]: event.target.checked } });
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.onSubmit();
|
||||
this.props.authenticationContext.refresh();
|
||||
this.props.saveData();
|
||||
this.props.authenticatedContext.refresh();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, userData, onReset } = this.props;
|
||||
const { data, loadData } = this.props;
|
||||
const { user, creating } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<ValidatorForm onSubmit={this.onSubmit}>
|
||||
<Table className={classes.table}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Username</TableCell>
|
||||
@ -138,7 +120,7 @@ class ManageUsersForm extends React.Component {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{userData.users.sort(compareUsers).map(user => (
|
||||
{data.users.sort(compareUsers).map(user => (
|
||||
<TableRow key={user.username}>
|
||||
<TableCell component="th" scope="row">
|
||||
{user.username}
|
||||
@ -178,12 +160,14 @@ class ManageUsersForm extends React.Component {
|
||||
</Box>
|
||||
</Typography>
|
||||
}
|
||||
<Button startIcon={<SaveIcon />} variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
||||
</FormButton>
|
||||
<FormButton variant="contained" color="secondary" onClick={loadData}>
|
||||
Reset
|
||||
</Button>
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
</ValidatorForm>
|
||||
{
|
||||
user &&
|
||||
@ -203,14 +187,4 @@ class ManageUsersForm extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
ManageUsersForm.propTypes = {
|
||||
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));
|
||||
export default withAuthenticatedContext(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 { withSnackbar } from 'notistack';
|
||||
|
||||
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 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 { Avatar, Button, Divider, Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core';
|
||||
import { List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
|
||||
|
||||
import DevicesIcon from '@material-ui/icons/Devices';
|
||||
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 RefreshIcon from '@material-ui/icons/Refresh';
|
||||
|
||||
import { SYSTEM_STATUS_ENDPOINT, RESTART_ENDPOINT } from '../constants/Endpoints';
|
||||
import { restComponent } from '../components/RestComponent';
|
||||
import LoadingNotification from '../components/LoadingNotification';
|
||||
import SectionContent from '../components/SectionContent';
|
||||
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
import { RestFormProps, FormButton, FormActions } from '../components';
|
||||
import { RESTART_ENDPOINT } from '../api';
|
||||
|
||||
const styles = theme => ({
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
}
|
||||
});
|
||||
import { SystemStatus } from './types';
|
||||
|
||||
class SystemStatus extends Component {
|
||||
interface SystemStatusFormState {
|
||||
confirmRestart: boolean;
|
||||
processing: boolean;
|
||||
}
|
||||
|
||||
type SystemStatusFormProps = RestFormProps<SystemStatus>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusFormState> {
|
||||
|
||||
this.state = {
|
||||
state: SystemStatusFormState = {
|
||||
confirmRestart: false,
|
||||
processing: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadData();
|
||||
}
|
||||
|
||||
createListItems(data, classes) {
|
||||
createListItems() {
|
||||
const { data } = this.props
|
||||
return (
|
||||
<Fragment>
|
||||
<ListItem >
|
||||
@ -103,20 +84,26 @@ class SystemStatus extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderSystemStatus(data, classes) {
|
||||
renderRestartDialog() {
|
||||
return (
|
||||
<div>
|
||||
<List>
|
||||
{this.createListItems(data, classes)}
|
||||
</List>
|
||||
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button startIcon={<AutorenewIcon />} variant="contained" color="secondary" className={classes.button} onClick={this.onRestart}>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
<Button variant="contained" onClick={this.onRestartRejected} color="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
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() {
|
||||
const { data, fetched, errorMessage, loadData, classes } = this.props;
|
||||
return (
|
||||
<SectionContent title="System Status">
|
||||
<LoadingNotification
|
||||
onRestart={loadData}
|
||||
fetched={fetched}
|
||||
errorMessage={errorMessage}
|
||||
render={
|
||||
() => this.renderSystemStatus(data, classes)
|
||||
}
|
||||
/>
|
||||
<Fragment>
|
||||
<List>
|
||||
{this.createListItems()}
|
||||
</List>
|
||||
<FormActions>
|
||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
||||
Refresh
|
||||
</FormButton>
|
||||
<FormButton startIcon={<AutorenewIcon />} variant="contained" color="primary" onClick={this.onRestart}>
|
||||
Restart
|
||||
</FormButton>
|
||||
</FormActions>
|
||||
{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