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