initial commit of C++ back end and react front end
This commit is contained in:
		
							
								
								
									
										29
									
								
								interface/config-overrides.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								interface/config-overrides.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| const CompressionPlugin = require("compression-webpack-plugin"); | ||||
| const ManifestPlugin = require('webpack-manifest-plugin'); | ||||
| const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); | ||||
|  | ||||
| const path = require('path'); | ||||
| const fs = require('fs'); | ||||
|  | ||||
| module.exports = function override(config, env) { | ||||
|   if (env === "production") { | ||||
|     // rename the ouput file, we need it's path to be short, for SPIFFS | ||||
|     config.output.filename = 'js/[name].[chunkhash:4].js'; | ||||
|      | ||||
|     // disable sourcemap for production build | ||||
|     config.devtool = false; | ||||
|  | ||||
|     // take out the manifest and service worker | ||||
|     config.plugins = config.plugins.filter(plugin => !(plugin instanceof ManifestPlugin)); | ||||
|     config.plugins = config.plugins.filter(plugin => !(plugin instanceof SWPrecacheWebpackPlugin)); | ||||
|  | ||||
|     // add compression plugin, compress javascript, html and css | ||||
|     config.plugins.push(new CompressionPlugin({ | ||||
|       asset: "[path].gz[query]", | ||||
|       algorithm: "gzip", | ||||
|       test: /\.(js|html|css)$/, | ||||
|       deleteOriginalAssets: true | ||||
|     })); | ||||
|   } | ||||
|   return config; | ||||
| } | ||||
							
								
								
									
										10960
									
								
								interface/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10960
									
								
								interface/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										29
									
								
								interface/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								interface/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| { | ||||
|   "name": "fresh", | ||||
|   "version": "0.1.0", | ||||
|   "private": true, | ||||
|   "dependencies": { | ||||
|     "compression-webpack-plugin": "^1.1.8", | ||||
|     "material-ui": "^1.0.0-beta.32", | ||||
|     "material-ui-icons": "^1.0.0-beta.17", | ||||
|     "moment": "^2.20.1", | ||||
|     "prop-types": "^15.6.0", | ||||
|     "react": "^16.2.0", | ||||
|     "react-autosuggest": "^9.3.3", | ||||
|     "react-dom": "^16.2.0", | ||||
|     "react-form-validator-core": "^0.3.0", | ||||
|     "react-material-ui-form-validator": "^2.0.0-beta.4", | ||||
|     "react-router": "^4.2.0", | ||||
|     "react-router-dom": "^4.2.2", | ||||
|     "react-scripts": "1.0.17" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "react-app-rewired start", | ||||
|     "build": "react-app-rewired build && rm -rf ../data/www && cp -r build ../data/www", | ||||
|     "test": "react-app-rewired test --env=jsdom", | ||||
|     "eject": "react-scripts eject" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "react-app-rewired": "^1.4.1" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								interface/public/app/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/app/icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.7 KiB | 
							
								
								
									
										12
									
								
								interface/public/app/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								interface/public/app/manifest.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| { | ||||
|   "name":"ESP8266 React", | ||||
|   "icons":[ | ||||
|     { | ||||
|       "src":"/app/icon.png", | ||||
|       "sizes":"48x48 72x72 96x96 128x128 256x256" | ||||
|     } | ||||
|   ], | ||||
|   "start_url":"/", | ||||
|   "display":"fullscreen", | ||||
|   "orientation":"any" | ||||
| } | ||||
							
								
								
									
										22
									
								
								interface/public/css/roboto.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								interface/public/css/roboto.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| /* Just supporting latin due to size constrains on the esp chip */ | ||||
| @font-face { | ||||
|   font-family: 'Roboto'; | ||||
|   font-style: normal; | ||||
|   font-weight: 300; | ||||
|   src: local('Roboto Light'), local('Roboto-Light'), url(../fonts/ro-li.w2) format('woff2'); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Roboto'; | ||||
|   font-style: normal; | ||||
|   font-weight: 400; | ||||
|   src: local('Roboto'), local('Roboto-Regular'), url(../fonts/ro-re.w2) format('woff2'); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215; | ||||
| } | ||||
| @font-face { | ||||
|   font-family: 'Roboto'; | ||||
|   font-style: normal; | ||||
|   font-weight: 500; | ||||
|   src: local('Roboto Medium'), local('Roboto-Medium'), url(../fonts/ro-me.w2) format('woff2'); | ||||
|   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2212, U+2215; | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								interface/public/fonts/ro-li.w2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/fonts/ro-li.w2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								interface/public/fonts/ro-me.w2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/fonts/ro-me.w2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								interface/public/fonts/ro-re.w2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								interface/public/fonts/ro-re.w2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										16
									
								
								interface/public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								interface/public/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|     <link rel="stylesheet" href="%PUBLIC_URL%/css/roboto.css"> | ||||
|     <link rel="manifest" href="%PUBLIC_URL%/app/manifest.json"> | ||||
|     <title>ESP8266 React</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <noscript> | ||||
|       You need to enable JavaScript to run this app. | ||||
|     </noscript> | ||||
|     <div id="root"></div> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										54
									
								
								interface/src/App.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								interface/src/App.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import AppRouting from './AppRouting'; | ||||
|  | ||||
| import JssProvider from 'react-jss/lib/JssProvider'; | ||||
| import { create } from 'jss'; | ||||
|  | ||||
| import Reboot from 'material-ui/Reboot'; | ||||
|  | ||||
| import blueGrey from 'material-ui/colors/blueGrey'; | ||||
| import indigo from 'material-ui/colors/indigo'; | ||||
| import orange from 'material-ui/colors/orange'; | ||||
| import red from 'material-ui/colors/red'; | ||||
| import green from 'material-ui/colors/green'; | ||||
|  | ||||
| import { | ||||
|   MuiThemeProvider, | ||||
|   createMuiTheme, | ||||
|   createGenerateClassName, | ||||
|   jssPreset, | ||||
| } from 'material-ui/styles'; | ||||
|  | ||||
| // 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()); | ||||
|  | ||||
| // Class name generator. | ||||
| const generateClassName = createGenerateClassName(); | ||||
|  | ||||
| class App extends Component { | ||||
| 	render() { | ||||
| 	   return ( | ||||
| 		 <JssProvider jss={jss} generateClassName={generateClassName}> | ||||
| 			<MuiThemeProvider theme={theme}> | ||||
| 				<Reboot /> | ||||
|         <AppRouting /> | ||||
| 			</MuiThemeProvider> | ||||
| 		 </JssProvider> | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export default App | ||||
							
								
								
									
										25
									
								
								interface/src/AppRouting.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								interface/src/AppRouting.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { Route, Redirect, Switch } from 'react-router'; | ||||
|  | ||||
| // containers | ||||
| import WiFiConfiguration from './containers/WiFiConfiguration'; | ||||
| import NTPConfiguration from './containers/NTPConfiguration'; | ||||
| import OTAConfiguration from './containers/OTAConfiguration'; | ||||
| import APConfiguration from './containers/APConfiguration'; | ||||
|  | ||||
| class AppRouting extends Component { | ||||
| 	render() { | ||||
| 	   return ( | ||||
|        <Switch> | ||||
|          <Route exact path="/wifi-configuration" component={WiFiConfiguration} /> | ||||
| 				 <Route exact path="/ap-configuration" component={APConfiguration} /> | ||||
| 				 <Route exact path="/ntp-configuration" component={NTPConfiguration} /> | ||||
| 				 <Route exact path="/ota-configuration" component={OTAConfiguration} /> | ||||
|          <Redirect to="/wifi-configuration" /> | ||||
|        </Switch> | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| export default AppRouting; | ||||
							
								
								
									
										189
									
								
								interface/src/components/MenuAppBar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								interface/src/components/MenuAppBar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Drawer from 'material-ui/Drawer'; | ||||
| import AppBar from 'material-ui/AppBar'; | ||||
| import Toolbar from 'material-ui/Toolbar'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
| import IconButton from 'material-ui/IconButton'; | ||||
| import Hidden from 'material-ui/Hidden'; | ||||
| import Divider from 'material-ui/Divider'; | ||||
| import {  Link } from 'react-router-dom'; | ||||
| import List, { ListItem, ListItemIcon, ListItemText } from 'material-ui/List'; | ||||
|  | ||||
| import MenuIcon from 'material-ui-icons/Menu'; | ||||
| import WifiIcon from 'material-ui-icons/Wifi'; | ||||
| import SystemUpdateIcon from  'material-ui-icons/SystemUpdate'; | ||||
| import AccessTimeIcon from 'material-ui-icons/AccessTime'; | ||||
| import SettingsInputAntennaIcon from 'material-ui-icons/SettingsInputAntenna'; | ||||
|  | ||||
| const drawerWidth = 250; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   root: { | ||||
|     zIndex: 1, | ||||
|     width: '100%', | ||||
|     height: '100%', | ||||
|   }, | ||||
|   toolbar: { | ||||
|     paddingLeft: theme.spacing.unit, | ||||
|     paddingRight:  theme.spacing.unit, | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|       paddingLeft: theme.spacing.unit * 3, | ||||
|       paddingRight: theme.spacing.unit  * 3, | ||||
|     } | ||||
|   }, | ||||
|   appFrame: { | ||||
|     position: 'relative', | ||||
|     display: 'flex', | ||||
|     width: '100%', | ||||
|     height: '100%', | ||||
|   }, | ||||
|   appBar: { | ||||
|     position: 'absolute', | ||||
|     marginLeft: drawerWidth, | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|       width: `calc(100% - ${drawerWidth}px)`, | ||||
|     }, | ||||
|   }, | ||||
|   navIconHide: { | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|       display: 'none', | ||||
|     }, | ||||
|   }, | ||||
|   drawerPaper: { | ||||
|     width: drawerWidth, | ||||
|     height: '100%', | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|       width: drawerWidth, | ||||
|       position:'fixed', | ||||
|       left:0, | ||||
|       top:0, | ||||
|       overflow:'auto' | ||||
|     }, | ||||
|   }, | ||||
|   content: { | ||||
|     backgroundColor: theme.palette.background.default, | ||||
|     width:"100%", | ||||
|     marginTop: 56, | ||||
|     [theme.breakpoints.up('md')]: { | ||||
|       paddingLeft: drawerWidth | ||||
|     }, | ||||
|     [theme.breakpoints.up('sm')]: { | ||||
|       height: 'calc(100% - 64px)', | ||||
|       marginTop: 64, | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| class MenuAppBar extends React.Component { | ||||
|   state = { | ||||
|     mobileOpen: false, | ||||
|   }; | ||||
|  | ||||
|   handleDrawerToggle = () => { | ||||
|     this.setState({ mobileOpen: !this.state.mobileOpen }); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { classes, theme, children, sectionTitle } = this.props; | ||||
|  | ||||
|     const drawer = ( | ||||
|       <div> | ||||
|         <Toolbar> | ||||
|             <Typography variant="title" color="primary"> | ||||
|               ESP8266 React | ||||
|             </Typography> | ||||
|           <Divider absolute /> | ||||
|         </Toolbar> | ||||
|         <Divider /> | ||||
|         <List> | ||||
|           <ListItem button component={Link} to='/wifi-configuration'> | ||||
|             <ListItemIcon> | ||||
|               <WifiIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText primary="WiFi Configuration" /> | ||||
|           </ListItem> | ||||
|           <ListItem button component={Link} to='/ap-configuration'> | ||||
|             <ListItemIcon> | ||||
|               <SettingsInputAntennaIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText primary="AP Configuration" /> | ||||
|           </ListItem> | ||||
|           <ListItem button component={Link} to='/ntp-configuration'> | ||||
|             <ListItemIcon> | ||||
|               <AccessTimeIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText primary="NTP Configuration" /> | ||||
|           </ListItem> | ||||
|           <ListItem button component={Link} to='/ota-configuration'> | ||||
|             <ListItemIcon> | ||||
|               <SystemUpdateIcon /> | ||||
|             </ListItemIcon> | ||||
|             <ListItemText primary="OTA Configuration" /> | ||||
|           </ListItem> | ||||
|         </List> | ||||
|       </div> | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|       <div className={classes.root}> | ||||
|         <div className={classes.appFrame}> | ||||
|           <AppBar className={classes.appBar}> | ||||
|             <Toolbar className={classes.toolbar} disableGutters={true}> | ||||
|               <IconButton | ||||
|                 color="inherit" | ||||
|                 aria-label="open drawer" | ||||
|                 onClick={this.handleDrawerToggle} | ||||
|                 className={classes.navIconHide} | ||||
|               > | ||||
|                 <MenuIcon /> | ||||
|               </IconButton> | ||||
|               <Typography variant="title" color="inherit" noWrap> | ||||
|                 {sectionTitle} | ||||
|               </Typography> | ||||
|             </Toolbar> | ||||
|           </AppBar> | ||||
|           <Hidden mdUp> | ||||
|             <Drawer | ||||
|               variant="temporary" | ||||
|               anchor={theme.direction === 'rtl' ? 'right' : 'left'} | ||||
|               open={this.state.mobileOpen} | ||||
|               classes={{ | ||||
|                 paper: classes.drawerPaper, | ||||
|               }} | ||||
|               onClose={this.handleDrawerToggle} | ||||
|               ModalProps={{ | ||||
|                 keepMounted: true, // Better open performance on mobile. | ||||
|               }} | ||||
|             > | ||||
|               {drawer} | ||||
|             </Drawer> | ||||
|           </Hidden> | ||||
|           <Hidden smDown implementation="css"> | ||||
|             <Drawer | ||||
|               variant="permanent" | ||||
|               open | ||||
|               classes={{ | ||||
|                 paper: classes.drawerPaper, | ||||
|               }} | ||||
|             > | ||||
|               {drawer} | ||||
|             </Drawer> | ||||
|           </Hidden> | ||||
|           <main className={classes.content}> | ||||
|             {children} | ||||
|           </main> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| MenuAppBar.propTypes = { | ||||
|   classes: PropTypes.object.isRequired, | ||||
|   theme: PropTypes.object.isRequired, | ||||
|   sectionTitle: PropTypes.string.isRequired, | ||||
| }; | ||||
|  | ||||
| export default withStyles(styles, { withTheme: true })(MenuAppBar); | ||||
							
								
								
									
										36
									
								
								interface/src/components/SectionContent.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								interface/src/components/SectionContent.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import Paper from 'material-ui/Paper'; | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   content: { | ||||
|     padding: theme.spacing.unit * 2, | ||||
|     margin: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| function SectionContent(props) { | ||||
|   const { children, classes, title } = props; | ||||
|   return ( | ||||
|       <Paper className={classes.content}> | ||||
|         <Typography variant="display1"> | ||||
|           {title} | ||||
|         </Typography> | ||||
|         {children} | ||||
|       </Paper> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| SectionContent.propTypes = { | ||||
|   classes: PropTypes.object.isRequired, | ||||
| 	children: PropTypes.oneOfType([ | ||||
|         PropTypes.arrayOf(PropTypes.node), | ||||
|         PropTypes.node | ||||
|     ]).isRequired, | ||||
|   title: PropTypes.string.isRequired | ||||
| }; | ||||
|  | ||||
| export default withStyles(styles)(SectionContent); | ||||
							
								
								
									
										74
									
								
								interface/src/components/SnackbarNotification.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								interface/src/components/SnackbarNotification.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Snackbar from 'material-ui/Snackbar'; | ||||
| import IconButton from 'material-ui/IconButton'; | ||||
| import CloseIcon from 'material-ui-icons/Close'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   close: { | ||||
|     width: theme.spacing.unit * 4, | ||||
|     height: theme.spacing.unit * 4, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| class SnackbarNotification extends React.Component { | ||||
|   state = { | ||||
|     open: false, | ||||
|     message: null | ||||
|   }; | ||||
|  | ||||
|   raiseNotification = (message) => { | ||||
|     this.setState({ open: true, message:message }); | ||||
|   }; | ||||
|  | ||||
|   handleClose = (event, reason) => { | ||||
|     if (reason === 'clickaway') { | ||||
|       return; | ||||
|     } | ||||
|     this.setState({ open: false }); | ||||
|   }; | ||||
|  | ||||
|   componentWillReceiveProps(nextProps){ | ||||
|     if (nextProps.notificationRef){ | ||||
|       nextProps.notificationRef(this.raiseNotification); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { classes } = this.props; | ||||
|     return ( | ||||
|         <Snackbar | ||||
|           anchorOrigin={{ | ||||
|             vertical: 'bottom', | ||||
|             horizontal: 'left', | ||||
|           }} | ||||
|           open={this.state.open} | ||||
|           autoHideDuration={6000} | ||||
|           onClose={this.handleClose} | ||||
|           SnackbarContentProps={{ | ||||
|             'aria-describedby': 'message-id', | ||||
|           }} | ||||
|           message={<span id="message-id">{this.state.message}</span>} | ||||
|           action={ | ||||
|             <IconButton | ||||
|               key="close" | ||||
|               aria-label="Close" | ||||
|               color="inherit" | ||||
|               className={classes.close} | ||||
|               onClick={this.handleClose} | ||||
|             > | ||||
|               <CloseIcon /> | ||||
|             </IconButton> | ||||
|           } | ||||
|         /> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| SnackbarNotification.propTypes = { | ||||
|   classes: PropTypes.object.isRequired, | ||||
|   notificationRef: PropTypes.func.isRequired, | ||||
| }; | ||||
|  | ||||
| export default withStyles(styles)(SnackbarNotification); | ||||
							
								
								
									
										28
									
								
								interface/src/constants/Endpoints.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								interface/src/constants/Endpoints.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| export const ENDPOINT_ROOT = ""; | ||||
|  | ||||
| export const NTP_STATUS_PATH = "/ntpStatus"; | ||||
| export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + NTP_STATUS_PATH; | ||||
|  | ||||
| export const NTP_SETTINGS_PATH = "/ntpSettings"; | ||||
| export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + NTP_SETTINGS_PATH; | ||||
|  | ||||
| export const AP_SETTINGS_PATH = "/apSettings"; | ||||
| export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + AP_SETTINGS_PATH; | ||||
|  | ||||
| export const AP_STATUS_PATH = "/apStatus"; | ||||
| export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + AP_STATUS_PATH; | ||||
|  | ||||
| export const SCAN_NETWORKS_PATH = "/scanNetworks"; | ||||
| export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + SCAN_NETWORKS_PATH; | ||||
|  | ||||
| export const LIST_NETWORKS_PATH = "/listNetworks"; | ||||
| export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + LIST_NETWORKS_PATH; | ||||
|  | ||||
| export const WIFI_SETTINGS_PATH = "/wifiSettings"; | ||||
| export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + WIFI_SETTINGS_PATH; | ||||
|  | ||||
| export const WIFI_STATUS_PATH = "/wifiStatus"; | ||||
| export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + WIFI_STATUS_PATH; | ||||
|  | ||||
| export const OTA_SETTINGS_PATH = "/otaSettings"; | ||||
| export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + OTA_SETTINGS_PATH; | ||||
							
								
								
									
										4
									
								
								interface/src/constants/Highlight.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								interface/src/constants/Highlight.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| export const IDLE = "idle"; | ||||
| export const SUCCESS = "success"; | ||||
| export const ERROR = "error"; | ||||
| export const WARN = "warn"; | ||||
							
								
								
									
										32
									
								
								interface/src/constants/NTPStatus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								interface/src/constants/NTPStatus.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import * as Highlight from '../constants/Highlight'; | ||||
|  | ||||
| export const NTP_TIME_NOT_SET = 0; | ||||
| export const NTP_TIME_NEEDS_SYNC = 1; | ||||
| export const NTP_TIME_SET = 2; | ||||
|  | ||||
| export const isSynchronized = ntpStatus => ntpStatus && (ntpStatus.status === NTP_TIME_NEEDS_SYNC || ntpStatus.status === NTP_TIME_SET); | ||||
|  | ||||
| export const ntpStatusHighlight = ntpStatus => { | ||||
|   switch (ntpStatus.status){ | ||||
|     case NTP_TIME_SET: | ||||
|       return Highlight.SUCCESS; | ||||
|     case NTP_TIME_NEEDS_SYNC: | ||||
|       return Highlight.WARN; | ||||
|     case NTP_TIME_NOT_SET: | ||||
|     default: | ||||
|       return Highlight.ERROR; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const ntpStatus = ntpStatus => { | ||||
|   switch (ntpStatus.status){ | ||||
|     case NTP_TIME_SET: | ||||
|       return "Synchronized"; | ||||
|     case NTP_TIME_NEEDS_SYNC: | ||||
|       return "Synchronization required"; | ||||
|     case NTP_TIME_NOT_SET: | ||||
|       return "Time not set" | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										4
									
								
								interface/src/constants/TimeFormat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								interface/src/constants/TimeFormat.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| import moment from 'moment'; | ||||
|  | ||||
| export const TIME_AND_DATE = 'DD/MM/YYYY HH:mm:ss'; | ||||
| export const unixTimeToTimeAndDate = unixTime => moment.unix(unixTime).format(TIME_AND_DATE); | ||||
							
								
								
									
										5
									
								
								interface/src/constants/WiFiAPModes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								interface/src/constants/WiFiAPModes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| 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; | ||||
							
								
								
									
										44
									
								
								interface/src/constants/WiFiConnectionStatus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								interface/src/constants/WiFiConnectionStatus.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| import * as Highlight from '../constants/Highlight'; | ||||
|  | ||||
| export const WIFI_STATUS_IDLE = 0; | ||||
| export const WIFI_STATUS_NO_SSID_AVAIL = 1; | ||||
| export const WIFI_STATUS_CONNECTED = 3; | ||||
| export const WIFI_STATUS_CONNECT_FAILED = 4; | ||||
| export const WIFI_STATUS_CONNECTION_LOST = 5; | ||||
| export const WIFI_STATUS_DISCONNECTED = 6; | ||||
|  | ||||
| export const isConnected = wifiStatus => wifiStatus && wifiStatus.status === WIFI_STATUS_CONNECTED; | ||||
|  | ||||
| export const connectionStatusHighlight = wifiStatus => { | ||||
|   switch (wifiStatus.status){ | ||||
|     case WIFI_STATUS_IDLE: | ||||
|     case WIFI_STATUS_DISCONNECTED: | ||||
|       return Highlight.IDLE; | ||||
|     case WIFI_STATUS_CONNECTED: | ||||
|       return Highlight.SUCCESS; | ||||
|     case WIFI_STATUS_CONNECT_FAILED: | ||||
|     case WIFI_STATUS_CONNECTION_LOST: | ||||
|       return Highlight.ERROR; | ||||
|     default: | ||||
|       return Highlight.WARN; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const connectionStatus = wifiStatus => { | ||||
|   switch (wifiStatus.status){ | ||||
|     case WIFI_STATUS_IDLE: | ||||
|       return "Idle"; | ||||
|     case WIFI_STATUS_NO_SSID_AVAIL: | ||||
|       return "No SSID Available"; | ||||
|     case WIFI_STATUS_CONNECTED: | ||||
|       return "Connected"; | ||||
|     case WIFI_STATUS_CONNECT_FAILED: | ||||
|       return "Connection Failed"; | ||||
|     case WIFI_STATUS_CONNECTION_LOST: | ||||
|       return "Connection Lost"; | ||||
|     case WIFI_STATUS_DISCONNECTED: | ||||
|       return "Disconnected"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										23
									
								
								interface/src/constants/WiFiSecurityModes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								interface/src/constants/WiFiSecurityModes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| export const WIFI_SECURITY_WPA_TKIP = 2; | ||||
| export const WIFI_SECURITY_WEP = 5; | ||||
| export const WIFI_SECURITY_WPA_CCMP = 4; | ||||
| export const WIFI_SECURITY_NONE = 7; | ||||
| export const WIFI_SECURITY_AUTO = 8; | ||||
|  | ||||
| export const isNetworkOpen = selectedNetwork => selectedNetwork && selectedNetwork.encryption_type === WIFI_SECURITY_NONE; | ||||
|  | ||||
| export const networkSecurityMode = selectedNetwork => { | ||||
|   switch (selectedNetwork.encryption_type){ | ||||
|     case WIFI_SECURITY_WPA_TKIP: | ||||
|     case WIFI_SECURITY_WPA_CCMP: | ||||
|       return "WPA"; | ||||
|     case WIFI_SECURITY_WEP: | ||||
|       return "WEP"; | ||||
|     case WIFI_SECURITY_AUTO: | ||||
|       return "Auto"; | ||||
|     case WIFI_SECURITY_NONE: | ||||
|       return "None"; | ||||
|     default: | ||||
|       return "Unknown"; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										48
									
								
								interface/src/containers/APConfiguration.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								interface/src/containers/APConfiguration.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import Tabs, { Tab } from 'material-ui/Tabs'; | ||||
|  | ||||
| import MenuAppBar from '../components/MenuAppBar'; | ||||
| import APSettings from './APSettings'; | ||||
| import APStatus from './APStatus'; | ||||
|  | ||||
| class APConfiguration extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|         selectedTab: "apStatus", | ||||
|         selectedNetwork: null | ||||
|     }; | ||||
|     this.selectNetwork = this.selectNetwork.bind(this); | ||||
|     this.deselectNetwork = this.deselectNetwork.bind(this); | ||||
|   } | ||||
|  | ||||
|   selectNetwork(network) { | ||||
|     this.setState({ selectedTab: "wifiSettings", selectedNetwork:network }); | ||||
|   } | ||||
|  | ||||
|   deselectNetwork(network) { | ||||
|     this.setState({ selectedNetwork:null }); | ||||
|   } | ||||
|  | ||||
|   handleTabChange = (event, selectedTab) => { | ||||
|     this.setState({ selectedTab }); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { selectedTab } = this.state; | ||||
|     return ( | ||||
|       <MenuAppBar sectionTitle="AP Configuration"> | ||||
|         <Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" fullWidth centered scrollable> | ||||
|            <Tab value="apStatus" label="AP Status" /> | ||||
|            <Tab value="apSettings" label="AP Settings" /> | ||||
|          </Tabs> | ||||
|          {selectedTab === "apStatus" && <APStatus fullDetails={true} />} | ||||
|          {selectedTab === "apSettings" && <APSettings />} | ||||
|       </MenuAppBar> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default APConfiguration; | ||||
							
								
								
									
										70
									
								
								interface/src/containers/APSettings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								interface/src/containers/APSettings.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { AP_SETTINGS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
| import SectionContent from '../components/SectionContent'; | ||||
| import SnackbarNotification from '../components/SnackbarNotification'; | ||||
| import APSettingsForm from '../forms/APSettingsForm'; | ||||
| import { simpleGet } from  '../helpers/SimpleGet'; | ||||
| import { simplePost } from '../helpers/SimplePost'; | ||||
|  | ||||
| class APSettings extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.state = { | ||||
|              apSettings:null, | ||||
|              apSettingsFetched: false, | ||||
|              errorMessage:null | ||||
|            }; | ||||
|  | ||||
|     this.setState = this.setState.bind(this); | ||||
|     this.loadAPSettings = this.loadAPSettings.bind(this); | ||||
|     this.saveAPSettings = this.saveAPSettings.bind(this); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|     this.loadAPSettings(); | ||||
|   } | ||||
|  | ||||
|   loadAPSettings() { | ||||
|     simpleGet( | ||||
|       AP_SETTINGS_ENDPOINT, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "apSettings", | ||||
|       "apSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   saveAPSettings(e) { | ||||
|     simplePost( | ||||
|       AP_SETTINGS_ENDPOINT, | ||||
|       this.state, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "apSettings", | ||||
|       "apSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   wifiSettingValueChange = name => event => { | ||||
|     const { apSettings } = this.state; | ||||
|     apSettings[name] = event.target.value; | ||||
|     this.setState({apSettings}); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { apSettingsFetched, apSettings, errorMessage } = this.state; | ||||
|     return ( | ||||
|       <SectionContent title="AP Settings"> | ||||
|         <SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} /> | ||||
|       	<APSettingsForm  apSettingsFetched={apSettingsFetched} apSettings={apSettings} errorMessage={errorMessage} | ||||
|           onSubmit={this.saveAPSettings} onReset={this.loadAPSettings} handleValueChange={this.wifiSettingValueChange} /> | ||||
|       </SectionContent> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default APSettings; | ||||
							
								
								
									
										165
									
								
								interface/src/containers/APStatus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								interface/src/containers/APStatus.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
| import List, { ListItem, ListItemText } from 'material-ui/List'; | ||||
| import Avatar from 'material-ui/Avatar'; | ||||
| import Divider from 'material-ui/Divider'; | ||||
| import SettingsInputAntennaIcon from 'material-ui-icons/SettingsInputAntenna'; | ||||
| import DeviceHubIcon from 'material-ui-icons/DeviceHub'; | ||||
| import ComputerIcon from 'material-ui-icons/Computer'; | ||||
|  | ||||
| import SnackbarNotification from '../components/SnackbarNotification' | ||||
| import SectionContent from '../components/SectionContent' | ||||
|  | ||||
| import * as Highlight from '../constants/Highlight'; | ||||
| import { AP_STATUS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
| import { simpleGet }  from  '../helpers/SimpleGet'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   ["apStatus_" + Highlight.SUCCESS]: { | ||||
|     backgroundColor: theme.palette.highlight_success | ||||
|   }, | ||||
|   ["apStatus_" + Highlight.IDLE]: { | ||||
|     backgroundColor: theme.palette.highlight_idle | ||||
|   }, | ||||
|   fetching: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class APStatus extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.state = { | ||||
|              status:null, | ||||
|              fetched: false, | ||||
|              errorMessage:null | ||||
|            }; | ||||
|  | ||||
|     this.setState = this.setState.bind(this); | ||||
|     this.loadAPStatus = this.loadAPStatus.bind(this); | ||||
|     this.raiseNotification=this.raiseNotification.bind(this); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|     this.loadAPStatus(); | ||||
|   } | ||||
|  | ||||
|   loadAPStatus() { | ||||
|     simpleGet( | ||||
|       AP_STATUS_ENDPOINT, | ||||
|       this.setState, | ||||
|       this.raiseNotification | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   raiseNotification(errorMessage) { | ||||
|     this.snackbarNotification(errorMessage); | ||||
|   } | ||||
|  | ||||
|   apStatusHighlight(status){ | ||||
|     return status.active ? Highlight.SUCCESS : Highlight.IDLE; | ||||
|   } | ||||
|  | ||||
|   apStatus(status){ | ||||
|     return status.active ? "Active" : "Inactive"; | ||||
|   } | ||||
|  | ||||
|   // active, ip_address, mac_address, station_num | ||||
|  | ||||
|   renderAPStatus(status, fullDetails, classes){ | ||||
|     const listItems = []; | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="ap_status"> | ||||
|         <Avatar className={classes["apStatus_" + this.apStatusHighlight(status)]}> | ||||
|           <SettingsInputAntennaIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="Status" secondary={this.apStatus(status)} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="ap_status_divider" inset component="li" />); | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="ip_address"> | ||||
|         <Avatar>IP</Avatar> | ||||
|         <ListItemText primary="IP Address" secondary={status.ip_address} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="ip_address_divider" inset component="li" />); | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="mac_address"> | ||||
|         <Avatar> | ||||
|           <DeviceHubIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="MAC Address" secondary={status.mac_address} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="mac_address_divider" inset component="li" />); | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="station_num"> | ||||
|         <Avatar> | ||||
|           <ComputerIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="AP Clients" secondary={status.station_num} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="mac_address_divider" inset component="li" />); | ||||
|  | ||||
|     return  ( | ||||
|       <div> | ||||
|         <List> | ||||
|           {listItems} | ||||
|         </List> | ||||
|         <Button variant="raised" color="secondary" className={classes.button} onClick={this.loadAPStatus}> | ||||
|           Refresh | ||||
|         </Button> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { status, fetched, errorMessage } = this.state; | ||||
|     const { classes, fullDetails }  = this.props; | ||||
|  | ||||
|     return ( | ||||
|       <SectionContent title="AP Status"> | ||||
|         <SnackbarNotification notificationRef={(snackbarNotification)=>this.snackbarNotification = snackbarNotification} /> | ||||
|         { | ||||
|          !fetched ? | ||||
|          <div> | ||||
|            <LinearProgress className={classes.fetching}/> | ||||
|            <Typography variant="display1" className={classes.fetching}> | ||||
|              Loading... | ||||
|            </Typography> | ||||
|          </div> | ||||
|        : | ||||
|         status ? this.renderAPStatus(status, fullDetails, classes) | ||||
|        : | ||||
|         <div> | ||||
|           <Typography variant="display1" className={classes.fetching}> | ||||
|             {errorMessage} | ||||
|           </Typography> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={this.loadAPStatus}> | ||||
|             Refresh | ||||
|           </Button> | ||||
|         </div> | ||||
|       } | ||||
|       </SectionContent> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default withStyles(styles)(APStatus); | ||||
							
								
								
									
										37
									
								
								interface/src/containers/NTPConfiguration.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								interface/src/containers/NTPConfiguration.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import MenuAppBar from '../components/MenuAppBar'; | ||||
| import NTPSettings from './NTPSettings'; | ||||
| import NTPStatus from './NTPStatus'; | ||||
|  | ||||
| import Tabs, { Tab } from 'material-ui/Tabs'; | ||||
|  | ||||
| class NTPConfiguration extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|         selectedTab: "ntpStatus" | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   handleTabChange = (event, selectedTab) => { | ||||
|     this.setState({ selectedTab }); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { selectedTab } = this.state; | ||||
|     return ( | ||||
|         <MenuAppBar sectionTitle="NTP Configuration"> | ||||
|         <Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" fullWidth centered scrollable> | ||||
|            <Tab value="ntpStatus" label="NTP Status" /> | ||||
|            <Tab value="ntpSettings" label="NTP Settings" /> | ||||
|          </Tabs> | ||||
|          {selectedTab === "ntpStatus" && <NTPStatus />} | ||||
|          {selectedTab === "ntpSettings" && <NTPSettings />} | ||||
|         </MenuAppBar> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default NTPConfiguration | ||||
							
								
								
									
										70
									
								
								interface/src/containers/NTPSettings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								interface/src/containers/NTPSettings.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { NTP_SETTINGS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
| import SectionContent from '../components/SectionContent'; | ||||
| import SnackbarNotification from '../components/SnackbarNotification'; | ||||
| import NTPSettingsForm from '../forms/NTPSettingsForm'; | ||||
| import { simpleGet }  from  '../helpers/SimpleGet'; | ||||
| import { simplePost } from '../helpers/SimplePost'; | ||||
|  | ||||
| class NTPSettings extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.state = { | ||||
|              ntpSettings:{}, | ||||
|              ntpSettingsFetched: false, | ||||
|              errorMessage:null | ||||
|            }; | ||||
|  | ||||
|     this.setState = this.setState.bind(this); | ||||
|     this.loadNTPSettings = this.loadNTPSettings.bind(this); | ||||
|     this.saveNTPSettings = this.saveNTPSettings.bind(this); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|       this.loadNTPSettings(); | ||||
|   } | ||||
|  | ||||
|   loadNTPSettings() { | ||||
|     simpleGet( | ||||
|       NTP_SETTINGS_ENDPOINT, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "ntpSettings", | ||||
|       "ntpSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   saveNTPSettings(e) { | ||||
|     simplePost( | ||||
|       NTP_SETTINGS_ENDPOINT, | ||||
|       this.state, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "ntpSettings", | ||||
|       "ntpSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   ntpSettingValueChange = name => event => { | ||||
|     const { ntpSettings } = this.state; | ||||
|     ntpSettings[name] = event.target.value; | ||||
|     this.setState({ntpSettings}); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { ntpSettingsFetched, ntpSettings, errorMessage } = this.state; | ||||
|     return ( | ||||
|       <SectionContent title="NTP Settings"> | ||||
|         <SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} /> | ||||
|       	<NTPSettingsForm ntpSettingsFetched={ntpSettingsFetched} ntpSettings={ntpSettings} errorMessage={errorMessage} | ||||
|           onSubmit={this.saveNTPSettings} onReset={this.loadNTPSettings} handleValueChange={this.ntpSettingValueChange} /> | ||||
|       </SectionContent> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default NTPSettings; | ||||
							
								
								
									
										189
									
								
								interface/src/containers/NTPStatus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								interface/src/containers/NTPStatus.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
| import List, { ListItem, ListItemText } from 'material-ui/List'; | ||||
| import Avatar from 'material-ui/Avatar'; | ||||
| import Divider from 'material-ui/Divider'; | ||||
| import SwapVerticalCircleIcon from 'material-ui-icons/SwapVerticalCircle'; | ||||
| import AccessTimeIcon from 'material-ui-icons/AccessTime'; | ||||
| import DNSIcon from 'material-ui-icons/Dns'; | ||||
| import TimerIcon from 'material-ui-icons/Timer'; | ||||
| import UpdateIcon from 'material-ui-icons/Update'; | ||||
| import AvTimerIcon from 'material-ui-icons/AvTimer'; | ||||
|  | ||||
| import { isSynchronized, ntpStatusHighlight, ntpStatus }  from  '../constants/NTPStatus'; | ||||
| import * as Highlight from '../constants/Highlight'; | ||||
| import { unixTimeToTimeAndDate } from '../constants/TimeFormat'; | ||||
| import { NTP_STATUS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
|  | ||||
| import SnackbarNotification from '../components/SnackbarNotification'; | ||||
| import SectionContent from '../components/SectionContent'; | ||||
|  | ||||
| import { simpleGet }  from  '../helpers/SimpleGet'; | ||||
|  | ||||
| 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 | ||||
|   }, | ||||
|   fetching: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class NTPStatus extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.state = { | ||||
|              status:null, | ||||
|              fetched: false, | ||||
|              errorMessage:null | ||||
|            }; | ||||
|  | ||||
|     this.setState = this.setState.bind(this); | ||||
|     this.loadNTPStatus = this.loadNTPStatus.bind(this); | ||||
|     this.raiseNotification=this.raiseNotification.bind(this); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|     this.loadNTPStatus(); | ||||
|   } | ||||
|  | ||||
|   loadNTPStatus() { | ||||
|     simpleGet( | ||||
|       NTP_STATUS_ENDPOINT, | ||||
|       this.setState, | ||||
|       this.raiseNotification | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   raiseNotification(errorMessage) { | ||||
|     this.snackbarNotification(errorMessage); | ||||
|   } | ||||
|  | ||||
|   renderNTPStatus(status, fullDetails, classes){ | ||||
|     const listItems = []; | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="ntp_status"> | ||||
|         <Avatar className={classes["ntpStatus_" + ntpStatusHighlight(status)]}> | ||||
|           <UpdateIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="Status" secondary={ntpStatus(status)} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="ntp_status_divider" inset component="li" />); | ||||
|  | ||||
|     if (isSynchronized(status)) { | ||||
|       listItems.push( | ||||
|         <ListItem key="time_now"> | ||||
|           <Avatar> | ||||
|             <AccessTimeIcon /> | ||||
|           </Avatar> | ||||
|           <ListItemText primary="Time Now" secondary={unixTimeToTimeAndDate(status.now)} /> | ||||
|         </ListItem> | ||||
|       ); | ||||
|       listItems.push(<Divider key="time_now_divider" inset component="li" />); | ||||
|     } | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="last_sync"> | ||||
|         <Avatar> | ||||
|           <SwapVerticalCircleIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="Last Sync" secondary={status.last_sync > 0 ? unixTimeToTimeAndDate(status.last_sync) : "never" } /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="last_sync_divider" inset component="li" />); | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="ntp_server"> | ||||
|         <Avatar> | ||||
|           <DNSIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="NTP Server" secondary={status.server} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="ntp_server_divider" inset component="li" />); | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="sync_interval"> | ||||
|         <Avatar> | ||||
|           <TimerIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="Sync Interval" secondary={moment.duration(status.interval, 'seconds').humanize()} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|     listItems.push(<Divider key="sync_interval_divider" inset component="li" />); | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="uptime"> | ||||
|         <Avatar> | ||||
|           <AvTimerIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="Uptime" secondary={moment.duration(status.uptime, 'seconds').humanize()} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|  | ||||
|     return  ( | ||||
|       <div> | ||||
|         <List> | ||||
|           {listItems} | ||||
|         </List> | ||||
|         <Button variant="raised" color="secondary" className={classes.button} onClick={this.loadNTPStatus}> | ||||
|           Refresh | ||||
|         </Button> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { status, fetched, errorMessage } = this.state; | ||||
|     const { classes, fullDetails }  = this.props; | ||||
|  | ||||
|     return ( | ||||
|       <SectionContent title="NTP Status"> | ||||
|         <SnackbarNotification notificationRef={(snackbarNotification)=>this.snackbarNotification = snackbarNotification} /> | ||||
|         { | ||||
|          !fetched ? | ||||
|          <div> | ||||
|            <LinearProgress className={classes.fetching}/> | ||||
|            <Typography variant="display1" className={classes.fetching}> | ||||
|              Loading... | ||||
|            </Typography> | ||||
|          </div> | ||||
|        : | ||||
|         status ? this.renderNTPStatus(status, fullDetails, classes) | ||||
|        : | ||||
|         <div> | ||||
|           <Typography variant="display1" className={classes.fetching}> | ||||
|             {errorMessage} | ||||
|           </Typography> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={this.loadNTPStatus}> | ||||
|             Refresh | ||||
|           </Button> | ||||
|         </div> | ||||
|       } | ||||
|       </SectionContent> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default withStyles(styles)(NTPStatus); | ||||
							
								
								
									
										15
									
								
								interface/src/containers/OTAConfiguration.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								interface/src/containers/OTAConfiguration.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import MenuAppBar from '../components/MenuAppBar'; | ||||
| import OTASettings from './OTASettings'; | ||||
|  | ||||
| class OTAConfiguration extends Component { | ||||
|   render() { | ||||
|     return ( | ||||
|         <MenuAppBar sectionTitle="OTA Configuration"> | ||||
|           <OTASettings /> | ||||
|         </MenuAppBar> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default OTAConfiguration | ||||
							
								
								
									
										77
									
								
								interface/src/containers/OTASettings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								interface/src/containers/OTASettings.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { OTA_SETTINGS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
| import SectionContent from '../components/SectionContent'; | ||||
| import SnackbarNotification from '../components/SnackbarNotification'; | ||||
| import OTASettingsForm from '../forms/OTASettingsForm'; | ||||
| import { simpleGet }  from  '../helpers/SimpleGet'; | ||||
| import { simplePost } from '../helpers/SimplePost'; | ||||
|  | ||||
| class OTASettings extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.state = { | ||||
|              otaSettings:null, | ||||
|              otaSettingsFetched: false, | ||||
|              errorMessage:null | ||||
|            }; | ||||
|  | ||||
|     this.setState = this.setState.bind(this); | ||||
|     this.loadOTASettings = this.loadOTASettings.bind(this); | ||||
|     this.saveOTASettings = this.saveOTASettings.bind(this); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|       this.loadOTASettings(); | ||||
|   } | ||||
|  | ||||
|   loadOTASettings() { | ||||
|     simpleGet( | ||||
|       OTA_SETTINGS_ENDPOINT, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "otaSettings", | ||||
|       "otaSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   saveOTASettings(e) { | ||||
|     simplePost( | ||||
|       OTA_SETTINGS_ENDPOINT, | ||||
|       this.state, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "otaSettings", | ||||
|       "otaSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   otaSettingValueChange = name => event => { | ||||
|     const { otaSettings } = this.state; | ||||
|     otaSettings[name] = event.target.value; | ||||
|     this.setState({otaSettings}); | ||||
|   }; | ||||
|  | ||||
|   otaSettingCheckboxChange = name => event => { | ||||
|     const { otaSettings } = this.state; | ||||
|     otaSettings[name] = event.target.checked; | ||||
|     this.setState({otaSettings}); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { otaSettingsFetched, otaSettings, errorMessage } = this.state; | ||||
|     return ( | ||||
|       <SectionContent title="OTA Settings"> | ||||
|         <SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} /> | ||||
|       	<OTASettingsForm otaSettingsFetched={otaSettingsFetched} otaSettings={otaSettings} errorMessage={errorMessage} | ||||
|           onSubmit={this.saveOTASettings} onReset={this.loadOTASettings} handleValueChange={this.otaSettingValueChange} | ||||
|           handleCheckboxChange={this.otaSettingCheckboxChange} /> | ||||
|       </SectionContent> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| export default OTASettings; | ||||
							
								
								
									
										53
									
								
								interface/src/containers/WiFiConfiguration.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								interface/src/containers/WiFiConfiguration.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import Tabs, { Tab } from 'material-ui/Tabs'; | ||||
|  | ||||
| import MenuAppBar from '../components/MenuAppBar'; | ||||
| import WiFiNetworkScanner from './WiFiNetworkScanner'; | ||||
| import WiFiSettings from './WiFiSettings'; | ||||
| import WiFiStatus from './WiFiStatus'; | ||||
|  | ||||
| class WiFiConfiguration extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|     this.state = { | ||||
|         selectedTab: "wifiStatus", | ||||
|         selectedNetwork: null | ||||
|     }; | ||||
|     this.selectNetwork = this.selectNetwork.bind(this); | ||||
|     this.deselectNetwork = this.deselectNetwork.bind(this); | ||||
|   } | ||||
|  | ||||
|   // TODO - slightly inapproperate use of callback ref possibly. | ||||
|   selectNetwork(network) { | ||||
|     this.setState({ selectedTab: "wifiSettings", selectedNetwork:network }); | ||||
|   } | ||||
|  | ||||
|   // deselects the network after the settings component mounts. | ||||
|   deselectNetwork(network) { | ||||
|     this.setState({ selectedNetwork:null }); | ||||
|   } | ||||
|  | ||||
|   handleTabChange = (event, selectedTab) => { | ||||
|     this.setState({ selectedTab }); | ||||
|   }; | ||||
|  | ||||
|   render() { | ||||
|     const { selectedTab } = this.state; | ||||
|     return ( | ||||
|       <MenuAppBar sectionTitle="WiFi Configuration"> | ||||
|         <Tabs value={selectedTab} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" fullWidth centered scrollable> | ||||
|            <Tab value="wifiStatus" label="WiFi Status" /> | ||||
|            <Tab value="networkScanner" label="Network Scanner" /> | ||||
|            <Tab value="wifiSettings" label="WiFi Settings" /> | ||||
|          </Tabs> | ||||
|          {selectedTab === "wifiStatus" && <WiFiStatus fullDetails={true} />} | ||||
|          {selectedTab === "networkScanner" && <WiFiNetworkScanner selectNetwork={this.selectNetwork} />} | ||||
|          {selectedTab === "wifiSettings" && <WiFiSettings deselectNetwork={this.deselectNetwork} selectedNetwork={this.state.selectedNetwork} />} | ||||
|       </MenuAppBar> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default WiFiConfiguration; | ||||
							
								
								
									
										118
									
								
								interface/src/containers/WiFiNetworkScanner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								interface/src/containers/WiFiNetworkScanner.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
| import SectionContent from '../components/SectionContent'; | ||||
| import WiFiNetworkSelector from '../forms/WiFiNetworkSelector'; | ||||
|  | ||||
| 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}); | ||||
|     fetch(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.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() { | ||||
|     fetch(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 timley 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 => { | ||||
|       console.log(error.message); | ||||
|       if (error.name !== RETRY_EXCEPTION_TYPE) { | ||||
|         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 (WiFiNetworkScanner); | ||||
							
								
								
									
										102
									
								
								interface/src/containers/WiFiSettings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								interface/src/containers/WiFiSettings.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import { WIFI_SETTINGS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
| import SectionContent from '../components/SectionContent'; | ||||
| import SnackbarNotification from '../components/SnackbarNotification'; | ||||
| import WiFiSettingsForm from '../forms/WiFiSettingsForm'; | ||||
| import { simpleGet }  from  '../helpers/SimpleGet'; | ||||
| import { simplePost } from '../helpers/SimplePost'; | ||||
|  | ||||
| class WiFiSettings extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.state = { | ||||
|              wifiSettingsFetched: false, | ||||
|              wifiSettings:{}, | ||||
|              selectedNetwork: null, | ||||
|              errorMessage:null | ||||
|            }; | ||||
|  | ||||
|     this.setState = this.setState.bind(this); | ||||
|     this.loadWiFiSettings = this.loadWiFiSettings.bind(this); | ||||
|     this.saveWiFiSettings = this.saveWiFiSettings.bind(this); | ||||
|     this.deselectNetwork = this.deselectNetwork.bind(this); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|     const { selectedNetwork, deselectNetwork } = this.props; | ||||
|     if (selectedNetwork){ | ||||
|       var wifiSettings = { | ||||
|         ssid:selectedNetwork.ssid, | ||||
|         password:"", | ||||
|         hostname:"esp8266-react", | ||||
|         static_ip_config:false, | ||||
|       } | ||||
|       this.setState({wifiSettingsFetched:true, wifiSettings, selectedNetwork, errorMessage:null}); | ||||
|       deselectNetwork(); | ||||
|     }else { | ||||
|       this.loadWiFiSettings(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   loadWiFiSettings() { | ||||
|     this.deselectNetwork(); | ||||
|  | ||||
|     simpleGet( | ||||
|       WIFI_SETTINGS_ENDPOINT, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "wifiSettings", | ||||
|       "wifiSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   saveWiFiSettings(e) { | ||||
|     simplePost( | ||||
|       WIFI_SETTINGS_ENDPOINT, | ||||
|       this.state, | ||||
|       this.setState, | ||||
|       this.raiseNotification, | ||||
|       "wifiSettings", | ||||
|       "wifiSettingsFetched" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   deselectNetwork(nextNetwork) { | ||||
|     this.setState({selectedNetwork:null}); | ||||
|   } | ||||
|  | ||||
|   wifiSettingValueChange = name => event => { | ||||
|     const { wifiSettings } = this.state; | ||||
|     wifiSettings[name] = event.target.value; | ||||
|     this.setState({wifiSettings}); | ||||
|   }; | ||||
|  | ||||
|   wifiSettingCheckboxChange = name => event => { | ||||
|     const { wifiSettings } = this.state; | ||||
|     wifiSettings[name] = event.target.checked; | ||||
|     this.setState({wifiSettings}); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { wifiSettingsFetched, wifiSettings, errorMessage, selectedNetwork } = this.state; | ||||
|     return ( | ||||
|       <SectionContent title="WiFi Settings"> | ||||
|         <SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} /> | ||||
|       	<WiFiSettingsForm wifiSettingsFetched={wifiSettingsFetched} wifiSettings={wifiSettings} errorMessage={errorMessage} selectedNetwork={selectedNetwork} deselectNetwork={this.deselectNetwork} | ||||
|           onSubmit={this.saveWiFiSettings} onReset={this.loadWiFiSettings} handleValueChange={this.wifiSettingValueChange} handleCheckboxChange={this.wifiSettingCheckboxChange} /> | ||||
|       </SectionContent> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| WiFiSettings.propTypes = { | ||||
|   deselectNetwork: PropTypes.func, | ||||
|   selectedNetwork: PropTypes.object | ||||
| }; | ||||
|  | ||||
| export default WiFiSettings; | ||||
							
								
								
									
										188
									
								
								interface/src/containers/WiFiStatus.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								interface/src/containers/WiFiStatus.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
|  | ||||
| import SnackbarNotification from '../components/SnackbarNotification'; | ||||
| import SectionContent from '../components/SectionContent'; | ||||
|  | ||||
| import { WIFI_STATUS_ENDPOINT }  from  '../constants/Endpoints'; | ||||
| import { isConnected, connectionStatus, connectionStatusHighlight }  from  '../constants/WiFiConnectionStatus'; | ||||
| import * as Highlight from '../constants/Highlight'; | ||||
|  | ||||
| import { simpleGet }  from  '../helpers/SimpleGet'; | ||||
|  | ||||
| import List, { ListItem, ListItemText } from 'material-ui/List'; | ||||
| import Avatar from 'material-ui/Avatar'; | ||||
| import Divider from 'material-ui/Divider'; | ||||
| import WifiIcon from 'material-ui-icons/Wifi'; | ||||
| import DNSIcon from 'material-ui-icons/Dns'; | ||||
| import SettingsInputComponentIcon from 'material-ui-icons/SettingsInputComponent'; | ||||
| import SettingsInputAntennaIcon from 'material-ui-icons/SettingsInputAntenna'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   ["wifiStatus_" + Highlight.IDLE]: { | ||||
|     backgroundColor: theme.palette.highlight_idle | ||||
|   }, | ||||
|   ["wifiStatus_" + Highlight.SUCCESS]: { | ||||
|     backgroundColor: theme.palette.highlight_success | ||||
|   }, | ||||
|   ["wifiStatus_" + Highlight.ERROR]: { | ||||
|     backgroundColor: theme.palette.highlight_error | ||||
|   }, | ||||
|   ["wifiStatus_" + Highlight.WARN]: { | ||||
|     backgroundColor: theme.palette.highlight_warn | ||||
|   }, | ||||
|   fetching: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class WiFiStatus extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.state = { | ||||
|              status:null, | ||||
|              fetched: false, | ||||
|              errorMessage:null | ||||
|            }; | ||||
|  | ||||
|     this.setState = this.setState.bind(this); | ||||
|     this.loadWiFiStatus = this.loadWiFiStatus.bind(this); | ||||
|   } | ||||
|  | ||||
|   componentDidMount() { | ||||
|     this.loadWiFiStatus(); | ||||
|   } | ||||
|  | ||||
|   dnsServers(status) { | ||||
|     if (!status.dns_ip_1){ | ||||
|       return "none"; | ||||
|     } | ||||
|     return status.dns_ip_1 + (status.dns_ip_2 ? ','+status.dns_ip_2 : ''); | ||||
|   } | ||||
|  | ||||
|   loadWiFiStatus() { | ||||
|     simpleGet( | ||||
|       WIFI_STATUS_ENDPOINT, | ||||
|       this.setState, | ||||
|       this.raiseNotification | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   renderWiFiStatus(status, fullDetails, classes) { | ||||
|     const listItems = []; | ||||
|  | ||||
|     listItems.push( | ||||
|       <ListItem key="connection_status"> | ||||
|         <Avatar className={classes["wifiStatus_" + connectionStatusHighlight(status)]}> | ||||
|           <WifiIcon /> | ||||
|         </Avatar> | ||||
|         <ListItemText primary="Connection Status" secondary={connectionStatus(status)} /> | ||||
|       </ListItem> | ||||
|     ); | ||||
|  | ||||
|     if (fullDetails && isConnected(status)) { | ||||
|       listItems.push(<Divider key="connection_status_divider" inset component="li" />); | ||||
|  | ||||
|       listItems.push( | ||||
|         <ListItem key="ssid"> | ||||
|           <Avatar> | ||||
|             <SettingsInputAntennaIcon /> | ||||
|           </Avatar> | ||||
|           <ListItemText primary="SSID" secondary={status.ssid} /> | ||||
|         </ListItem> | ||||
|       ); | ||||
|       listItems.push(<Divider key="ssid_divider" inset component="li" />); | ||||
|  | ||||
|       listItems.push( | ||||
|         <ListItem key="ip_address"> | ||||
|           <Avatar>IP</Avatar> | ||||
|           <ListItemText primary="IP Address" secondary={status.local_ip} /> | ||||
|         </ListItem> | ||||
|       ); | ||||
|       listItems.push(<Divider key="ip_address_divider" inset component="li" />); | ||||
|  | ||||
|       listItems.push( | ||||
|         <ListItem key="subnet_mask"> | ||||
|           <Avatar>#</Avatar> | ||||
|           <ListItemText primary="Subnet Mask" secondary={status.subnet_mask} /> | ||||
|         </ListItem> | ||||
|       ); | ||||
|       listItems.push(<Divider key="subnet_mask_divider" inset component="li" />); | ||||
|  | ||||
|       listItems.push( | ||||
|         <ListItem key="gateway_ip"> | ||||
|           <Avatar> | ||||
|             <SettingsInputComponentIcon /> | ||||
|           </Avatar> | ||||
|           <ListItemText primary="Gateway IP" secondary={status.gateway_ip ? status.gateway_ip : "none"} /> | ||||
|         </ListItem> | ||||
|       ); | ||||
|       listItems.push(<Divider key="gateway_ip_divider" inset component="li" />); | ||||
|  | ||||
|       listItems.push( | ||||
|         <ListItem key="dns_server_ip"> | ||||
|           <Avatar> | ||||
|             <DNSIcon /> | ||||
|           </Avatar> | ||||
|           <ListItemText primary="DNS Server IP" secondary={this.dnsServers(status)} /> | ||||
|         </ListItem> | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return  ( | ||||
|       <div> | ||||
|         <List> | ||||
|         {listItems} | ||||
|         </List> | ||||
|         <Button variant="raised" color="secondary" className={classes.button} onClick={this.loadWiFiStatus}> | ||||
|           Refresh | ||||
|         </Button> | ||||
|       </div> | ||||
|     ); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { status, fetched, errorMessage } = this.state; | ||||
|     const { classes, fullDetails }  = this.props; | ||||
|  | ||||
|     return ( | ||||
|       <SectionContent title="WiFi Status"> | ||||
|         <SnackbarNotification notificationRef={(raiseNotification)=>this.raiseNotification = raiseNotification} /> | ||||
|         { | ||||
|          !fetched ? | ||||
|          <div> | ||||
|            <LinearProgress className={classes.fetching}/> | ||||
|            <Typography variant="display1" className={classes.fetching}> | ||||
|              Loading... | ||||
|            </Typography> | ||||
|          </div> | ||||
|        : | ||||
|         status ? this.renderWiFiStatus(status, fullDetails, classes) | ||||
|        : | ||||
|         <div> | ||||
|           <Typography variant="display1" className={classes.fetching}> | ||||
|             {errorMessage} | ||||
|           </Typography> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={this.loadWiFiStatus}> | ||||
|             Refresh | ||||
|           </Button> | ||||
|         </div> | ||||
|       } | ||||
|       </SectionContent> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default withStyles(styles)(WiFiStatus); | ||||
							
								
								
									
										124
									
								
								interface/src/forms/APSettingsForm.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								interface/src/forms/APSettingsForm.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
| import { MenuItem } from 'material-ui/Menu'; | ||||
|  | ||||
| import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator'; | ||||
|  | ||||
| import {isAPEnabled} from '../constants/WiFiAPModes'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   loadingSettings: { | ||||
|     margin: theme.spacing.unit, | ||||
|   }, | ||||
|   loadingSettingsDetails: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   textField: { | ||||
|     width: "100%" | ||||
|   }, | ||||
|   selectField:{ | ||||
|     width: "100%", | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|     marginBottom: theme.spacing.unit | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class APSettingsForm extends React.Component { | ||||
|  | ||||
|   render() { | ||||
|     const { classes, apSettingsFetched, apSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props; | ||||
|     return ( | ||||
|       <div> | ||||
|         { | ||||
|          !apSettingsFetched ? | ||||
|  | ||||
|          <div className={classes.loadingSettings}> | ||||
|            <LinearProgress className={classes.loadingSettingsDetails}/> | ||||
|            <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|              Loading... | ||||
|            </Typography> | ||||
|          </div> | ||||
|  | ||||
|          : apSettings ? | ||||
|  | ||||
|         <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) && | ||||
|             [ | ||||
|               <TextValidator key="ssid" | ||||
|                 validators={['required', 'matchRegexp:^.{0,32}$']} | ||||
|                 errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characeters or less']} | ||||
|                 name="ssid" | ||||
|                 label="Access Point SSID" | ||||
|                 className={classes.textField} | ||||
|                 value={apSettings.ssid} | ||||
|                 onChange={handleValueChange('ssid')} | ||||
|                 margin="normal" | ||||
|               />, | ||||
|               <TextValidator key="password" | ||||
|                     validators={['required', 'matchRegexp:^.{0,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" | ||||
|               /> | ||||
|             ] | ||||
|           } | ||||
|  | ||||
|           <Button variant="raised" color="primary" className={classes.button} type="submit"> | ||||
|             Save | ||||
|           </Button> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|  | ||||
|         </ValidatorForm> | ||||
|  | ||||
|         : | ||||
|  | ||||
|         <div className={classes.loadingSettings}> | ||||
|           <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|             {errorMessage} | ||||
|           </Typography> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|         </div> | ||||
|       } | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| APSettingsForm.propTypes = { | ||||
|   classes: PropTypes.object.isRequired, | ||||
|   apSettingsFetched: PropTypes.bool.isRequired, | ||||
|   apSettings: PropTypes.object, | ||||
|   errorMessage: PropTypes.string, | ||||
|   onSubmit: PropTypes.func.isRequired, | ||||
|   onReset: PropTypes.func.isRequired, | ||||
|   handleValueChange: PropTypes.func.isRequired | ||||
| }; | ||||
|  | ||||
| export default withStyles(styles)(APSettingsForm); | ||||
							
								
								
									
										113
									
								
								interface/src/forms/NTPSettingsForm.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								interface/src/forms/NTPSettingsForm.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
|  | ||||
| import isIP from '../validators/isIP'; | ||||
| import isHostname from '../validators/isHostname'; | ||||
| import or from '../validators/or'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   loadingSettings: { | ||||
|     margin: theme.spacing.unit, | ||||
|   }, | ||||
|   loadingSettingsDetails: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   textField: { | ||||
|     width: "100%" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class NTPSettingsForm extends React.Component { | ||||
|  | ||||
|   componentWillMount() { | ||||
|     ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname)); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { classes, ntpSettingsFetched, ntpSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props; | ||||
|     return ( | ||||
|       <div> | ||||
|         { | ||||
|          !ntpSettingsFetched ? | ||||
|  | ||||
|          <div className={classes.loadingSettings}> | ||||
|            <LinearProgress className={classes.loadingSettingsDetails}/> | ||||
|            <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|              Loading... | ||||
|            </Typography> | ||||
|          </div> | ||||
|  | ||||
|          : ntpSettings ? | ||||
|  | ||||
|       	 <ValidatorForm onSubmit={onSubmit}> | ||||
|  | ||||
|            <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" | ||||
|              /> | ||||
|  | ||||
|              <TextValidator | ||||
|                  validators={['required','isNumber','minNumber:60','maxNumber:86400']} | ||||
|                  errorMessages={['Interval is required','Interval must be a number','Must be at least 60 seconds',"Must not be more than 86400 seconds (24 hours)"]} | ||||
|                  name="interval" | ||||
|                  label="Interval (Seconds)" | ||||
|                  className={classes.textField} | ||||
|                  value={ntpSettings.interval} | ||||
|                  type="number" | ||||
|                  onChange={handleValueChange('interval')} | ||||
|                  margin="normal" | ||||
|                /> | ||||
|  | ||||
|           <Button variant="raised" color="primary" className={classes.button} type="submit"> | ||||
|             Save | ||||
|           </Button> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|  | ||||
|          </ValidatorForm> | ||||
|  | ||||
|         : | ||||
|  | ||||
|         <div className={classes.loadingSettings}> | ||||
|           <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|             {errorMessage} | ||||
|           </Typography> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|         </div> | ||||
|       } | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| NTPSettingsForm.propTypes = { | ||||
|   classes: PropTypes.object.isRequired, | ||||
|   ntpSettingsFetched: PropTypes.bool.isRequired, | ||||
|   ntpSettings: PropTypes.object, | ||||
|   errorMessage: PropTypes.string, | ||||
|   onSubmit: PropTypes.func.isRequired, | ||||
|   onReset: PropTypes.func.isRequired, | ||||
|   handleValueChange: PropTypes.func.isRequired, | ||||
| }; | ||||
|  | ||||
| export default withStyles(styles)(NTPSettingsForm); | ||||
							
								
								
									
										132
									
								
								interface/src/forms/OTASettingsForm.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								interface/src/forms/OTASettingsForm.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import Switch from 'material-ui/Switch'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
| import { FormControlLabel } from 'material-ui/Form'; | ||||
|  | ||||
| import isIP from '../validators/isIP'; | ||||
| import isHostname from '../validators/isHostname'; | ||||
| import or from '../validators/or'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   loadingSettings: { | ||||
|     margin: theme.spacing.unit, | ||||
|   }, | ||||
|   loadingSettingsDetails: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   switchControl: { | ||||
|     width: "100%", | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|     marginBottom: theme.spacing.unit | ||||
|   }, | ||||
|   textField: { | ||||
|     width: "100%" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class OTASettingsForm extends React.Component { | ||||
|  | ||||
|   componentWillMount() { | ||||
|     ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname)); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const { classes, otaSettingsFetched, otaSettings, errorMessage, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props; | ||||
|     return ( | ||||
|       <div> | ||||
|         { | ||||
|          !otaSettingsFetched ? | ||||
|  | ||||
|          <div className={classes.loadingSettings}> | ||||
|            <LinearProgress className={classes.loadingSettingsDetails}/> | ||||
|            <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|              Loading... | ||||
|            </Typography> | ||||
|          </div> | ||||
|  | ||||
|          : otaSettings ? | ||||
|  | ||||
|       	 <ValidatorForm onSubmit={onSubmit}> | ||||
|  | ||||
|             <FormControlLabel className={classes.switchControl} | ||||
|                control={ | ||||
|                  <Switch | ||||
|                         checked={otaSettings.enabled} | ||||
|                         onChange={handleCheckboxChange('enabled')} | ||||
|                         value="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" | ||||
|                className={classes.textField} | ||||
|                value={otaSettings.port} | ||||
|                type="number" | ||||
|                onChange={handleValueChange('port')} | ||||
|                margin="normal" | ||||
|              /> | ||||
|  | ||||
|              <TextValidator key="password" | ||||
|                    validators={['required', 'matchRegexp:^.{0,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 variant="raised" color="primary" className={classes.button} type="submit"> | ||||
|             Save | ||||
|           </Button> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|  | ||||
|          </ValidatorForm> | ||||
|  | ||||
|         : | ||||
|  | ||||
|         <div className={classes.loadingSettings}> | ||||
|           <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|             {errorMessage} | ||||
|           </Typography> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|         </div> | ||||
|       } | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| OTASettingsForm.propTypes = { | ||||
|   classes: PropTypes.object.isRequired, | ||||
|   otaSettingsFetched: PropTypes.bool.isRequired, | ||||
|   otaSettings: PropTypes.object, | ||||
|   errorMessage: PropTypes.string, | ||||
|   onSubmit: PropTypes.func.isRequired, | ||||
|   onReset: PropTypes.func.isRequired, | ||||
|   handleValueChange: PropTypes.func.isRequired, | ||||
|   handleCheckboxChange: PropTypes.func.isRequired, | ||||
| }; | ||||
|  | ||||
| export default withStyles(styles)(OTASettingsForm); | ||||
							
								
								
									
										102
									
								
								interface/src/forms/WiFiNetworkSelector.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								interface/src/forms/WiFiNetworkSelector.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| import React, { Component } from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
|  | ||||
| import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes'; | ||||
|  | ||||
| import List, { ListItem, ListItemText, ListItemIcon,  ListItemAvatar } from 'material-ui/List'; | ||||
| import Avatar from 'material-ui/Avatar'; | ||||
| import Badge from 'material-ui/Badge'; | ||||
|  | ||||
| import WifiIcon from 'material-ui-icons/Wifi'; | ||||
| import LockIcon from 'material-ui-icons/Lock'; | ||||
| import LockOpenIcon from 'material-ui-icons/LockOpen'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   scanningProgress: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 2, | ||||
|   } | ||||
| }); | ||||
|  | ||||
| class WiFiNetworkSelector extends Component { | ||||
|  | ||||
|   constructor(props) { | ||||
|     super(props); | ||||
|  | ||||
|     this.renderNetwork = this.renderNetwork.bind(this); | ||||
|   } | ||||
|  | ||||
|   renderNetwork(network) { | ||||
|     return ([ | ||||
|         <ListItem key={network.ssid} 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="display1" className={classes.scanningProgress}> | ||||
|               Scanning... | ||||
|             </Typography> | ||||
|           </div> | ||||
|           : | ||||
|           networkList ? | ||||
|           <List> | ||||
|             {networkList.networks.map(this.renderNetwork)} | ||||
|           </List> | ||||
|           : | ||||
|           <div> | ||||
|             <Typography variant="display1" className={classes.scanningProgress}> | ||||
|               {errorMessage} | ||||
|             </Typography> | ||||
|           </div> | ||||
|         } | ||||
|  | ||||
|         <Button variant="raised" 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); | ||||
							
								
								
									
										237
									
								
								interface/src/forms/WiFiSettingsForm.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								interface/src/forms/WiFiSettingsForm.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | ||||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
|  | ||||
| import { withStyles } from 'material-ui/styles'; | ||||
| import Button from 'material-ui/Button'; | ||||
| import { LinearProgress } from 'material-ui/Progress'; | ||||
| import Checkbox from 'material-ui/Checkbox'; | ||||
| import { FormControlLabel } from 'material-ui/Form'; | ||||
| import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; | ||||
| import Typography from 'material-ui/Typography'; | ||||
| import List, { ListItem, ListItemText, ListItemSecondaryAction,  ListItemAvatar } from 'material-ui/List'; | ||||
|  | ||||
| import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes'; | ||||
|  | ||||
| import Avatar from 'material-ui/Avatar'; | ||||
| import IconButton from 'material-ui/IconButton'; | ||||
| import LockIcon from 'material-ui-icons/Lock'; | ||||
| import LockOpenIcon from 'material-ui-icons/LockOpen'; | ||||
| import DeleteIcon from 'material-ui-icons/Delete'; | ||||
|  | ||||
| import isIP from '../validators/isIP'; | ||||
| import isHostname from '../validators/isHostname'; | ||||
| import optional from '../validators/optional'; | ||||
|  | ||||
| const styles = theme => ({ | ||||
|   loadingSettings: { | ||||
|     margin: theme.spacing.unit, | ||||
|   }, | ||||
|   loadingSettingsDetails: { | ||||
|     margin: theme.spacing.unit * 4, | ||||
|     textAlign: "center" | ||||
|   }, | ||||
|   textField: { | ||||
|     width: "100%" | ||||
|   }, | ||||
|   checkboxControl: { | ||||
|     width: "100%" | ||||
|   }, | ||||
|   button: { | ||||
|     marginRight: theme.spacing.unit * 2, | ||||
|     marginTop: theme.spacing.unit * 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 key={selectedNetwork.ssid}> | ||||
|           <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, formRef, wifiSettingsFetched, wifiSettings, errorMessage, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props; | ||||
|     return ( | ||||
|       <div ref={formRef}> | ||||
|         { | ||||
|          !wifiSettingsFetched ? | ||||
|  | ||||
|          <div className={classes.loadingSettings}> | ||||
|            <LinearProgress className={classes.loadingSettingsDetails}/> | ||||
|            <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|              Loading... | ||||
|            </Typography> | ||||
|          </div> | ||||
|  | ||||
|          : wifiSettings ? | ||||
|  | ||||
|       	 <ValidatorForm onSubmit={onSubmit} ref="WiFiSettingsForm"> | ||||
|            { | ||||
|              selectedNetwork ? this.renderSelectedNetwork() : | ||||
|               <TextValidator | ||||
|                 validators={['required', 'matchRegexp:^.{0,32}$']} | ||||
|                 errorMessages={['SSID is required', 'SSID must be 32 characeters or less']} | ||||
|                 name="ssid" | ||||
|                 label="SSID" | ||||
|                 className={classes.textField} | ||||
|                 value={wifiSettings.ssid} | ||||
|                 onChange={handleValueChange('ssid')} | ||||
|                 margin="normal" | ||||
|               /> | ||||
|             } | ||||
|               { | ||||
|                 !isNetworkOpen(selectedNetwork) && | ||||
|          		<TextValidator | ||||
|                   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 && | ||||
|           		[ | ||||
|           			<TextValidator key="local_ip" | ||||
|           			  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 key="gateway_ip" | ||||
|                   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 key="subnet_mask" | ||||
|           			  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 key="dns_ip_1" | ||||
|           			  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 key="dns_ip_2" | ||||
|           			  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" | ||||
|           			/> | ||||
|           		] | ||||
|       		  } | ||||
|  | ||||
|           <Button variant="raised" color="primary" className={classes.button} type="submit"> | ||||
|             Save | ||||
|           </Button> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|  | ||||
|         </ValidatorForm> | ||||
|  | ||||
|         : | ||||
|  | ||||
|         <div className={classes.loadingSettings}> | ||||
|           <Typography variant="display1" className={classes.loadingSettingsDetails}> | ||||
|             {errorMessage} | ||||
|           </Typography> | ||||
|           <Button variant="raised" color="secondary" className={classes.button} onClick={onReset}> | ||||
|       		  Reset | ||||
|       		</Button> | ||||
|         </div> | ||||
|       } | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| WiFiSettingsForm.propTypes = { | ||||
|   classes: PropTypes.object.isRequired, | ||||
|   wifiSettingsFetched: PropTypes.bool.isRequired, | ||||
|   wifiSettings: PropTypes.object, | ||||
|   errorMessage: PropTypes.string, | ||||
|   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); | ||||
							
								
								
									
										35
									
								
								interface/src/helpers/SimpleGet.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								interface/src/helpers/SimpleGet.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| /** | ||||
| * Executes a get request for an endpoint, updating the local state of the calling | ||||
| * component. The calling component must bind setState before using this | ||||
| * function. | ||||
| * | ||||
| * This is designed for re-use in simple situations, we arn't using redux here! | ||||
| */ | ||||
| export const simpleGet = ( | ||||
|   endpointUrl, | ||||
|   setState, | ||||
|   raiseNotification = null, | ||||
|   dataKey="status", | ||||
|   fetchedKey="fetched", | ||||
|   errorMessageKey = "errorMessage" | ||||
| ) => { | ||||
|   setState({ | ||||
|            [dataKey]:null, | ||||
|            [fetchedKey]: false, | ||||
|            [errorMessageKey]:null | ||||
|          }); | ||||
|   fetch(endpointUrl) | ||||
|     .then(response => { | ||||
|       if (response.status === 200) { | ||||
|         return response.json(); | ||||
|       } | ||||
|       throw Error("Invalid status code: " + response.status); | ||||
|     }) | ||||
|     .then(json => {setState({[dataKey]: json, [fetchedKey]:true})}) | ||||
|     .catch(error =>{ | ||||
|       if (raiseNotification) { | ||||
|         raiseNotification("Problem fetching. " + error.message); | ||||
|       } | ||||
|       setState({[dataKey]: null, [fetchedKey]:true, [errorMessageKey]:error.message}); | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										38
									
								
								interface/src/helpers/SimplePost.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								interface/src/helpers/SimplePost.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| /** | ||||
| * Executes a post request for saving data to an endpoint, updating the local | ||||
| * state with the response. The calling component must bind setState before | ||||
| * using this function. | ||||
| * | ||||
| * This is designed for re-use in simple situations, we arn't using redux here! | ||||
| */ | ||||
| export const simplePost = ( | ||||
|   endpointUrl, | ||||
|   state, | ||||
|   setState, | ||||
|   raiseNotification = null, | ||||
|   dataKey="settings", | ||||
|   fetchedKey="fetched", | ||||
|   errorMessageKey = "errorMessage" | ||||
| ) => { | ||||
|   setState({[fetchedKey]: false}); | ||||
|   fetch(endpointUrl, { | ||||
|     method: 'POST', | ||||
|     body: JSON.stringify(state[dataKey]), | ||||
|     headers: new Headers({ | ||||
|       'Content-Type': 'application/json' | ||||
|     }) | ||||
|   }) | ||||
|   .then(response => { | ||||
|     if (response.status === 200) { | ||||
|       return response.json(); | ||||
|     } | ||||
|     throw Error("Invalid status code: " + response.status); | ||||
|   }) | ||||
|   .then(json => { | ||||
|     raiseNotification("Changes successfully applied."); | ||||
|     setState({[dataKey]: json, [fetchedKey]:true}); | ||||
|   }).catch(error => { | ||||
|     raiseNotification("Problem saving. " + error.message); | ||||
|     setState({[dataKey]: null, [fetchedKey]:true, [errorMessageKey]:error.message}); | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										5
									
								
								interface/src/history.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								interface/src/history.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import { createBrowserHistory } from 'history'; | ||||
|  | ||||
| export default createBrowserHistory({ | ||||
|   /* pass a configuration object here if needed */ | ||||
| }) | ||||
							
								
								
									
										16
									
								
								interface/src/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								interface/src/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import React from 'react'; | ||||
| import { render } from 'react-dom'; | ||||
|  | ||||
| import history from './history'; | ||||
| import { Router, Route, Redirect, Switch } from 'react-router'; | ||||
|  | ||||
| import App from './App'; | ||||
|  | ||||
| render(( | ||||
|   <Router history={history}> | ||||
|     <Switch> | ||||
|       <Redirect exact from='/' to='/home'/> | ||||
|       <Route path="/" component={App} /> | ||||
|     </Switch> | ||||
|   </Router> | ||||
| ), document.getElementById("root")) | ||||
							
								
								
									
										6
									
								
								interface/src/validators/isHostname.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								interface/src/validators/isHostname.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| const hostnameLengthRegex = /^.{0,32}$/ | ||||
| const hostnamePatternRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/ | ||||
|  | ||||
| export default function isHostname(hostname) { | ||||
|   return hostnameLengthRegex.test(hostname) && hostnamePatternRegex.test(hostname); | ||||
| } | ||||
							
								
								
									
										5
									
								
								interface/src/validators/isIP.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								interface/src/validators/isIP.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| const ipAddressRegexp = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ | ||||
|  | ||||
| export default function isIp(ipAddress) { | ||||
|   return ipAddressRegexp.test(ipAddress); | ||||
| } | ||||
							
								
								
									
										1
									
								
								interface/src/validators/optional.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								interface/src/validators/optional.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export default validator => value => !value || validator(value); | ||||
							
								
								
									
										1
									
								
								interface/src/validators/or.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								interface/src/validators/or.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export default (validator1, validator2) => value => validator1(value) || validator2(value); | ||||
		Reference in New Issue
	
	Block a user