Merge pull request #49 from rjwats/ft_demo_project

Ft demo project
This commit is contained in:
rjwats 2019-09-30 22:06:06 +01:00 committed by GitHub
commit 67eb9d4017
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 2677 additions and 2125 deletions

View File

@ -42,8 +42,9 @@ Resource | Description
---- | ----------- ---- | -----------
[data/](data) | The file system image directory [data/](data) | The file system image directory
[interface/](interface) | React based front end [interface/](interface) | React based front end
[src/](src) | C++ back end for the ESP8266 device [src/](src) | The main.cpp and demo project to get you started
[platformio.ini](platformio.ini) | PlatformIO project configuration file [platformio.ini](platformio.ini) | PlatformIO project configuration file
[lib/framework/](lib/framework) | C++ back end for the ESP8266 device
### Building the firmware ### Building the firmware
@ -247,13 +248,92 @@ There is also a manifest file which contains the app name to use when adding the
} }
``` ```
## Back End Overview ## Back end overview
The back end is a set of REST endpoints hosted by a [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) instance. The source is split up by feature, for example [WiFiScanner.h](src/WiFiScanner.h) implements the end points for scanning for available networks. The back end is a set of REST endpoints hosted by a [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) instance. The ['lib/framework'](lib/framework) directory contains the majority of the back end code. The framework contains of a number of useful utility classes which you can use when extending it. The project also comes with a demo project to give you some help getting started.
There is an abstract class [SettingsService.h](src/SettingsService.h) that provides an easy means of adding configurable services/features to the device. It takes care of writing the settings as JSON to SPIFFS. All you need to do is extend the class with your required configuration and implement the functions which serialize the settings to/from JSON. JSON serialization utilizes the excellent [ArduinoJson](https://github.com/bblanchon/ArduinoJson) library. The framework's source is split up by feature, for example [WiFiScanner.h](lib/framework/WiFiScanner.h) implements the end points for scanning for available networks where as [WiFiSettingsService.h](lib/framework/WiFiSettingsService.h) handles configuring the WiFi settings and managing the WiFi connection.
Here is a example of a service with username and password settings: ### Initializing the framework
The ['src/main.cpp'](src/main.cpp) file constructs the webserver and initializes the framework. You can add endpoints to the server here to support your IoT project. The main loop is also accessable so you can run your own code easily.
The following code creates the web server, esp8266React framework and the demo project instance:
```cpp
AsyncWebServer server(80);
ESP8266React esp8266React(&server, &SPIFFS);
DemoProject demoProject = DemoProject(&server, &SPIFFS, esp8266React.getSecurityManager());
```
Now in the `setup()` function the initialization is performed:
```cpp
void setup() {
// start serial and filesystem
Serial.begin(SERIAL_BAUD_RATE);
// start the file system (must be done before starting the framework)
SPIFFS.begin();
// start the framework and demo project
esp8266React.begin();
// start the demo project
demoProject.begin();
// start the server
server.begin();
}
```
Finally the loop calls the framework's loop function to service the frameworks features. You can add your own code in here, as shown with the demo project:
```cpp
void loop() {
// run the framework's loop function
esp8266React.loop();
// run the demo project's loop function
demoProject.loop();
}
```
### Adding endpoints
There are some simple classes that support adding configurable services/features to the device:
Class | Description
----- | -----------
[SimpleService.h](lib/framework/SimpleService.h) | Exposes an endpoint to read and write settings as JSON. Extend this class and implement the functions which serialize the settings to/from JSON.
[SettingsService.h](lib/framework/SettingsService.h) | As above, however this class also handles persisting the settings as JSON to the file system.
[AdminSettingsService.h](lib/framework/AdminSettingsService.h) | Extends SettingsService to secure the endpoint to administrators only, the authentication predicate can be overridden if required.
The demo project shows how these can be used, explore the framework classes for more examples.
### Security features
The framework has security features to prevent unauthorized use of the device. This is driven by [SecurityManager.h](lib/framework/SecurityManager.h).
On successful authentication, the /rest/signIn endpoint issues a JWT which is then sent using Bearer Authentication. The framework come with built in predicates for verifying a users access level. The built in AuthenticationPredicates can be found in [SecurityManager.h](lib/framework/SecurityManager.h):
Predicate | Description
-------------------- | -----------
NONE_REQUIRED | No authentication is required.
IS_AUTHENTICATED | Any authenticated principal is permitted.
IS_ADMIN | The authenticated principal must be an admin.
You can use the security manager to wrap any web handler with an authentication predicate:
```cpp
server->on("/rest/someService", HTTP_GET,
_securityManager->wrapRequest(std::bind(&SomeService::someService, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
);
```
Alternatively you can extend [AdminSettingsService.h](lib/framework/AdminSettingsService.h) and optionally override `getAuthenticationPredicate()` to secure an endpoint.
## Extending the framework
```cpp ```cpp
#include <SettingsService.h> #include <SettingsService.h>
@ -321,6 +401,7 @@ void reconfigureTheService() {
* [React](https://reactjs.org/) * [React](https://reactjs.org/)
* [Material-UI](https://material-ui-next.com/) * [Material-UI](https://material-ui-next.com/)
* [notistack](https://github.com/iamhosseindhv/notistack)
* [Time](https://github.com/PaulStoffregen/Time) * [Time](https://github.com/PaulStoffregen/Time)
* [NtpClient](https://github.com/gmag11/NtpClient) * [NtpClient](https://github.com/gmag11/NtpClient)
* [ArduinoJson](https://github.com/bblanchon/ArduinoJson) * [ArduinoJson](https://github.com/bblanchon/ArduinoJson)

View File

@ -1,5 +1,5 @@
{ {
"provision_mode": 0, "provision_mode": 0,
"ssid": "ESP8266-React", "ssid": "ESP8266-React",
"password": "esp-react" "password": "esp-react"
} }

View File

@ -0,0 +1,3 @@
{
"blink_speed": 100
}

View File

@ -1,4 +1,4 @@
{ {
"server":"pool.ntp.org", "server": "pool.ntp.org",
"interval":3600 "interval": 3600
} }

View File

@ -1,5 +1,5 @@
{ {
"enabled":true, "enabled": true,
"port": 8266, "port": 8266,
"password": "esp-react" "password": "esp-react"
} }

View File

@ -1,15 +1,15 @@
{ {
"jwt_secret":"esp8266-react", "jwt_secret": "esp8266-react",
"users": [ "users": [
{ {
"username": "admin", "username": "admin",
"password": "admin", "password": "admin",
"admin": true "admin": true
}, },
{ {
"username": "guest", "username": "guest",
"password": "guest", "password": "guest",
"admin": false "admin": false
} }
] ]
} }

View File

@ -1,6 +1,6 @@
{ {
"ssid":"", "ssid": "",
"password":"password", "password": "password",
"hostname":"esp8266-react", "hostname": "esp8266-react",
"static_ip_config":false "static_ip_config": false
} }

View File

@ -1 +1,5 @@
REACT_APP_NAME=ESP8266 React # This is the name of your project. It appears on the sign-in page and in the menu bar.
REACT_APP_PROJECT_NAME=ESP8266 React
# This is the url path your project will be exposed under.
REACT_APP_PROJECT_PATH=project

View File

@ -1 +1,3 @@
REACT_APP_ENDPOINT_ROOT=http://192.168.0.11/rest/ # Change the IP address to that of your ESP device to enable local development of the UI.
# Remember to also enable CORS in platformio.ini before uploading the code to the device.
REACT_APP_ENDPOINT_ROOT=http://192.168.0.20/rest/

File diff suppressed because it is too large Load Diff

View File

@ -3,20 +3,20 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@material-ui/core": "^4.3.1", "@material-ui/core": "^4.4.3",
"@material-ui/icons": "^4.2.1", "@material-ui/icons": "^4.4.3",
"compression-webpack-plugin": "^2.0.0", "compression-webpack-plugin": "^2.0.0",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"notistack": "^0.8.9", "notistack": "^0.8.9",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.8.6", "react": "^16.10.1",
"react-dom": "^16.8.6", "react-dom": "^16.10.1",
"react-form-validator-core": "^0.6.3", "react-form-validator-core": "^0.6.4",
"react-jss": "^10.0.0-alpha.23", "react-jss": "^10.0.0",
"react-material-ui-form-validator": "^2.0.9", "react-material-ui-form-validator": "^2.0.9",
"react-router": "^5.0.1", "react-router": "^5.1.1",
"react-router-dom": "^5.0.1", "react-router-dom": "^5.1.1",
"react-scripts": "3.0.1" "react-scripts": "3.0.1"
}, },
"scripts": { "scripts": {

View File

@ -2,18 +2,18 @@ import React, { Component } from 'react';
import { Redirect, Switch } from 'react-router'; import { Redirect, Switch } from 'react-router';
import { PROJECT_PATH } from './constants/Env';
import * as Authentication from './authentication/Authentication'; import * as Authentication from './authentication/Authentication';
import AuthenticationWrapper from './authentication/AuthenticationWrapper'; import AuthenticationWrapper from './authentication/AuthenticationWrapper';
import AuthenticatedRoute from './authentication/AuthenticatedRoute'; import AuthenticatedRoute from './authentication/AuthenticatedRoute';
import UnauthenticatedRoute from './authentication/UnauthenticatedRoute'; import UnauthenticatedRoute from './authentication/UnauthenticatedRoute';
import SignInPage from './containers/SignInPage'; import SignInPage from './containers/SignInPage';
import WiFiConnection from './sections/WiFiConnection'; import WiFiConnection from './sections/WiFiConnection';
import AccessPoint from './sections/AccessPoint'; import AccessPoint from './sections/AccessPoint';
import NetworkTime from './sections/NetworkTime'; import NetworkTime from './sections/NetworkTime';
import Security from './sections/Security'; import Security from './sections/Security';
import System from './sections/System'; import System from './sections/System';
import ProjectRouting from './project/ProjectRouting';
class AppRouting extends Component { class AppRouting extends Component {
@ -31,6 +31,7 @@ class AppRouting extends Component {
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} /> <AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
<AuthenticatedRoute exact path="/security/*" component={Security} /> <AuthenticatedRoute exact path="/security/*" component={Security} />
<AuthenticatedRoute exact path="/system/*" component={System} /> <AuthenticatedRoute exact path="/system/*" component={System} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
<Redirect to="/" /> <Redirect to="/" />
</Switch> </Switch>
</AuthenticationWrapper> </AuthenticationWrapper>

View File

@ -1,4 +1,5 @@
import history from '../history'; import history from '../history';
import { PROJECT_PATH } from '../constants/Env';
export const ACCESS_TOKEN = 'access_token'; export const ACCESS_TOKEN = 'access_token';
export const LOGIN_PATHNAME = 'loginPathname'; export const LOGIN_PATHNAME = 'loginPathname';
@ -21,7 +22,7 @@ export function fetchLoginRedirect() {
const loginSearch = localStorage.getItem(LOGIN_SEARCH); const loginSearch = localStorage.getItem(LOGIN_SEARCH);
clearLoginRedirect(); clearLoginRedirect();
return { return {
pathname: loginPathname || "/wifi/", pathname: loginPathname || `/${PROJECT_PATH}/`,
search: (loginPathname && loginSearch) || undefined search: (loginPathname && loginSearch) || undefined
}; };
} }

View File

@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
const useStyles = makeStyles(theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
}
}));
export default function LoadingNotification(props) {
const classes = useStyles();
const { fetched, errorMessage, onReset, render } = props;
return (
<div>
{
fetched ?
errorMessage ?
<div className={classes.loadingSettings}>
<Typography variant="h6" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
:
render()
:
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails} />
<Typography variant="h6" className={classes.loadingSettingsDetails}>
Loading...
</Typography>
</div>
}
</div>
);
}
LoadingNotification.propTypes = {
fetched: PropTypes.bool.isRequired,
onReset: PropTypes.func.isRequired,
errorMessage: PropTypes.string,
render: PropTypes.func.isRequired
};

View File

@ -30,7 +30,8 @@ import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions'; import CardActions from '@material-ui/core/CardActions';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import { APP_NAME } from '../constants/App'; import ProjectMenu from '../project/ProjectMenu';
import { PROJECT_NAME } from '../constants/Env';
import { withAuthenticationContext } from '../authentication/Context.js'; import { withAuthenticationContext } from '../authentication/Context.js';
const drawerWidth = 290; const drawerWidth = 290;
@ -65,8 +66,7 @@ const styles = theme => ({
width: drawerWidth, width: drawerWidth,
}, },
content: { content: {
flexGrow: 1, flexGrow: 1
padding: theme.spacing(),
}, },
authMenu: { authMenu: {
zIndex: theme.zIndex.tooltip, zIndex: theme.zIndex.tooltip,
@ -112,11 +112,13 @@ class MenuAppBar extends React.Component {
<div> <div>
<Toolbar> <Toolbar>
<Typography variant="h6" color="primary"> <Typography variant="h6" color="primary">
{APP_NAME} {PROJECT_NAME}
</Typography> </Typography>
<Divider absolute /> <Divider absolute />
</Toolbar> </Toolbar>
<Divider /> <Divider />
<ProjectMenu />
<Divider />
<List> <List>
<ListItem to='/wifi/' selected={path.startsWith('/wifi/')} button component={Link}> <ListItem to='/wifi/' selected={path.startsWith('/wifi/')} button component={Link}>
<ListItemIcon> <ListItemIcon>
@ -195,7 +197,7 @@ class MenuAppBar extends React.Component {
</CardContent> </CardContent>
<Divider /> <Divider />
<CardActions className={classes.authMenuActions}> <CardActions className={classes.authMenuActions}>
<Button className={classes.authMenuButtons} variant="contained" color="primary" onClick={authenticationContext.signOut}>Sign Out</Button> <Button variant="contained" color="primary" onClick={authenticationContext.signOut}>Sign Out</Button>
</CardActions> </CardActions>
</Card> </Card>
</ClickAwayListener> </ClickAwayListener>

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { withSnackbar } from 'notistack'; import { withSnackbar } from 'notistack';
import { redirectingAuthorizedFetch } from '../authentication/Authentication'; import { redirectingAuthorizedFetch } from '../authentication/Authentication';
/* /*
* It is unlikely this application will grow complex enough to require redux. * It is unlikely this application will grow complex enough to require redux.
* *
@ -51,10 +52,11 @@ export const restComponent = (endpointUrl, FormComponent) => {
}) })
.then(json => { this.setState({ data: json, fetched: true }) }) .then(json => { this.setState({ data: json, fetched: true }) })
.catch(error => { .catch(error => {
this.props.enqueueSnackbar("Problem fetching: " + error.message, { const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, {
variant: 'error', variant: 'error',
}); });
this.setState({ data: null, fetched: true, errorMessage: error.message }); this.setState({ data: null, fetched: true, errorMessage });
}); });
} }
@ -79,19 +81,26 @@ export const restComponent = (endpointUrl, FormComponent) => {
}); });
this.setState({ data: json, fetched: true }); this.setState({ data: json, fetched: true });
}).catch(error => { }).catch(error => {
this.props.enqueueSnackbar("Problem saving: " + error.message, { const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem saving: " + errorMessage, {
variant: 'error', variant: 'error',
}); });
this.setState({ data: null, fetched: true, errorMessage: error.message }); this.setState({ data: null, fetched: true, errorMessage });
}); });
} }
handleValueChange = name => event => { handleValueChange = name => (event) => {
const { data } = this.state; const { data } = this.state;
data[name] = event.target.value; data[name] = event.target.value;
this.setState({ data }); this.setState({ data });
}; };
handleSliderChange = name => (event, newValue) => {
const { data } = this.state;
data[name] = newValue;
this.setState({ data });
};
handleCheckboxChange = name => event => { handleCheckboxChange = name => event => {
const { data } = this.state; const { data } = this.state;
data[name] = event.target.checked; data[name] = event.target.checked;
@ -102,6 +111,7 @@ export const restComponent = (endpointUrl, FormComponent) => {
return <FormComponent return <FormComponent
handleValueChange={this.handleValueChange} handleValueChange={this.handleValueChange}
handleCheckboxChange={this.handleCheckboxChange} handleCheckboxChange={this.handleCheckboxChange}
handleSliderChange={this.handleSliderChange}
setData={this.setData} setData={this.setData}
saveData={this.saveData} saveData={this.saveData}
loadData={this.loadData} loadData={this.loadData}

View File

@ -8,15 +8,15 @@ import Typography from '@material-ui/core/Typography';
const styles = theme => ({ const styles = theme => ({
content: { content: {
padding: theme.spacing(2), padding: theme.spacing(2),
margin: theme.spacing(2), margin: theme.spacing(3),
} }
}); });
function SectionContent(props) { function SectionContent(props) {
const { children, classes, title } = props; const { children, classes, title, titleGutter } = props;
return ( return (
<Paper className={classes.content}> <Paper className={classes.content}>
<Typography variant="h6"> <Typography variant="h6" gutterBottom={titleGutter}>
{title} {title}
</Typography> </Typography>
{children} {children}
@ -30,7 +30,8 @@ SectionContent.propTypes = {
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
PropTypes.node PropTypes.node
]).isRequired, ]).isRequired,
title: PropTypes.string.isRequired title: PropTypes.string.isRequired,
titleGutter: PropTypes.bool
}; };
export default withStyles(styles)(SectionContent); export default withStyles(styles)(SectionContent);

View File

@ -1 +0,0 @@
export const APP_NAME = process.env.REACT_APP_NAME;

View File

@ -1,4 +1,4 @@
const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT; import { ENDPOINT_ROOT } from '../constants/Env';
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus"; export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + "ntpStatus";
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings"; export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "ntpSettings";

View File

@ -0,0 +1,3 @@
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME;
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH;
export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT;

View File

@ -1,7 +1,8 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { AP_SETTINGS_ENDPOINT } from '../constants/Endpoints'; import { AP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
import APSettingsForm from '../forms/APSettingsForm'; import APSettingsForm from '../forms/APSettingsForm';
@ -12,16 +13,21 @@ class APSettings extends Component {
} }
render() { render() {
const { data, fetched, errorMessage } = this.props; const { fetched, errorMessage, data, saveData, loadData, handleValueChange } = this.props;
return ( return (
<SectionContent title="AP Settings"> <SectionContent title="AP Settings">
<APSettingsForm <LoadingNotification
apSettings={data} onReset={loadData}
apSettingsFetched={fetched} fetched={fetched}
errorMessage={errorMessage} errorMessage={errorMessage}
onSubmit={this.props.saveData} render={() =>
onReset={this.props.loadData} <APSettingsForm
handleValueChange={this.props.handleValueChange} apSettings={data}
onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
/>
}
/> />
</SectionContent> </SectionContent>
) )

View File

@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
@ -15,6 +13,7 @@ import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import ComputerIcon from '@material-ui/icons/Computer'; import ComputerIcon from '@material-ui/icons/Computer';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent' import SectionContent from '../components/SectionContent'
import * as Highlight from '../constants/Highlight'; import * as Highlight from '../constants/Highlight';
@ -27,10 +26,6 @@ const styles = theme => ({
["apStatus_" + Highlight.IDLE]: { ["apStatus_" + Highlight.IDLE]: {
backgroundColor: theme.palette.highlight_idle backgroundColor: theme.palette.highlight_idle
}, },
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: { button: {
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
@ -96,9 +91,7 @@ class APStatus extends Component {
return ( return (
<div> <div>
<List> <List>
<Fragment> {this.createListItems(data, classes)}
{this.createListItems(data, classes)}
</Fragment>
</List> </List>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}> <Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh Refresh
@ -108,30 +101,17 @@ class APStatus extends Component {
} }
render() { render() {
const { data, fetched, errorMessage, classes } = this.props; const { fetched, errorMessage, data, loadData, classes } = this.props;
return ( return (
<SectionContent title="AP Status"> <SectionContent title="AP Status">
{ <LoadingNotification
!fetched ? onReset={loadData}
<div> fetched={fetched}
<LinearProgress className={classes.fetching} /> errorMessage={errorMessage}
<Typography variant="h4" className={classes.fetching}> render={
Loading... () => this.renderAPStatus(data, classes)
</Typography> }
</div> />
:
data ? this.renderAPStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
</SectionContent> </SectionContent>
) )
} }

View File

@ -2,8 +2,9 @@ import React, { Component } from 'react';
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints'; import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import ManageUsersForm from '../forms/ManageUsersForm'; import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
import ManageUsersForm from '../forms/ManageUsersForm';
class ManageUsers extends Component { class ManageUsers extends Component {
@ -12,17 +13,22 @@ class ManageUsers extends Component {
} }
render() { render() {
const { data, fetched, errorMessage } = this.props; const { fetched, errorMessage, data, saveData, loadData, setData, handleValueChange } = this.props;
return ( return (
<SectionContent title="Manage Users"> <SectionContent title="Manage Users" titleGutter>
<ManageUsersForm <LoadingNotification
userData={data} onReset={loadData}
userDataFetched={fetched} fetched={fetched}
errorMessage={errorMessage} errorMessage={errorMessage}
onSubmit={this.props.saveData} render={() =>
onReset={this.props.loadData} <ManageUsersForm
setData={this.props.setData} userData={data}
handleValueChange={this.props.handleValueChange} onSubmit={saveData}
onReset={loadData}
setData={setData}
handleValueChange={handleValueChange}
/>
}
/> />
</SectionContent> </SectionContent>
) )

View File

@ -1,27 +1,33 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints'; import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import {restComponent} from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
import NTPSettingsForm from '../forms/NTPSettingsForm'; import NTPSettingsForm from '../forms/NTPSettingsForm';
class NTPSettings extends Component { class NTPSettings extends Component {
componentDidMount() { componentDidMount() {
this.props.loadData(); this.props.loadData();
} }
render() { render() {
const { data, fetched, errorMessage } = this.props; const { fetched, errorMessage, data, saveData, loadData, handleValueChange } = this.props;
return ( return (
<SectionContent title="NTP Settings"> <SectionContent title="NTP Settings">
<NTPSettingsForm <LoadingNotification
ntpSettings={data} onReset={loadData}
ntpSettingsFetched={fetched} fetched={fetched}
errorMessage={errorMessage} errorMessage={errorMessage}
onSubmit={this.props.saveData} render={() =>
onReset={this.props.loadData} <NTPSettingsForm
handleValueChange={this.props.handleValueChange} ntpSettings={data}
onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
/>
}
/> />
</SectionContent> </SectionContent>
) )

View File

@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemAvatar from '@material-ui/core/ListItemAvatar';
@ -22,6 +20,7 @@ import * as Highlight from '../constants/Highlight';
import { unixTimeToTimeAndDate } from '../constants/TimeFormat'; import { unixTimeToTimeAndDate } from '../constants/TimeFormat';
import { NTP_STATUS_ENDPOINT } from '../constants/Endpoints'; import { NTP_STATUS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
import moment from 'moment'; import moment from 'moment';
@ -36,10 +35,6 @@ const styles = theme => ({
["ntpStatus_" + Highlight.WARN]: { ["ntpStatus_" + Highlight.WARN]: {
backgroundColor: theme.palette.highlight_warn backgroundColor: theme.palette.highlight_warn
}, },
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: { button: {
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
@ -131,32 +126,19 @@ class NTPStatus extends Component {
} }
render() { render() {
const { data, fetched, errorMessage, classes } = this.props; const { data, fetched, errorMessage, loadData, classes } = this.props;
return ( return (
<SectionContent title="NTP Status"> <SectionContent title="NTP Status">
{ <LoadingNotification
!fetched ? onReset={loadData}
<div> fetched={fetched}
<LinearProgress className={classes.fetching} /> errorMessage={errorMessage}
<Typography variant="h4" className={classes.fetching}> render={
Loading... () => this.renderNTPStatus(data, classes)
</Typography> }
</div> />
:
data ? this.renderNTPStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
</SectionContent> </SectionContent>
) );
} }
} }

View File

@ -1,28 +1,34 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints'; import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import {restComponent} from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
import OTASettingsForm from '../forms/OTASettingsForm'; import OTASettingsForm from '../forms/OTASettingsForm';
class OTASettings extends Component { class OTASettings extends Component {
componentDidMount() { componentDidMount() {
this.props.loadData(); this.props.loadData();
} }
render() { render() {
const { data, fetched, errorMessage } = this.props; const { fetched, errorMessage, data, saveData, loadData, handleValueChange, handleCheckboxChange } = this.props;
return ( return (
<SectionContent title="OTA Settings"> <SectionContent title="OTA Settings">
<OTASettingsForm <LoadingNotification
otaSettings={data} onReset={loadData}
otaSettingsFetched={fetched} fetched={fetched}
errorMessage={errorMessage} errorMessage={errorMessage}
onSubmit={this.props.saveData} render={() =>
onReset={this.props.loadData} <OTASettingsForm
handleValueChange={this.props.handleValueChange} otaSettings={data}
handleCheckboxChange={this.props.handleCheckboxChange} onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
handleCheckboxChange={handleCheckboxChange}
/>
}
/> />
</SectionContent> </SectionContent>
) )

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints'; import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SecuritySettingsForm from '../forms/SecuritySettingsForm'; import SecuritySettingsForm from '../forms/SecuritySettingsForm';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
@ -12,16 +13,21 @@ class SecuritySettings extends Component {
} }
render() { render() {
const { data, fetched, errorMessage } = this.props; const { data, fetched, errorMessage, saveData, loadData, handleValueChange } = this.props;
return ( return (
<SectionContent title="Security Settings"> <SectionContent title="Security Settings">
<SecuritySettingsForm <LoadingNotification
securitySettings={data} onReset={loadData}
securitySettingsFetched={fetched} fetched={fetched}
errorMessage={errorMessage} errorMessage={errorMessage}
onSubmit={this.props.saveData} render={() =>
onReset={this.props.loadData} <SecuritySettingsForm
handleValueChange={this.props.handleValueChange} securitySettings={data}
onSubmit={saveData}
onReset={loadData}
handleValueChange={handleValueChange}
/>
}
/> />
</SectionContent> </SectionContent>
) )

View File

@ -4,7 +4,7 @@ import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Fab from '@material-ui/core/Fab'; import Fab from '@material-ui/core/Fab';
import { APP_NAME } from '../constants/App'; import { PROJECT_NAME } from '../constants/Env';
import ForwardIcon from '@material-ui/icons/Forward'; import ForwardIcon from '@material-ui/icons/Forward';
import { withSnackbar } from 'notistack'; import { withSnackbar } from 'notistack';
import { SIGN_IN_ENDPOINT } from '../constants/Endpoints'; import { SIGN_IN_ENDPOINT } from '../constants/Endpoints';
@ -97,7 +97,7 @@ class SignInPage extends Component {
return ( return (
<div className={classes.loginPage}> <div className={classes.loginPage}>
<Paper className={classes.loginPanel}> <Paper className={classes.loginPanel}>
<Typography variant="h4">{APP_NAME}</Typography> <Typography variant="h4">{PROJECT_NAME}</Typography>
<ValidatorForm onSubmit={this.onSubmit}> <ValidatorForm onSubmit={this.onSubmit}>
<TextValidator <TextValidator
disabled={processing} disabled={processing}

View File

@ -2,8 +2,6 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemAvatar from '@material-ui/core/ListItemAvatar';
@ -16,16 +14,12 @@ import ShowChartIcon from '@material-ui/icons/ShowChart';
import SdStorageIcon from '@material-ui/icons/SdStorage'; import SdStorageIcon from '@material-ui/icons/SdStorage';
import DataUsageIcon from '@material-ui/icons/DataUsage'; import DataUsageIcon from '@material-ui/icons/DataUsage';
import { SYSTEM_STATUS_ENDPOINT } from '../constants/Endpoints'; import { SYSTEM_STATUS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
const styles = theme => ({ const styles = theme => ({
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: { button: {
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
@ -85,12 +79,12 @@ class SystemStatus extends Component {
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary="Flash Chip Size" secondary={data.flash_chip_size + ' bytes'} /> <ListItemText primary="Flash Chip Size" secondary={data.flash_chip_size + ' bytes'} />
</ListItem> </ListItem>
<Divider variant="inset" component="li" /> <Divider variant="inset" component="li" />
</Fragment> </Fragment>
); );
} }
renderNTPStatus(data, classes) { renderSystemStatus(data, classes) {
return ( return (
<div> <div>
<List> <List>
@ -104,29 +98,17 @@ class SystemStatus extends Component {
} }
render() { render() {
const { data, fetched, errorMessage, classes } = this.props; const { data, fetched, errorMessage, loadData, classes } = this.props;
return ( return (
<SectionContent title="System Status"> <SectionContent title="System Status">
{ <LoadingNotification
!fetched ? onReset={loadData}
<div> fetched={fetched}
<LinearProgress className={classes.fetching} /> errorMessage={errorMessage}
<Typography variant="h4" className={classes.fetching}> render={
Loading... () => this.renderSystemStatus(data, classes)
</Typography> }
</div> />
:
data ? this.renderNTPStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
</SectionContent> </SectionContent>
) )
} }

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withSnackbar } from 'notistack';
import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../constants/Endpoints'; import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../constants/Endpoints';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
import WiFiNetworkSelector from '../forms/WiFiNetworkSelector'; import WiFiNetworkSelector from '../forms/WiFiNetworkSelector';
import { withSnackbar } from 'notistack';
import { redirectingAuthorizedFetch } from '../authentication/Authentication'; import { redirectingAuthorizedFetch } from '../authentication/Authentication';
const NUM_POLLS = 10 const NUM_POLLS = 10

View File

@ -1,8 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { WIFI_SETTINGS_ENDPOINT } from '../constants/Endpoints'; import { WIFI_SETTINGS_ENDPOINT } from '../constants/Endpoints';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import SectionContent from '../components/SectionContent'; import SectionContent from '../components/SectionContent';
import WiFiSettingsForm from '../forms/WiFiSettingsForm'; import WiFiSettingsForm from '../forms/WiFiSettingsForm';
@ -18,10 +19,10 @@ class WiFiSettings extends Component {
const { selectedNetwork } = this.props; const { selectedNetwork } = this.props;
if (selectedNetwork) { if (selectedNetwork) {
var wifiSettings = { var wifiSettings = {
ssid:selectedNetwork.ssid, ssid: selectedNetwork.ssid,
password:"", password: "",
hostname:"esp8266-react", hostname: "esp8266-react",
static_ip_config:false, static_ip_config: false,
} }
this.props.setData(wifiSettings); this.props.setData(wifiSettings);
} else { } else {
@ -35,19 +36,24 @@ class WiFiSettings extends Component {
} }
render() { render() {
const { data, fetched, errorMessage, selectedNetwork } = this.props; const { data, fetched, errorMessage, saveData, loadData, handleValueChange, handleCheckboxChange, selectedNetwork, deselectNetwork } = this.props;
return ( return (
<SectionContent title="WiFi Settings"> <SectionContent title="WiFi Settings">
<WiFiSettingsForm <LoadingNotification
wifiSettings={data} onReset={loadData}
wifiSettingsFetched={fetched} fetched={fetched}
errorMessage={errorMessage} errorMessage={errorMessage}
selectedNetwork={selectedNetwork} render={() =>
deselectNetwork={this.props.deselectNetwork} <WiFiSettingsForm
onSubmit={this.props.saveData} wifiSettings={data}
onReset={this.deselectNetworkAndLoadData} selectedNetwork={selectedNetwork}
handleValueChange={this.props.handleValueChange} deselectNetwork={deselectNetwork}
handleCheckboxChange={this.props.handleCheckboxChange} onSubmit={saveData}
onReset={this.deselectNetworkAndLoadData}
handleValueChange={handleValueChange}
handleCheckboxChange={handleCheckboxChange}
/>
}
/> />
</SectionContent> </SectionContent>
) )

View File

@ -2,14 +2,10 @@ import React, { Component, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import Avatar from '@material-ui/core/Avatar'; import Avatar from '@material-ui/core/Avatar';
import Divider from '@material-ui/core/Divider'; import Divider from '@material-ui/core/Divider';
import WifiIcon from '@material-ui/icons/Wifi'; import WifiIcon from '@material-ui/icons/Wifi';
@ -23,6 +19,7 @@ import { WIFI_STATUS_ENDPOINT } from '../constants/Endpoints';
import { isConnected, connectionStatus, connectionStatusHighlight } from '../constants/WiFiConnectionStatus'; import { isConnected, connectionStatus, connectionStatusHighlight } from '../constants/WiFiConnectionStatus';
import * as Highlight from '../constants/Highlight'; import * as Highlight from '../constants/Highlight';
import { restComponent } from '../components/RestComponent'; import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
const styles = theme => ({ const styles = theme => ({
["wifiStatus_" + Highlight.IDLE]: { ["wifiStatus_" + Highlight.IDLE]: {
@ -37,10 +34,6 @@ const styles = theme => ({
["wifiStatus_" + Highlight.WARN]: { ["wifiStatus_" + Highlight.WARN]: {
backgroundColor: theme.palette.highlight_warn backgroundColor: theme.palette.highlight_warn
}, },
fetching: {
margin: theme.spacing(4),
textAlign: "center"
},
button: { button: {
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
@ -145,32 +138,21 @@ class WiFiStatus extends Component {
} }
render() { render() {
const { data, fetched, errorMessage, classes } = this.props; const { data, fetched, errorMessage, loadData, classes } = this.props;
return ( return (
<SectionContent title="WiFi Status"> <SectionContent title="WiFi Status">
{ <LoadingNotification
!fetched ? onReset={loadData}
<div> fetched={fetched}
<LinearProgress className={classes.fetching} /> errorMessage={errorMessage}
<Typography variant="h4" className={classes.fetching}> render={
Loading... () => this.renderWiFiStatus(data, classes)
</Typography> }
</div> />
:
data ? this.renderWiFiStatus(data, classes)
:
<div>
<Typography variant="h4" className={classes.fetching}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
Refresh
</Button>
</div>
}
</SectionContent> </SectionContent>
) );
} }
} }
export default restComponent(WIFI_STATUS_ENDPOINT, withStyles(styles)(WiFiStatus)); export default restComponent(WIFI_STATUS_ENDPOINT, withStyles(styles)(WiFiStatus));

View File

@ -1,29 +1,19 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import { isAPEnabled } from '../constants/WiFiAPModes';
import PasswordValidator from '../components/PasswordValidator';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography';
import MenuItem from '@material-ui/core/MenuItem'; import MenuItem from '@material-ui/core/MenuItem';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import {isAPEnabled} from '../constants/WiFiAPModes';
import PasswordValidator from '../components/PasswordValidator';
const styles = theme => ({ const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: { textField: {
width: "100%" width: "100%"
}, },
selectField:{ selectField: {
width: "100%", width: "100%",
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
marginBottom: theme.spacing(0.5) marginBottom: theme.spacing(0.5)
@ -37,86 +27,54 @@ const styles = theme => ({
class APSettingsForm extends React.Component { class APSettingsForm extends React.Component {
render() { render() {
const { classes, apSettingsFetched, apSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props; const { classes, apSettings, handleValueChange, onSubmit, onReset } = this.props;
return ( return (
<div> <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>
{ {
!apSettingsFetched ? isAPEnabled(apSettings.provision_mode) &&
<Fragment>
<div className={classes.loadingSettings}> <TextValidator
<LinearProgress className={classes.loadingSettingsDetails}/> validators={['required', 'matchRegexp:^.{1,32}$']}
<Typography variant="h4" className={classes.loadingSettingsDetails}> errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
Loading... name="ssid"
</Typography> label="Access Point SSID"
</div> className={classes.textField}
value={apSettings.ssid}
: apSettings ? onChange={handleValueChange('ssid')}
margin="normal"
<ValidatorForm onSubmit={onSubmit} ref="APSettingsForm"> />
<PasswordValidator
<SelectValidator name="provision_mode" label="Provide Access Point..." value={apSettings.provision_mode} className={classes.selectField} validators={['required', 'matchRegexp:^.{1,64}$']}
onChange={handleValueChange('provision_mode')}> errorMessages={['Access Point Password is required', 'Access Point Password must be 64 characters or less']}
<MenuItem value={0}>Always</MenuItem> name="password"
<MenuItem value={1}>When WiFi Disconnected</MenuItem> label="Access Point Password"
<MenuItem value={2}>Never</MenuItem> className={classes.textField}
</SelectValidator> value={apSettings.password}
onChange={handleValueChange('password')}
{ margin="normal"
isAPEnabled(apSettings.provision_mode) && />
<Fragment> </Fragment>
<TextValidator }
validators={['required', 'matchRegexp:^.{1,32}$']} <Button variant="contained" color="primary" className={classes.button} type="submit">
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']} Save
name="ssid" </Button>
label="Access Point SSID" <Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
className={classes.textField} Reset
value={apSettings.ssid} </Button>
onChange={handleValueChange('ssid')} </ValidatorForm>
margin="normal"
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['Access Point Password is required', 'Access Point Password must be 64 characters or less']}
name="password"
label="Access Point Password"
className={classes.textField}
value={apSettings.password}
onChange={handleValueChange('password')}
margin="normal"
/>
</Fragment>
}
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
}
</div>
); );
} }
} }
APSettingsForm.propTypes = { APSettingsForm.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
apSettingsFetched: PropTypes.bool.isRequired,
apSettings: PropTypes.object, apSettings: PropTypes.object,
errorMessage: PropTypes.string,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired,
handleValueChange: PropTypes.func.isRequired handleValueChange: PropTypes.func.isRequired

View File

@ -5,7 +5,6 @@ import { ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Table from '@material-ui/core/Table'; import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody'; import TableBody from '@material-ui/core/TableBody';
@ -14,7 +13,6 @@ import TableFooter from '@material-ui/core/TableFooter';
import TableHead from '@material-ui/core/TableHead'; import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow'; import TableRow from '@material-ui/core/TableRow';
import Box from '@material-ui/core/Box'; import Box from '@material-ui/core/Box';
import EditIcon from '@material-ui/icons/Edit'; import EditIcon from '@material-ui/icons/Edit';
import DeleteIcon from '@material-ui/icons/Delete'; import DeleteIcon from '@material-ui/icons/Delete';
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from '@material-ui/icons/Close';
@ -25,22 +23,12 @@ import UserForm from './UserForm';
import { withAuthenticationContext } from '../authentication/Context'; import { withAuthenticationContext } from '../authentication/Context';
const styles = theme => ({ const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
button: { button: {
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
}, },
table: { table: {
'& td, & th': { padding: theme.spacing(0.5) } '& td, & th': { padding: theme.spacing(0.5) }
},
actions: {
whiteSpace: "nowrap"
} }
}); });
@ -134,98 +122,80 @@ class ManageUsersForm extends React.Component {
} }
render() { render() {
const { classes, userData, userDataFetched, errorMessage, onReset } = this.props; const { classes, userData, onReset } = this.props;
const { user, creating } = this.state; const { user, creating } = this.state;
return ( return (
!userDataFetched ? <Fragment>
<div className={classes.loadingSettings}> <ValidatorForm onSubmit={this.onSubmit}>
<LinearProgress className={classes.loadingSettingsDetails} /> <Table className={classes.table}>
<Typography variant="h4" className={classes.loadingSettingsDetails}> <TableHead>
Loading... <TableRow>
</Typography> <TableCell>Username</TableCell>
</div> <TableCell align="center">Admin?</TableCell>
: <TableCell />
userData ? </TableRow>
<Fragment> </TableHead>
<ValidatorForm onSubmit={this.onSubmit}> <TableBody>
<Table className={classes.table}> {userData.users.sort(compareUsers).map(user => (
<TableHead> <TableRow key={user.username}>
<TableRow> <TableCell component="th" scope="row">
<TableCell>Username</TableCell> {user.username}
<TableCell align="center">Admin?</TableCell> </TableCell>
<TableCell /> <TableCell align="center">
</TableRow> {
</TableHead> user.admin ? <CheckIcon /> : <CloseIcon />
<TableBody> }
{userData.users.sort(compareUsers).map(user => ( </TableCell>
<TableRow key={user.username}> <TableCell align="center">
<TableCell component="th" scope="row"> <IconButton aria-label="Delete" onClick={() => this.removeUser(user)}>
{user.username} <DeleteIcon />
</TableCell> </IconButton>
<TableCell align="center"> <IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}>
{ <EditIcon />
user.admin ? <CheckIcon /> : <CloseIcon /> </IconButton>
} </TableCell>
</TableCell> </TableRow>
<TableCell align="center"> ))}
<IconButton aria-label="Delete" onClick={() => this.removeUser(user)}> </TableBody>
<DeleteIcon /> <TableFooter>
</IconButton> <TableRow>
<IconButton aria-label="Edit" onClick={() => this.startEditingUser(user)}> <TableCell colSpan={2} />
<EditIcon /> <TableCell align="center">
</IconButton> <Button variant="contained" color="secondary" onClick={this.createUser}>
</TableCell> Add User
</TableRow> </Button>
))} </TableCell>
</TableBody> </TableRow>
<TableFooter> </TableFooter>
<TableRow> </Table>
<TableCell colSpan={2} /> {
<TableCell align="center"> this.noAdminConfigured() &&
<Button variant="contained" color="secondary" onClick={this.createUser}> <Typography component="div" variant="body1">
Add User <Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
</Button> You must have at least one admin user configured.
</TableCell> </Box>
</TableRow>
</TableFooter>
</Table>
{
this.noAdminConfigured() &&
<Typography component="div" variant="body1">
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
You must have at least one admin user configured.
</Box>
</Typography>
}
<Button variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
{
user &&
<UserForm
user={user}
creating={creating}
onDoneEditing={this.doneEditingUser}
onCancelEditing={this.cancelEditingUser}
handleValueChange={this.handleUserValueChange}
handleCheckboxChange={this.handleUserCheckboxChange}
uniqueUsername={this.uniqueUsername}
/>
}
</Fragment>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography> </Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}> }
Reset <Button variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
</Button> Save
</div> </Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
{
user &&
<UserForm
user={user}
creating={creating}
onDoneEditing={this.doneEditingUser}
onCancelEditing={this.cancelEditingUser}
handleValueChange={this.handleUserValueChange}
handleCheckboxChange={this.handleUserCheckboxChange}
uniqueUsername={this.uniqueUsername}
/>
}
</Fragment>
); );
} }
@ -234,8 +204,6 @@ class ManageUsersForm extends React.Component {
ManageUsersForm.propTypes = { ManageUsersForm.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
userData: PropTypes.object, userData: PropTypes.object,
userDataFetched: PropTypes.bool.isRequired,
errorMessage: PropTypes.string,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired,
setData: PropTypes.func.isRequired, setData: PropTypes.func.isRequired,

View File

@ -1,24 +1,15 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import Typography from '@material-ui/core/Typography';
import isIP from '../validators/isIP'; import isIP from '../validators/isIP';
import isHostname from '../validators/isHostname'; import isHostname from '../validators/isHostname';
import or from '../validators/or'; import or from '../validators/or';
const styles = theme => ({ const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: { textField: {
width: "100%" width: "100%"
}, },
@ -35,76 +26,44 @@ class NTPSettingsForm extends React.Component {
} }
render() { render() {
const { classes, ntpSettingsFetched, ntpSettings, errorMessage, handleValueChange, onSubmit, onReset } = this.props; const { classes, ntpSettings, handleValueChange, onSubmit, onReset } = this.props;
return ( return (
<div> <ValidatorForm onSubmit={onSubmit}>
{ <TextValidator
!ntpSettingsFetched ? validators={['required', 'isIPOrHostname']}
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
<div className={classes.loadingSettings}> name="server"
<LinearProgress className={classes.loadingSettingsDetails}/> label="Server"
<Typography variant="h4" className={classes.loadingSettingsDetails}> className={classes.textField}
Loading... value={ntpSettings.server}
</Typography> onChange={handleValueChange('server')}
</div> margin="normal"
/>
: ntpSettings ? <TextValidator
validators={['required', 'isNumber', 'minNumber:60', 'maxNumber:86400']}
<ValidatorForm onSubmit={onSubmit}> 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"
<TextValidator label="Interval (Seconds)"
validators={['required', 'isIPOrHostname']} className={classes.textField}
errorMessages={['Server is required', "Not a valid IP address or hostname"]} value={ntpSettings.interval}
name="server" type="number"
label="Server" onChange={handleValueChange('interval')}
className={classes.textField} margin="normal"
value={ntpSettings.server} />
onChange={handleValueChange('server')} <Button variant="contained" color="primary" className={classes.button} type="submit">
margin="normal" Save
/> </Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
<TextValidator Reset
validators={['required','isNumber','minNumber:60','maxNumber:86400']} </Button>
errorMessages={['Interval is required','Interval must be a number','Must be at least 60 seconds',"Must not be more than 86400 seconds (24 hours)"]} </ValidatorForm>
name="interval"
label="Interval (Seconds)"
className={classes.textField}
value={ntpSettings.interval}
type="number"
onChange={handleValueChange('interval')}
margin="normal"
/>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
}
</div>
); );
} }
} }
NTPSettingsForm.propTypes = { NTPSettingsForm.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
ntpSettingsFetched: PropTypes.bool.isRequired,
ntpSettings: PropTypes.object, ntpSettings: PropTypes.object,
errorMessage: PropTypes.string,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired,
handleValueChange: PropTypes.func.isRequired, handleValueChange: PropTypes.func.isRequired,

View File

@ -4,9 +4,7 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Switch from '@material-ui/core/Switch'; import Switch from '@material-ui/core/Switch';
import LinearProgress from '@material-ui/core/LinearProgress';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import Typography from '@material-ui/core/Typography';
import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormControlLabel from '@material-ui/core/FormControlLabel';
import isIP from '../validators/isIP'; import isIP from '../validators/isIP';
@ -15,13 +13,6 @@ import or from '../validators/or';
import PasswordValidator from '../components/PasswordValidator'; import PasswordValidator from '../components/PasswordValidator';
const styles = theme => ({ const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
switchControl: { switchControl: {
width: "100%", width: "100%",
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
@ -43,88 +34,55 @@ class OTASettingsForm extends React.Component {
} }
render() { render() {
const { classes, otaSettingsFetched, otaSettings, errorMessage, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props; const { classes, otaSettings, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
return ( return (
<div> <ValidatorForm onSubmit={onSubmit}>
{ <FormControlLabel className={classes.switchControl}
!otaSettingsFetched ? control={
<Switch
<div className={classes.loadingSettings}> checked={otaSettings.enabled}
<LinearProgress className={classes.loadingSettingsDetails}/> onChange={handleCheckboxChange('enabled')}
<Typography variant="h4" className={classes.loadingSettingsDetails}> value="enabled"
Loading... color="primary"
</Typography> />
</div> }
label="Enable OTA Updates?"
: otaSettings ? />
<TextValidator
<ValidatorForm onSubmit={onSubmit}> validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']}
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]}
<FormControlLabel className={classes.switchControl} name="port"
control={ label="Port"
<Switch className={classes.textField}
checked={otaSettings.enabled} value={otaSettings.port}
onChange={handleCheckboxChange('enabled')} type="number"
value="enabled" onChange={handleValueChange('port')}
color="primary" margin="normal"
/> />
} <PasswordValidator
label="Enable OTA Updates?" validators={['required', 'matchRegexp:^.{1,64}$']}
/> errorMessages={['OTA Password is required', 'OTA Point Password must be 64 characters or less']}
name="password"
<TextValidator label="Password"
validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']} className={classes.textField}
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]} value={otaSettings.password}
name="port" onChange={handleValueChange('password')}
label="Port" margin="normal"
className={classes.textField} />
value={otaSettings.port} <Button variant="contained" color="primary" className={classes.button} type="submit">
type="number" Save
onChange={handleValueChange('port')} </Button>
margin="normal" <Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
/> Reset
</Button>
<PasswordValidator </ValidatorForm>
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['OTA Password is required', 'OTA Point Password must be 64 characters or less']}
name="password"
label="Password"
className={classes.textField}
value={otaSettings.password}
onChange={handleValueChange('password')}
margin="normal"
/>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
}
</div>
); );
} }
} }
OTASettingsForm.propTypes = { OTASettingsForm.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
otaSettingsFetched: PropTypes.bool.isRequired,
otaSettings: PropTypes.object, otaSettings: PropTypes.object,
errorMessage: PropTypes.string,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired,
handleValueChange: PropTypes.func.isRequired, handleValueChange: PropTypes.func.isRequired,

View File

@ -4,7 +4,6 @@ import { ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Typography from '@material-ui/core/Typography'; import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box'; import Box from '@material-ui/core/Box';
@ -12,13 +11,6 @@ import PasswordValidator from '../components/PasswordValidator';
import { withAuthenticationContext } from '../authentication/Context'; import { withAuthenticationContext } from '../authentication/Context';
const styles = theme => ({ const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: { textField: {
width: "100%" width: "100%"
}, },
@ -36,58 +28,38 @@ class SecuritySettingsForm extends React.Component {
} }
render() { render() {
const { classes, securitySettingsFetched, securitySettings, errorMessage, handleValueChange, onReset } = this.props; const { classes, securitySettings, handleValueChange, onReset } = this.props;
return ( return (
!securitySettingsFetched ? <ValidatorForm onSubmit={this.onSubmit} ref="SecuritySettingsForm">
<div className={classes.loadingSettings}> <PasswordValidator
<LinearProgress className={classes.loadingSettingsDetails} /> validators={['required', 'matchRegexp:^.{1,64}$']}
<Typography variant="h4" className={classes.loadingSettingsDetails}> errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']}
Loading... name="jwt_secret"
</Typography> label="JWT Secret"
</div> className={classes.textField}
: value={securitySettings.jwt_secret}
securitySettings ? onChange={handleValueChange('jwt_secret')}
<ValidatorForm onSubmit={this.onSubmit} ref="SecuritySettingsForm"> margin="normal"
<PasswordValidator />
validators={['required', 'matchRegexp:^.{1,64}$']} <Typography component="div" variant="body1">
errorMessages={['JWT Secret Required', 'JWT Secret must be 64 characters or less']} <Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
name="jwt_secret" If you modify the JWT Secret, all users will be logged out.
label="JWT Secret" </Box>
className={classes.textField} </Typography>
value={securitySettings.jwt_secret} <Button variant="contained" color="primary" className={classes.button} type="submit">
onChange={handleValueChange('jwt_secret')} Save
margin="normal" </Button>
/> <Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
<Typography component="div" variant="body1"> Reset
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}> </Button>
If you modify the JWT Secret, all users will be logged out. </ValidatorForm>
</Box>
</Typography>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
); );
} }
} }
SecuritySettingsForm.propTypes = { SecuritySettingsForm.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
securitySettingsFetched: PropTypes.bool.isRequired,
securitySettings: PropTypes.object, securitySettings: PropTypes.object,
errorMessage: PropTypes.string,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired, onReset: PropTypes.func.isRequired,
handleValueChange: PropTypes.func.isRequired, handleValueChange: PropTypes.func.isRequired,

View File

@ -69,7 +69,7 @@ class WiFiNetworkSelector extends Component {
scanningForNetworks ? scanningForNetworks ?
<div> <div>
<LinearProgress className={classes.scanningProgress}/> <LinearProgress className={classes.scanningProgress}/>
<Typography variant="h4" className={classes.scanningProgress}> <Typography variant="h6" className={classes.scanningProgress}>
Scanning... Scanning...
</Typography> </Typography>
</div> </div>
@ -80,7 +80,7 @@ class WiFiNetworkSelector extends Component {
</List> </List>
: :
<div> <div>
<Typography variant="h4" className={classes.scanningProgress}> <Typography variant="h6" className={classes.scanningProgress}>
{errorMessage} {errorMessage}
</Typography> </Typography>
</div> </div>

View File

@ -3,10 +3,8 @@ import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles'; import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import LinearProgress from '@material-ui/core/LinearProgress';
import Checkbox from '@material-ui/core/Checkbox'; import Checkbox from '@material-ui/core/Checkbox';
import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormControlLabel from '@material-ui/core/FormControlLabel';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List'; import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem'; import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from '@material-ui/core/ListItemText';
@ -28,13 +26,6 @@ import optional from '../validators/optional';
import PasswordValidator from '../components/PasswordValidator'; import PasswordValidator from '../components/PasswordValidator';
const styles = theme => ({ const styles = theme => ({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
textField: { textField: {
width: "100%" width: "100%"
}, },
@ -80,157 +71,124 @@ class WiFiSettingsForm extends React.Component {
} }
render() { render() {
const { classes, wifiSettingsFetched, wifiSettings, errorMessage, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props; const { classes, wifiSettings, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
return ( return (
<div> <ValidatorForm onSubmit={onSubmit} ref="WiFiSettingsForm">
{ {
!wifiSettingsFetched ? selectedNetwork ? this.renderSelectedNetwork() :
<TextValidator
<div className={classes.loadingSettings}> validators={['matchRegexp:^.{0,32}$']}
<LinearProgress className={classes.loadingSettingsDetails} /> errorMessages={['SSID must be 32 characters or less']}
<Typography variant="h4" className={classes.loadingSettingsDetails}> name="ssid"
Loading... label="SSID"
</Typography> className={classes.textField}
</div> value={wifiSettings.ssid}
onChange={handleValueChange('ssid')}
: wifiSettings ? margin="normal"
/>
<ValidatorForm onSubmit={onSubmit} ref="WiFiSettingsForm">
{
selectedNetwork ? this.renderSelectedNetwork() :
<TextValidator
validators={['matchRegexp:^.{0,32}$']}
errorMessages={['SSID must be 32 characters or less']}
name="ssid"
label="SSID"
className={classes.textField}
value={wifiSettings.ssid}
onChange={handleValueChange('ssid')}
margin="normal"
/>
}
{
!isNetworkOpen(selectedNetwork) &&
<PasswordValidator
validators={['matchRegexp:^.{0,64}$']}
errorMessages={['Password must be 64 characters or less']}
name="password"
label="Password"
className={classes.textField}
value={wifiSettings.password}
onChange={handleValueChange('password')}
margin="normal"
/>
}
<TextValidator
validators={['required', 'isHostname']}
errorMessages={['Hostname is required', "Not a valid hostname"]}
name="hostname"
label="Hostname"
className={classes.textField}
value={wifiSettings.hostname}
onChange={handleValueChange('hostname')}
margin="normal"
/>
<FormControlLabel className={classes.checkboxControl}
control={
<Checkbox
value="static_ip_config"
checked={wifiSettings.static_ip_config}
onChange={handleCheckboxChange("static_ip_config")}
/>
}
label="Static IP Config?"
/>
{
wifiSettings.static_ip_config &&
<Fragment>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Local IP is required', 'Must be an IP address']}
name="local_ip"
label="Local IP"
className={classes.textField}
value={wifiSettings.local_ip}
onChange={handleValueChange('local_ip')}
margin="normal"
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Gateway IP is required', 'Must be an IP address']}
name="gateway_ip"
label="Gateway"
className={classes.textField}
value={wifiSettings.gateway_ip}
onChange={handleValueChange('gateway_ip')}
margin="normal"
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Subnet mask is required', 'Must be an IP address']}
name="subnet_mask"
label="Subnet"
className={classes.textField}
value={wifiSettings.subnet_mask}
onChange={handleValueChange('subnet_mask')}
margin="normal"
/>
<TextValidator
validators={['isOptionalIP']}
errorMessages={['Must be an IP address']}
name="dns_ip_1"
label="DNS IP #1"
className={classes.textField}
value={wifiSettings.dns_ip_1}
onChange={handleValueChange('dns_ip_1')}
margin="normal"
/>
<TextValidator
validators={['isOptionalIP']}
errorMessages={['Must be an IP address']}
name="dns_ip_2"
label="DNS IP #2"
className={classes.textField}
value={wifiSettings.dns_ip_2}
onChange={handleValueChange('dns_ip_2')}
margin="normal"
/>
</Fragment>
}
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
:
<div className={classes.loadingSettings}>
<Typography variant="h4" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</div>
} }
</div> {
!isNetworkOpen(selectedNetwork) &&
<PasswordValidator
validators={['matchRegexp:^.{0,64}$']}
errorMessages={['Password must be 64 characters or less']}
name="password"
label="Password"
className={classes.textField}
value={wifiSettings.password}
onChange={handleValueChange('password')}
margin="normal"
/>
}
<TextValidator
validators={['required', 'isHostname']}
errorMessages={['Hostname is required', "Not a valid hostname"]}
name="hostname"
label="Hostname"
className={classes.textField}
value={wifiSettings.hostname}
onChange={handleValueChange('hostname')}
margin="normal"
/>
<FormControlLabel className={classes.checkboxControl}
control={
<Checkbox
value="static_ip_config"
checked={wifiSettings.static_ip_config}
onChange={handleCheckboxChange("static_ip_config")}
/>
}
label="Static IP Config?"
/>
{
wifiSettings.static_ip_config &&
<Fragment>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Local IP is required', 'Must be an IP address']}
name="local_ip"
label="Local IP"
className={classes.textField}
value={wifiSettings.local_ip}
onChange={handleValueChange('local_ip')}
margin="normal"
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Gateway IP is required', 'Must be an IP address']}
name="gateway_ip"
label="Gateway"
className={classes.textField}
value={wifiSettings.gateway_ip}
onChange={handleValueChange('gateway_ip')}
margin="normal"
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Subnet mask is required', 'Must be an IP address']}
name="subnet_mask"
label="Subnet"
className={classes.textField}
value={wifiSettings.subnet_mask}
onChange={handleValueChange('subnet_mask')}
margin="normal"
/>
<TextValidator
validators={['isOptionalIP']}
errorMessages={['Must be an IP address']}
name="dns_ip_1"
label="DNS IP #1"
className={classes.textField}
value={wifiSettings.dns_ip_1}
onChange={handleValueChange('dns_ip_1')}
margin="normal"
/>
<TextValidator
validators={['isOptionalIP']}
errorMessages={['Must be an IP address']}
name="dns_ip_2"
label="DNS IP #2"
className={classes.textField}
value={wifiSettings.dns_ip_2}
onChange={handleValueChange('dns_ip_2')}
margin="normal"
/>
</Fragment>
}
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
); );
} }
} }
WiFiSettingsForm.propTypes = { WiFiSettingsForm.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
wifiSettingsFetched: PropTypes.bool.isRequired,
wifiSettings: PropTypes.object, wifiSettings: PropTypes.object,
errorMessage: PropTypes.string,
deselectNetwork: PropTypes.func, deselectNetwork: PropTypes.func,
selectedNetwork: PropTypes.object, selectedNetwork: PropTypes.object,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,

View File

@ -0,0 +1,82 @@
import React, { Component } from 'react';
import { ValidatorForm } from 'react-material-ui-form-validator';
import { ENDPOINT_ROOT } from '../constants/Env';
import SectionContent from '../components/SectionContent';
import { restComponent } from '../components/RestComponent';
import LoadingNotification from '../components/LoadingNotification';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import Slider from '@material-ui/core/Slider';
import { makeStyles } from '@material-ui/core/styles';
export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings";
const valueToPercentage = (value) => `${Math.round(value / 255 * 100)}%`;
class DemoController extends Component {
componentDidMount() {
this.props.loadData();
}
render() {
const { data, fetched, errorMessage, saveData, loadData, handleSliderChange } = this.props;
return (
<SectionContent title="Controller" titleGutter>
<LoadingNotification
onReset={loadData}
fetched={fetched}
errorMessage={errorMessage}
render={() =>
<DemoControllerForm
demoSettings={data}
onReset={loadData}
onSubmit={saveData}
handleSliderChange={handleSliderChange}
/>
}
/>
</SectionContent>
)
}
}
const useStyles = makeStyles(theme => ({
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
},
blinkSpeedLabel: {
marginBottom: theme.spacing(5),
}
}));
function DemoControllerForm(props) {
const { demoSettings, onSubmit, onReset, handleSliderChange } = props;
const classes = useStyles();
return (
<ValidatorForm onSubmit={onSubmit}>
<Typography id="blink-speed-slider" className={classes.blinkSpeedLabel}>
Blink Speed
</Typography>
<Slider
value={demoSettings.blink_speed}
valueLabelFormat={valueToPercentage}
aria-labelledby="blink-speed-slider"
valueLabelDisplay="on"
min={0}
max={255}
onChange={handleSliderChange('blink_speed')}
/>
<Button variant="contained" color="primary" className={classes.button} type="submit">
Save
</Button>
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
Reset
</Button>
</ValidatorForm>
);
}
export default restComponent(DEMO_SETTINGS_ENDPOINT, DemoController);

View File

@ -0,0 +1,100 @@
import React, { Component } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import TableCell from '@material-ui/core/TableCell';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import Typography from '@material-ui/core/Typography';
import SectionContent from '../components/SectionContent';
const styles = theme => ({
fileTable: {
marginBottom: theme.spacing(2)
}
});
class DemoInformation extends Component {
render() {
const { classes } = this.props;
return (
<SectionContent title="Demo Project - Blink Speed Controller" titleGutter>
<Typography variant="body1" paragraph>
This simple demo project allows you to control the blink speed of the built-in LED.
It demonstrates how the esp8266-react framework may be extended for your own IoT project.
</Typography>
<Typography variant="body1" paragraph>
It is recommended that you keep your project interface code under the 'project' directory.
This serves to isolate your project code from the from the rest of the user interface which should
simplify merges should you wish to update your project with future framework changes.
</Typography>
<Typography variant="body1" paragraph>
The demo project interface code stored in the interface/project directory:
</Typography>
<Table className={classes.fileTable}>
<TableHead>
<TableRow>
<TableCell>
File
</TableCell>
<TableCell>
Description
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>
ProjectMenu.js
</TableCell>
<TableCell>
You can add your project's screens to the side bar here.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
ProjectRouting.js
</TableCell>
<TableCell>
The routing which controls the screens of your project.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoProject.js
</TableCell>
<TableCell>
This screen, with tabs and tab routing.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoInformation.js
</TableCell>
<TableCell>
The demo information tab.
</TableCell>
</TableRow>
<TableRow>
<TableCell>
DemoController.js
</TableCell>
<TableCell>
The demo controller tab, to control the built-in LED.
</TableCell>
</TableRow>
</TableBody>
</Table>
<Typography variant="body1" paragraph>
See the project <a href="https://github.com/rjwats/esp8266-react/">README</a> for a full description of the demo project.
</Typography>
</SectionContent>
)
}
}
export default withStyles(styles)(DemoInformation);

View File

@ -0,0 +1,37 @@
import React, { Component } from 'react';
import { Redirect, Switch } from 'react-router-dom'
import { PROJECT_PATH } from '../constants/Env';
import MenuAppBar from '../components/MenuAppBar';
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
import DemoInformation from './DemoInformation';
import DemoController from './DemoController';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
class DemoProject extends Component {
handleTabChange = (event, path) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Demo Project">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} indicatorColor="primary" textColor="primary" variant="fullWidth">
<Tab value={`/${PROJECT_PATH}/demo/information`} label="Information" />
<Tab value={`/${PROJECT_PATH}/demo/controller`} label="Controller" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/information`} component={DemoInformation} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/controller`} component={DemoController} />
<Redirect to={`/${PROJECT_PATH}/demo/information`} />
</Switch>
</MenuAppBar>
)
}
}
export default DemoProject;

View File

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { PROJECT_PATH } from '../constants/Env';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
class ProjectMenu extends Component {
render() {
const path = this.props.match.url;
return (
<List>
<ListItem to={`/${PROJECT_PATH}/demo/`} selected={path.startsWith(`/${PROJECT_PATH}/demo/`)} button component={Link}>
<ListItemIcon>
<SettingsRemoteIcon />
</ListItemIcon>
<ListItemText primary="Demo Project" />
</ListItem>
</List>
)
}
}
export default withRouter(ProjectMenu);

View File

@ -0,0 +1,32 @@
import React, { Component } from 'react';
import { Redirect, Switch } from 'react-router';
import { PROJECT_PATH } from '../constants/Env';
import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
import DemoProject from './DemoProject';
class ProjectRouting extends Component {
render() {
return (
<Switch>
{
/*
* Add your project page routing below.
*/
}
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/demo/*`} component={DemoProject} />
{
/*
* The redirect below caters for the default project route and redirecting invalid paths.
* The "to" property must match one of the routes above for this to work correctly.
*/
}
<Redirect to={`/${PROJECT_PATH}/demo/`} />
</Switch>
)
}
}
export default ProjectRouting;

View File

@ -32,6 +32,7 @@ class NetworkTime extends Component {
</MenuAppBar> </MenuAppBar>
) )
} }
} }
export default withAuthenticationContext(NetworkTime) export default withAuthenticationContext(NetworkTime)

View File

@ -1,11 +1,14 @@
#include <APSettingsService.h> #include <APSettingsService.h>
APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) { APSettingsService::APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, AP_SETTINGS_SERVICE_PATH, AP_SETTINGS_FILE) {}
onConfigUpdated();
}
APSettingsService::~APSettingsService() {} APSettingsService::~APSettingsService() {}
void APSettingsService::begin() {
SettingsService::begin();
onConfigUpdated();
}
void APSettingsService::loop() { void APSettingsService::loop() {
unsigned long currentMillis = millis(); unsigned long currentMillis = millis();
unsigned long manageElapsed = (unsigned long)(currentMillis - _lastManaged); unsigned long manageElapsed = (unsigned long)(currentMillis - _lastManaged);
@ -80,7 +83,4 @@ void APSettingsService::writeToJsonObject(JsonObject& root) {
void APSettingsService::onConfigUpdated() { void APSettingsService::onConfigUpdated() {
_lastManaged = millis() - MANAGE_NETWORK_DELAY; _lastManaged = millis() - MANAGE_NETWORK_DELAY;
// stop softAP - forces reconfiguration in loop()
stopAP();
} }

View File

@ -1,7 +1,7 @@
#ifndef APSettingsConfig_h #ifndef APSettingsConfig_h
#define APSettingsConfig_h #define APSettingsConfig_h
#include <SettingsService.h> #include <AdminSettingsService.h>
#include <DNSServer.h> #include <DNSServer.h>
#include <IPAddress.h> #include <IPAddress.h>
@ -26,6 +26,7 @@ class APSettingsService : public AdminSettingsService {
APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager); APSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~APSettingsService(); ~APSettingsService();
void begin();
void loop(); void loop();
protected: protected:
@ -49,7 +50,7 @@ class APSettingsService : public AdminSettingsService {
void manageAP(); void manageAP();
void startAP(); void startAP();
void stopAP(); void stopAP() ;
void handleDNS(); void handleDNS();
}; };

View File

@ -1,8 +1,8 @@
#include <APStatus.h> #include <APStatus.h>
APStatus::APStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) { APStatus::APStatus(AsyncWebServer* server, SecurityManager* securityManager) {
_server->on(AP_STATUS_SERVICE_PATH, HTTP_GET, server->on(AP_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED) securityManager->wrapRequest(std::bind(&APStatus::apStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
); );
} }

View File

@ -22,13 +22,10 @@ class APStatus {
public: public:
APStatus(AsyncWebServer *server, SecurityManager* securityManager); APStatus(AsyncWebServer* server, SecurityManager* securityManager);
private: private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
void apStatus(AsyncWebServerRequest *request); void apStatus(AsyncWebServerRequest *request);
}; };

View File

@ -0,0 +1,45 @@
#ifndef AdminSettingsService_h
#define AdminSettingsService_h
#include <SettingsService.h>
class AdminSettingsService : public SettingsService {
public:
AdminSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager, char const* servicePath, char const* filePath):
SettingsService(server, fs, servicePath, filePath), _securityManager(securityManager) {}
protected:
// will validate the requests with the security manager
SecurityManager* _securityManager;
void fetchConfig(AsyncWebServerRequest *request) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::fetchConfig(request);
}
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::updateConfig(request, jsonDocument);
}
// override this to replace the default authentication predicate, IS_ADMIN
AuthenticationPredicate getAuthenticationPredicate() {
return AuthenticationPredicates::IS_ADMIN;
}
};
#endif // end AdminSettingsService

View File

@ -1,7 +1,6 @@
#include <AuthenticationService.h> #include <AuthenticationService.h>
AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager): AuthenticationService::AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) : _securityManager(securityManager) {
_server(server), _securityManager(securityManager) {
server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1)); server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, std::bind(&AuthenticationService::verifyAuthorization, this, std::placeholders::_1));
_signInHandler.setUri(SIGN_IN_PATH); _signInHandler.setUri(SIGN_IN_PATH);

View File

@ -15,12 +15,11 @@ class AuthenticationService {
public: public:
AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager) ; AuthenticationService(AsyncWebServer* server, SecurityManager* securityManager);
~AuthenticationService(); ~AuthenticationService();
private: private:
// server instance
AsyncWebServer* _server;
SecurityManager* _securityManager; SecurityManager* _securityManager;
AsyncJsonWebHandler _signInHandler; AsyncJsonWebHandler _signInHandler;

View File

@ -0,0 +1,55 @@
#include <ESP8266React.h>
ESP8266React::ESP8266React(AsyncWebServer* server, FS* fs):
_securitySettingsService(server, fs),
_wifiSettingsService(server, fs, &_securitySettingsService),
_apSettingsService(server, fs, &_securitySettingsService),
_ntpSettingsService(server, fs, &_securitySettingsService),
_otaSettingsService(server, fs, &_securitySettingsService),
_authenticationService(server, &_securitySettingsService),
_wifiScanner(server, &_securitySettingsService),
_wifiStatus(server, &_securitySettingsService),
_ntpStatus(server, &_securitySettingsService),
_apStatus(server, &_securitySettingsService),
_systemStatus(server, &_securitySettingsService) {
// Serve static resources from /www/
server->serveStatic("/js/", SPIFFS, "/www/js/");
server->serveStatic("/css/", SPIFFS, "/www/css/");
server->serveStatic("/fonts/", SPIFFS, "/www/fonts/");
server->serveStatic("/app/", SPIFFS, "/www/app/");
server->serveStatic("/favicon.ico", SPIFFS, "/www/favicon.ico");
// Serving all other get requests with "/www/index.htm"
// OPTIONS get a straight up 200 response
server->onNotFound([](AsyncWebServerRequest *request) {
if (request->method() == HTTP_GET) {
request->send(SPIFFS, "/www/index.html");
} else if (request->method() == HTTP_OPTIONS) {
request->send(200);
} else {
request->send(404);
}
});
// Disable CORS if required
#if defined(ENABLE_CORS)
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
}
void ESP8266React::begin() {
_securitySettingsService.begin();
_wifiSettingsService.begin();
_apSettingsService.begin();
_ntpSettingsService.begin();
_otaSettingsService.begin();
}
void ESP8266React::loop() {
_wifiSettingsService.loop();
_apSettingsService.loop();
_ntpSettingsService.loop();
_otaSettingsService.loop();
}

View File

@ -0,0 +1,59 @@
#ifndef ESP8266React_h
#define ESP8266React_h
#include <Arduino.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(ESP_PLATFORM)
#include <WiFi.h>
#include <AsyncTCP.h>
#include <SPIFFS.h>
#endif
#include <FS.h>
#include <SecuritySettingsService.h>
#include <WiFiSettingsService.h>
#include <APSettingsService.h>
#include <NTPSettingsService.h>
#include <OTASettingsService.h>
#include <AuthenticationService.h>
#include <WiFiScanner.h>
#include <WiFiStatus.h>
#include <NTPStatus.h>
#include <APStatus.h>
#include <SystemStatus.h>
class ESP8266React {
public:
ESP8266React(AsyncWebServer* server, FS* fs);
void begin();
void loop();
SecurityManager* getSecurityManager(){
return &_securitySettingsService;
}
private:
SecuritySettingsService _securitySettingsService;
WiFiSettingsService _wifiSettingsService;
APSettingsService _apSettingsService;
NTPSettingsService _ntpSettingsService;
OTASettingsService _otaSettingsService;
AuthenticationService _authenticationService;
WiFiScanner _wifiScanner;
WiFiStatus _wifiStatus;
NTPStatus _ntpStatus;
APStatus _apStatus;
SystemStatus _systemStatus;
};
#endif

View File

@ -1,7 +1,7 @@
#ifndef NTPSettingsService_h #ifndef NTPSettingsService_h
#define NTPSettingsService_h #define NTPSettingsService_h
#include <SettingsService.h> #include <AdminSettingsService.h>
#include <TimeLib.h> #include <TimeLib.h>
#include <NtpClientLib.h> #include <NtpClientLib.h>

View File

@ -1,8 +1,8 @@
#include <NTPStatus.h> #include <NTPStatus.h>
NTPStatus::NTPStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) { NTPStatus::NTPStatus(AsyncWebServer* server, SecurityManager* securityManager) {
_server->on(NTP_STATUS_SERVICE_PATH, HTTP_GET, server->on(NTP_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED) securityManager->wrapRequest(std::bind(&NTPStatus::ntpStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
); );
} }

View File

@ -23,13 +23,10 @@ class NTPStatus {
public: public:
NTPStatus(AsyncWebServer *server, SecurityManager* securityManager); NTPStatus(AsyncWebServer* server, SecurityManager* securityManager);
private: private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
void ntpStatus(AsyncWebServerRequest *request); void ntpStatus(AsyncWebServerRequest *request);
}; };

View File

@ -1,7 +1,7 @@
#ifndef OTASettingsService_h #ifndef OTASettingsService_h
#define OTASettingsService_h #define OTASettingsService_h
#include <SettingsService.h> #include <AdminSettingsService.h>
#if defined(ESP8266) #if defined(ESP8266)
#include <ESP8266mDNS.h> #include <ESP8266mDNS.h>
@ -52,4 +52,4 @@ class OTASettingsService : public AdminSettingsService {
}; };
#endif // end NTPSettingsService_h #endif // end OTASettingsService_h

View File

@ -29,7 +29,3 @@ void SecuritySettingsService::writeToJsonObject(JsonObject& root) {
user["admin"] = _user.isAdmin(); user["admin"] = _user.isAdmin();
} }
} }
void SecuritySettingsService::begin() {
readFromFS();
}

View File

@ -1,7 +1,7 @@
#ifndef SecuritySettingsService_h #ifndef SecuritySettingsService_h
#define SecuritySettingsService_h #define SecuritySettingsService_h
#include <SettingsService.h> #include <AdminSettingsService.h>
#include <SecurityManager.h> #include <SecurityManager.h>
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json" #define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
@ -14,8 +14,6 @@ class SecuritySettingsService : public AdminSettingsService, public SecurityMana
SecuritySettingsService(AsyncWebServer* server, FS* fs); SecuritySettingsService(AsyncWebServer* server, FS* fs);
~SecuritySettingsService(); ~SecuritySettingsService();
void begin();
protected: protected:
void readFromJsonObject(JsonObject& root); void readFromJsonObject(JsonObject& root);

View File

@ -47,7 +47,7 @@ protected:
return true; return true;
} }
void readFromFS(){ void readFromFS() {
File configFile = _fs->open(_filePath, "r"); File configFile = _fs->open(_filePath, "r");
// use defaults if no config found // use defaults if no config found

View File

@ -16,7 +16,6 @@
#include <AsyncJsonWebHandler.h> #include <AsyncJsonWebHandler.h>
#include <AsyncArduinoJson6.h> #include <AsyncArduinoJson6.h>
/* /*
* Abstraction of a service which stores it's settings as JSON in a file system. * Abstraction of a service which stores it's settings as JSON in a file system.
*/ */
@ -24,30 +23,25 @@ class SettingsService : public SettingsPersistence {
public: public:
SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath): SettingsService(AsyncWebServer* server, FS* fs, char const* servicePath, char const* filePath): SettingsPersistence(fs, filePath), _servicePath(servicePath) {
SettingsPersistence(fs, filePath), _server(server) { server->on(_servicePath, HTTP_GET, std::bind(&SettingsService::fetchConfig, this, std::placeholders::_1));
// configure fetch config handler
_server->on(servicePath, HTTP_GET, std::bind(&SettingsService::fetchConfig, this, std::placeholders::_1));
// configure update settings handler
_updateHandler.setUri(servicePath); _updateHandler.setUri(servicePath);
_updateHandler.setMethod(HTTP_POST); _updateHandler.setMethod(HTTP_POST);
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); _updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE);
_updateHandler.onRequest(std::bind(&SettingsService::updateConfig, this, std::placeholders::_1, std::placeholders::_2)); _updateHandler.onRequest(std::bind(&SettingsService::updateConfig, this, std::placeholders::_1, std::placeholders::_2));
_server->addHandler(&_updateHandler); server->addHandler(&_updateHandler);
} }
virtual ~SettingsService() {} virtual ~SettingsService() {}
virtual void begin() { void begin() {
// read the initial data from the file system
readFromFS(); readFromFS();
} }
protected: protected:
// will serve setting endpoints from here char const* _servicePath;
AsyncWebServer* _server;
AsyncJsonWebHandler _updateHandler; AsyncJsonWebHandler _updateHandler;
virtual void fetchConfig(AsyncWebServerRequest *request) { virtual void fetchConfig(AsyncWebServerRequest *request) {
@ -82,43 +76,4 @@ protected:
}; };
class AdminSettingsService : public SettingsService {
public:
AdminSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager, char const* servicePath, char const* filePath):
SettingsService(server, fs, servicePath, filePath), _securityManager(securityManager) {
}
protected:
// will validate the requests with the security manager
SecurityManager* _securityManager;
void fetchConfig(AsyncWebServerRequest *request) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::fetchConfig(request);
}
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
// verify the request against the predicate
Authentication authentication = _securityManager->authenticateRequest(request);
if (!getAuthenticationPredicate()(authentication)) {
request->send(401);
return;
}
// delegate to underlying implemetation
SettingsService::updateConfig(request, jsonDocument);
}
// override to override the default authentication predicate, IS_ADMIN
AuthenticationPredicate getAuthenticationPredicate() {
return AuthenticationPredicates::IS_ADMIN;
}
};
#endif // end SettingsService #endif // end SettingsService

View File

@ -33,7 +33,7 @@ private:
AsyncJsonWebHandler _updateHandler; AsyncJsonWebHandler _updateHandler;
void fetchConfig(AsyncWebServerRequest *request){ void fetchConfig(AsyncWebServerRequest *request) {
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE); AsyncJsonResponse * response = new AsyncJsonResponse(MAX_SETTINGS_SIZE);
JsonObject jsonObject = response->getRoot(); JsonObject jsonObject = response->getRoot();
writeToJsonObject(jsonObject); writeToJsonObject(jsonObject);
@ -41,8 +41,8 @@ private:
request->send(response); request->send(response);
} }
void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument){ void updateConfig(AsyncWebServerRequest *request, JsonDocument &jsonDocument) {
if (jsonDocument.is<JsonObject>()){ if (jsonDocument.is<JsonObject>()) {
JsonObject newConfig = jsonDocument.as<JsonObject>(); JsonObject newConfig = jsonDocument.as<JsonObject>();
readFromJsonObject(newConfig); readFromJsonObject(newConfig);
@ -59,32 +59,25 @@ private:
protected: protected:
// will serve setting endpoints from here
AsyncWebServer* _server;
// reads the local config from the // reads the local config from the
virtual void readFromJsonObject(JsonObject& root){} virtual void readFromJsonObject(JsonObject& root) {}
virtual void writeToJsonObject(JsonObject& root){} virtual void writeToJsonObject(JsonObject& root) {}
// implement to perform action when config has been updated // implement to perform action when config has been updated
virtual void onConfigUpdated(){} virtual void onConfigUpdated() {}
public: public:
SimpleService(AsyncWebServer* server, char const* servicePath): SimpleService(AsyncWebServer* server, char const* servicePath) {
_server(server) { server->on(servicePath, HTTP_GET, std::bind(&SimpleService::fetchConfig, this, std::placeholders::_1));
// configure fetch config handler
_server->on(servicePath, HTTP_GET, std::bind(&SimpleService::fetchConfig, this, std::placeholders::_1));
// configure update settings handler
_updateHandler.setUri(servicePath); _updateHandler.setUri(servicePath);
_updateHandler.setMethod(HTTP_POST); _updateHandler.setMethod(HTTP_POST);
_updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE); _updateHandler.setMaxContentLength(MAX_SETTINGS_SIZE);
_updateHandler.onRequest(std::bind(&SimpleService::updateConfig, this, std::placeholders::_1, std::placeholders::_2)); _updateHandler.onRequest(std::bind(&SimpleService::updateConfig, this, std::placeholders::_1, std::placeholders::_2));
_server->addHandler(&_updateHandler); server->addHandler(&_updateHandler);
} }
virtual ~SimpleService() {} virtual ~SimpleService() {}
}; };

View File

@ -1,12 +1,12 @@
#include <SystemStatus.h> #include <SystemStatus.h>
SystemStatus::SystemStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) { SystemStatus::SystemStatus(AsyncWebServer* server, SecurityManager* securityManager) {
_server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET, server->on(SYSTEM_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED) securityManager->wrapRequest(std::bind(&SystemStatus::systemStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
); );
} }
void SystemStatus::systemStatus(AsyncWebServerRequest *request) { void SystemStatus::systemStatus(AsyncWebServerRequest *request) {
AsyncJsonResponse * response = new AsyncJsonResponse(MAX_ESP_STATUS_SIZE); AsyncJsonResponse * response = new AsyncJsonResponse(MAX_ESP_STATUS_SIZE);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
#if defined(ESP8266) #if defined(ESP8266)

View File

@ -20,14 +20,11 @@
class SystemStatus { class SystemStatus {
public: public:
SystemStatus(AsyncWebServer *server, SecurityManager* securityManager); SystemStatus(AsyncWebServer* server, SecurityManager* securityManager);
private: private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
void systemStatus(AsyncWebServerRequest *request); void systemStatus(AsyncWebServerRequest *request);
}; };

View File

@ -1,13 +1,13 @@
#include <WiFiScanner.h> #include <WiFiScanner.h>
WiFiScanner::WiFiScanner(AsyncWebServer *server, SecurityManager* securityManager) : _server(server) { WiFiScanner::WiFiScanner(AsyncWebServer *server, SecurityManager* securityManager) {
_server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET, server->on(SCAN_NETWORKS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN) securityManager->wrapRequest(std::bind(&WiFiScanner::scanNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN)
); );
_server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET, server->on(LIST_NETWORKS_SERVICE_PATH, HTTP_GET,
securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN) securityManager->wrapRequest(std::bind(&WiFiScanner::listNetworks, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN)
); );
} };
void WiFiScanner::scanNetworks(AsyncWebServerRequest *request) { void WiFiScanner::scanNetworks(AsyncWebServerRequest *request) {
if (WiFi.scanComplete() != -1){ if (WiFi.scanComplete() != -1){

View File

@ -28,8 +28,6 @@ class WiFiScanner {
private: private:
AsyncWebServer* _server;
void scanNetworks(AsyncWebServerRequest *request); void scanNetworks(AsyncWebServerRequest *request);
void listNetworks(AsyncWebServerRequest *request); void listNetworks(AsyncWebServerRequest *request);

View File

@ -1,9 +1,16 @@
#include <WiFiSettingsService.h> #include <WiFiSettingsService.h>
WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) { WiFiSettingsService::WiFiSettingsService(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, WIFI_SETTINGS_SERVICE_PATH, WIFI_SETTINGS_FILE) {
// Disable WiFi config persistance and auto reconnect
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
#if defined(ESP8266) #if defined(ESP8266)
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1)); _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1));
#elif defined(ESP_PLATFORM) #elif defined(ESP_PLATFORM)
// Init the wifi driver on ESP32
WiFi.mode(WIFI_MODE_MAX);
WiFi.mode(WIFI_MODE_NULL);
WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED); WiFi.onEvent(std::bind(&WiFiSettingsService::onStationModeDisconnected, this, std::placeholders::_1, std::placeholders::_2), WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED);
#endif #endif
} }

View File

@ -1,7 +1,7 @@
#ifndef WiFiSettingsService_h #ifndef WiFiSettingsService_h
#define WiFiSettingsService_h #define WiFiSettingsService_h
#include <SettingsService.h> #include <AdminSettingsService.h>
#include <IPAddress.h> #include <IPAddress.h>
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json" #define WIFI_SETTINGS_FILE "/config/wifiSettings.json"

View File

@ -1,9 +1,9 @@
#include <WiFiStatus.h> #include <WiFiStatus.h>
WiFiStatus::WiFiStatus(AsyncWebServer *server, SecurityManager* securityManager) : _server(server), _securityManager(securityManager) { WiFiStatus::WiFiStatus(AsyncWebServer* server, SecurityManager* securityManager) {
_server->on(WIFI_STATUS_SERVICE_PATH, HTTP_GET, server->on(WIFI_STATUS_SERVICE_PATH, HTTP_GET,
_securityManager->wrapRequest(std::bind(&WiFiStatus::wifiStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED) securityManager->wrapRequest(std::bind(&WiFiStatus::wifiStatus, this, std::placeholders::_1), AuthenticationPredicates::IS_AUTHENTICATED)
); );
#if defined(ESP8266) #if defined(ESP8266)
_onStationModeConnectedHandler = WiFi.onStationModeConnected(onStationModeConnected); _onStationModeConnectedHandler = WiFi.onStationModeConnected(onStationModeConnected);
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(onStationModeDisconnected); _onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(onStationModeDisconnected);

View File

@ -22,13 +22,10 @@ class WiFiStatus {
public: public:
WiFiStatus(AsyncWebServer *server, SecurityManager* securityManager); WiFiStatus(AsyncWebServer* server, SecurityManager* securityManager);
private: private:
AsyncWebServer* _server;
SecurityManager* _securityManager;
#if defined(ESP8266) #if defined(ESP8266)
// handler refrences for logging important WiFi events over serial // handler refrences for logging important WiFi events over serial
WiFiEventHandler _onStationModeConnectedHandler; WiFiEventHandler _onStationModeConnectedHandler;

26
src/DemoProject.cpp Normal file
View File

@ -0,0 +1,26 @@
#include <DemoProject.h>
DemoProject::DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager) : AdminSettingsService(server, fs, securityManager, DEMO_SETTINGS_PATH, DEMO_SETTINGS_FILE) {
pinMode(BLINK_LED, OUTPUT);
}
DemoProject::~DemoProject() {}
void DemoProject::loop() {
unsigned delay = MAX_DELAY / 255 * (255 - _blinkSpeed);
unsigned long currentMillis = millis();
if (!_lastBlink || (unsigned long)(currentMillis - _lastBlink) >= delay) {
_lastBlink = currentMillis;
digitalWrite(BLINK_LED, !digitalRead(BLINK_LED));
}
}
void DemoProject::readFromJsonObject(JsonObject& root) {
_blinkSpeed = root["blink_speed"] | DEFAULT_BLINK_SPEED;
}
void DemoProject::writeToJsonObject(JsonObject& root) {
// connection settings
root["blink_speed"] = _blinkSpeed;
}

34
src/DemoProject.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef DemoProject_h
#define DemoProject_h
#include <AdminSettingsService.h>
#define BLINK_LED 2
#define MAX_DELAY 1000
#define DEFAULT_BLINK_SPEED 100
#define DEMO_SETTINGS_FILE "/config/demoSettings.json"
#define DEMO_SETTINGS_PATH "/rest/demoSettings"
class DemoProject : public AdminSettingsService {
public:
DemoProject(AsyncWebServer* server, FS* fs, SecurityManager* securityManager);
~DemoProject();
void loop();
private:
unsigned long _lastBlink = 0;
uint8_t _blinkSpeed = 255;
protected:
void readFromJsonObject(JsonObject& root);
void writeToJsonObject(JsonObject& root);
};
#endif

View File

@ -1,99 +1,34 @@
#include <Arduino.h> #include <ESP8266React.h>
#include <DemoProject.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(ESP_PLATFORM)
#include <WiFi.h>
#include <AsyncTCP.h>
#include <SPIFFS.h>
#endif
#include <FS.h> #include <FS.h>
#include <SecuritySettingsService.h>
#include <WiFiSettingsService.h>
#include <APSettingsService.h>
#include <NTPSettingsService.h>
#include <OTASettingsService.h>
#include <AuthenticationService.h>
#include <WiFiScanner.h>
#include <WiFiStatus.h>
#include <NTPStatus.h>
#include <APStatus.h>
#include <SystemStatus.h>
#define SERIAL_BAUD_RATE 115200 #define SERIAL_BAUD_RATE 115200
AsyncWebServer server(80); AsyncWebServer server(80);
ESP8266React esp8266React(&server, &SPIFFS);
SecuritySettingsService securitySettingsService = SecuritySettingsService(&server, &SPIFFS); DemoProject demoProject = DemoProject(&server, &SPIFFS, esp8266React.getSecurityManager());
WiFiSettingsService wifiSettingsService = WiFiSettingsService(&server, &SPIFFS, &securitySettingsService);
APSettingsService apSettingsService = APSettingsService(&server, &SPIFFS, &securitySettingsService);
NTPSettingsService ntpSettingsService = NTPSettingsService(&server, &SPIFFS, &securitySettingsService);
OTASettingsService otaSettingsService = OTASettingsService(&server, &SPIFFS, &securitySettingsService);
AuthenticationService authenticationService = AuthenticationService(&server, &securitySettingsService);
WiFiScanner wifiScanner = WiFiScanner(&server, &securitySettingsService);
WiFiStatus wifiStatus = WiFiStatus(&server, &securitySettingsService);
NTPStatus ntpStatus = NTPStatus(&server, &securitySettingsService);
APStatus apStatus = APStatus(&server, &securitySettingsService);
SystemStatus systemStatus = SystemStatus(&server, &securitySettingsService);;
void setup() { void setup() {
// Disable wifi config persistance and auto reconnect // start serial and filesystem
WiFi.persistent(false);
WiFi.setAutoReconnect(false);
#if defined(ESP_PLATFORM)
// Init the wifi driver on ESP32
WiFi.mode(WIFI_MODE_MAX);
WiFi.mode(WIFI_MODE_NULL);
#endif
Serial.begin(SERIAL_BAUD_RATE); Serial.begin(SERIAL_BAUD_RATE);
// start the file system (must be done before starting the framework)
SPIFFS.begin(); SPIFFS.begin();
// Start security settings service first
securitySettingsService.begin();
// Start services // start the framework and demo project
ntpSettingsService.begin(); esp8266React.begin();
otaSettingsService.begin();
apSettingsService.begin();
wifiSettingsService.begin();
// Serving static resources from /www/ // start the demo project
server.serveStatic("/js/", SPIFFS, "/www/js/"); demoProject.begin();
server.serveStatic("/css/", SPIFFS, "/www/css/");
server.serveStatic("/fonts/", SPIFFS, "/www/fonts/");
server.serveStatic("/app/", SPIFFS, "/www/app/");
server.serveStatic("/favicon.ico", SPIFFS, "/www/favicon.ico");
// Serving all other get requests with "/www/index.htm"
// OPTIONS get a straight up 200 response
server.onNotFound([](AsyncWebServerRequest *request) {
if (request->method() == HTTP_GET) {
request->send(SPIFFS, "/www/index.html");
} else if (request->method() == HTTP_OPTIONS) {
request->send(200);
} else {
request->send(404);
}
});
// Disable CORS if required
#if defined(ENABLE_CORS)
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
#endif
// start the server
server.begin(); server.begin();
} }
void loop() { void loop() {
wifiSettingsService.loop(); // run the framework's loop function
apSettingsService.loop(); esp8266React.loop();
ntpSettingsService.loop();
otaSettingsService.loop(); // run the demo project's loop function
demoProject.loop();
} }