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