>(endpointUrl: string, RestController: React.ComponentType>) {
+ return withSnackbar(
+ class extends React.Component> & WithSnackbarProps, RestControllerState> {
+
+ state: RestControllerState = {
+ data: undefined,
+ loading: false,
+ errorMessage: undefined
+ };
+
+ setData = (data: D) => {
+ this.setState({
+ data,
+ loading: false,
+ errorMessage: undefined
+ });
+ }
+
+ loadData = () => {
+ this.setState({
+ data: undefined,
+ loading: true,
+ errorMessage: undefined
+ });
+ redirectingAuthorizedFetch(endpointUrl).then(response => {
+ if (response.status === 200) {
+ return response.json();
+ }
+ throw Error("Invalid status code: " + response.status);
+ }).then(json => {
+ this.setState({ data: json, loading: false })
+ }).catch(error => {
+ const errorMessage = error.message || "Unknown error";
+ this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
+ this.setState({ data: undefined, loading: false, errorMessage });
+ });
+ }
+
+ saveData = () => {
+ this.setState({ loading: true });
+ redirectingAuthorizedFetch(endpointUrl, {
+ method: 'POST',
+ body: JSON.stringify(this.state.data),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ }).then(response => {
+ if (response.status === 200) {
+ return response.json();
+ }
+ throw Error("Invalid status code: " + response.status);
+ }).then(json => {
+ this.props.enqueueSnackbar("Changes successfully applied.", { variant: 'success' });
+ this.setState({ data: json, loading: false });
+ }).catch(error => {
+ const errorMessage = error.message || "Unknown error";
+ this.props.enqueueSnackbar("Problem saving: " + errorMessage, { variant: 'error' });
+ this.setState({ data: undefined, loading: false, errorMessage });
+ });
+ }
+
+ handleValueChange = (name: keyof D) => (event: React.ChangeEvent) => {
+ const data = { ...this.state.data!, [name]: event.target.value };
+ this.setState({ data });
+ }
+
+ handleCheckboxChange = (name: keyof D) => (event: React.ChangeEvent) => {
+ const data = { ...this.state.data!, [name]: event.target.checked };
+ this.setState({ data });
+ }
+
+ handleSliderChange = (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => {
+ const data = { ...this.state.data!, [name]: value };
+ this.setState({ data });
+ };
+
+ render() {
+ return ;
+ }
+
+ });
+}
diff --git a/interface/src/components/RestFormLoader.tsx b/interface/src/components/RestFormLoader.tsx
new file mode 100644
index 0000000..f4a8501
--- /dev/null
+++ b/interface/src/components/RestFormLoader.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+
+import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
+import { Button, LinearProgress, Typography } from '@material-ui/core';
+import { RestControllerProps } from './RestController';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ loadingSettings: {
+ margin: theme.spacing(0.5),
+ },
+ loadingSettingsDetails: {
+ margin: theme.spacing(4),
+ textAlign: "center"
+ },
+ button: {
+ marginRight: theme.spacing(2),
+ marginTop: theme.spacing(2),
+ }
+ })
+);
+
+export type RestFormProps = Omit, "loading" | "errorMessage"> & { data: D };
+
+interface RestFormLoaderProps extends RestControllerProps {
+ render: (props: RestFormProps) => JSX.Element;
+}
+
+export default function RestFormLoader(props: RestFormLoaderProps) {
+ const { loading, errorMessage, loadData, render, data, ...rest } = props;
+ const classes = useStyles();
+ if (loading || !data) {
+ return (
+
+
+
+ Loading...
+
+
+ );
+ }
+ if (errorMessage) {
+ return (
+
+
+ {errorMessage}
+
+
+
+ );
+ }
+ return render({ ...rest, loadData, data });
+}
diff --git a/interface/src/components/SectionContent.js b/interface/src/components/SectionContent.js
deleted file mode 100644
index f5a48ee..0000000
--- a/interface/src/components/SectionContent.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import Paper from '@material-ui/core/Paper';
-import { withStyles } from '@material-ui/core/styles';
-import Typography from '@material-ui/core/Typography';
-
-const styles = theme => ({
- content: {
- padding: theme.spacing(2),
- margin: theme.spacing(3),
- }
-});
-
-function SectionContent(props) {
- const { children, classes, title, titleGutter } = props;
- return (
-
-
- {title}
-
- {children}
-
- );
-}
-
-SectionContent.propTypes = {
- classes: PropTypes.object.isRequired,
- children: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.node),
- PropTypes.node
- ]).isRequired,
- title: PropTypes.string.isRequired,
- titleGutter: PropTypes.bool
-};
-
-export default withStyles(styles)(SectionContent);
diff --git a/interface/src/components/SectionContent.tsx b/interface/src/components/SectionContent.tsx
new file mode 100644
index 0000000..457014f
--- /dev/null
+++ b/interface/src/components/SectionContent.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import { Typography, Paper } from '@material-ui/core';
+import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ content: {
+ padding: theme.spacing(2),
+ margin: theme.spacing(3),
+ }
+ })
+);
+
+interface SectionContentProps {
+ title: string;
+ titleGutter?: boolean;
+}
+
+const SectionContent: React.FC = (props) => {
+ const { children, title, titleGutter } = props;
+ const classes = useStyles();
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+};
+
+export default SectionContent;
diff --git a/interface/src/components/index.ts b/interface/src/components/index.ts
new file mode 100644
index 0000000..6b6a5ff
--- /dev/null
+++ b/interface/src/components/index.ts
@@ -0,0 +1,11 @@
+export { default as BlockFormControlLabel } from './BlockFormControlLabel';
+export { default as FormActions } from './FormActions';
+export { default as FormButton } from './FormButton';
+export { default as HighlightAvatar } from './HighlightAvatar';
+export { default as MenuAppBar } from './MenuAppBar';
+export { default as PasswordValidator } from './PasswordValidator';
+export { default as RestFormLoader } from './RestFormLoader';
+export { default as SectionContent } from './SectionContent';
+
+export * from './RestFormLoader';
+export * from './RestController';
diff --git a/interface/src/constants/Env.js b/interface/src/constants/Env.js
deleted file mode 100644
index df674aa..0000000
--- a/interface/src/constants/Env.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME;
-export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH;
-export const ENDPOINT_ROOT = process.env.REACT_APP_ENDPOINT_ROOT;
diff --git a/interface/src/constants/Highlight.js b/interface/src/constants/Highlight.js
deleted file mode 100644
index 6eae2e0..0000000
--- a/interface/src/constants/Highlight.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const IDLE = "idle";
-export const SUCCESS = "success";
-export const ERROR = "error";
-export const WARN = "warn";
diff --git a/interface/src/constants/NTPStatus.js b/interface/src/constants/NTPStatus.js
deleted file mode 100644
index 48d4565..0000000
--- a/interface/src/constants/NTPStatus.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as Highlight from '../constants/Highlight';
-
-export const NTP_INACTIVE = 0;
-export const NTP_ACTIVE = 1;
-
-export const isNtpActive = ntpStatus => ntpStatus && ntpStatus.status === NTP_ACTIVE;
-
-export const ntpStatusHighlight = ntpStatus => {
- switch (ntpStatus.status) {
- case NTP_INACTIVE:
- return Highlight.IDLE;
- case NTP_ACTIVE:
- return Highlight.SUCCESS;
- default:
- return Highlight.ERROR;
- }
-}
-
-export const ntpStatus = ntpStatus => {
- switch (ntpStatus.status) {
- case NTP_INACTIVE:
- return "Inactive";
- case NTP_ACTIVE:
- return "Active";
- default:
- return "Unknown";
- }
-}
diff --git a/interface/src/constants/TimeFormat.js b/interface/src/constants/TimeFormat.js
deleted file mode 100644
index e2fe1d5..0000000
--- a/interface/src/constants/TimeFormat.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import moment from 'moment';
-
-export const formatIsoDateTime = isoDateString => moment.parseZone(isoDateString).format('ll @ HH:mm:ss');
diff --git a/interface/src/constants/WiFiAPModes.js b/interface/src/constants/WiFiAPModes.js
deleted file mode 100644
index 6c1b7a7..0000000
--- a/interface/src/constants/WiFiAPModes.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const WIFI_AP_MODE_ALWAYS = 0;
-export const WIFI_AP_MODE_DISCONNECTED = 1;
-export const WIFI_AP_NEVER = 2;
-
-export const isAPEnabled = apMode => apMode === WIFI_AP_MODE_ALWAYS || apMode === WIFI_AP_MODE_DISCONNECTED;
diff --git a/interface/src/containers/APSettings.js b/interface/src/containers/APSettings.js
deleted file mode 100644
index 45ae270..0000000
--- a/interface/src/containers/APSettings.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { Component } from 'react';
-
-import { AP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent';
-import APSettingsForm from '../forms/APSettingsForm';
-
-class APSettings extends Component {
-
- componentDidMount() {
- this.props.loadData();
- }
-
- render() {
- const { fetched, errorMessage, data, saveData, loadData, handleValueChange } = this.props;
- return (
-
-
-
- }
- />
-
- )
- }
-
-}
-
-export default restComponent(AP_SETTINGS_ENDPOINT, APSettings);
diff --git a/interface/src/containers/APStatus.js b/interface/src/containers/APStatus.js
deleted file mode 100644
index d4ced8e..0000000
--- a/interface/src/containers/APStatus.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import React, { Component, Fragment } from 'react';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemText from '@material-ui/core/ListItemText';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-import Avatar from '@material-ui/core/Avatar';
-import Divider from '@material-ui/core/Divider';
-import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
-import DeviceHubIcon from '@material-ui/icons/DeviceHub';
-import ComputerIcon from '@material-ui/icons/Computer';
-import RefreshIcon from '@material-ui/icons/Refresh';
-
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent'
-
-import * as Highlight from '../constants/Highlight';
-import { AP_STATUS_ENDPOINT } from '../constants/Endpoints';
-
-const styles = theme => ({
- ["apStatus_" + Highlight.SUCCESS]: {
- backgroundColor: theme.palette.highlight_success
- },
- ["apStatus_" + Highlight.IDLE]: {
- backgroundColor: theme.palette.highlight_idle
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class APStatus extends Component {
-
- componentDidMount() {
- this.props.loadData();
- }
-
- apStatusHighlight(data) {
- return data.active ? Highlight.SUCCESS : Highlight.IDLE;
- }
-
- apStatus(data) {
- return data.active ? "Active" : "Inactive";
- }
-
- createListItems(data, classes) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- IP
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- renderAPStatus(data, classes) {
- return (
-
-
- {this.createListItems(data, classes)}
-
- } variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
- Refresh
-
-
- );
- }
-
- render() {
- const { fetched, errorMessage, data, loadData, classes } = this.props;
- return (
-
- this.renderAPStatus(data, classes)
- }
- />
-
- )
- }
-}
-
-export default restComponent(AP_STATUS_ENDPOINT, withStyles(styles)(APStatus));
diff --git a/interface/src/containers/ManageUsers.js b/interface/src/containers/ManageUsers.js
deleted file mode 100644
index 6b91556..0000000
--- a/interface/src/containers/ManageUsers.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { Component } from 'react';
-
-import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent';
-import ManageUsersForm from '../forms/ManageUsersForm';
-
-class ManageUsers extends Component {
-
- componentDidMount() {
- this.props.loadData();
- }
-
- render() {
- const { fetched, errorMessage, data, saveData, loadData, setData, handleValueChange } = this.props;
- return (
-
-
-
- }
- />
-
- )
- }
-
-}
-
-export default restComponent(SECURITY_SETTINGS_ENDPOINT, ManageUsers);
diff --git a/interface/src/containers/NTPSettings.js b/interface/src/containers/NTPSettings.js
deleted file mode 100644
index 6d1b968..0000000
--- a/interface/src/containers/NTPSettings.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { Component } from 'react';
-
-import { NTP_SETTINGS_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent';
-import NTPSettingsForm from '../forms/NTPSettingsForm';
-
-class NTPSettings extends Component {
-
- componentDidMount() {
- this.props.loadData();
- }
-
- render() {
- const { fetched, errorMessage, data, saveData, setData, loadData, handleValueChange, handleCheckboxChange } = this.props;
- return (
-
-
-
- }
- />
-
- )
- }
-
-}
-
-export default restComponent(NTP_SETTINGS_ENDPOINT, NTPSettings);
diff --git a/interface/src/containers/NTPStatus.js b/interface/src/containers/NTPStatus.js
deleted file mode 100644
index 8029ee0..0000000
--- a/interface/src/containers/NTPStatus.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import React, { Component, Fragment } from 'react';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-import ListItemText from '@material-ui/core/ListItemText';
-import Avatar from '@material-ui/core/Avatar';
-import Divider from '@material-ui/core/Divider';
-
-import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
-import AccessTimeIcon from '@material-ui/icons/AccessTime';
-import DNSIcon from '@material-ui/icons/Dns';
-import UpdateIcon from '@material-ui/icons/Update';
-import AvTimerIcon from '@material-ui/icons/AvTimer';
-import RefreshIcon from '@material-ui/icons/Refresh';
-
-import { isNtpActive, ntpStatusHighlight, ntpStatus } from '../constants/NTPStatus';
-import * as Highlight from '../constants/Highlight';
-import { formatIsoDateTime } from '../constants/TimeFormat';
-import { NTP_STATUS_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent';
-
-import moment from 'moment';
-
-const styles = theme => ({
- ["ntpStatus_" + Highlight.SUCCESS]: {
- backgroundColor: theme.palette.highlight_success
- },
- ["ntpStatus_" + Highlight.ERROR]: {
- backgroundColor: theme.palette.highlight_error
- },
- ["ntpStatus_" + Highlight.WARN]: {
- backgroundColor: theme.palette.highlight_warn
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class NTPStatus extends Component {
-
- componentDidMount() {
- this.props.loadData();
- }
-
- createListItems(data, classes) {
- return (
-
-
-
-
-
-
-
-
-
-
- {
- isNtpActive(data) && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- renderNTPStatus(data, classes) {
- return (
-
-
- {this.createListItems(data, classes)}
-
- } variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
- Refresh
-
-
- );
- }
-
- render() {
- const { data, fetched, errorMessage, loadData, classes } = this.props;
- return (
-
- this.renderNTPStatus(data, classes)
- }
- />
-
- );
- }
-}
-
-export default restComponent(NTP_STATUS_ENDPOINT, withStyles(styles)(NTPStatus));
diff --git a/interface/src/containers/OTASettings.js b/interface/src/containers/OTASettings.js
deleted file mode 100644
index 46fd4ec..0000000
--- a/interface/src/containers/OTASettings.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { Component } from 'react';
-
-import { OTA_SETTINGS_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent';
-import OTASettingsForm from '../forms/OTASettingsForm';
-
-class OTASettings extends Component {
-
- componentDidMount() {
- this.props.loadData();
- }
-
- render() {
- const { fetched, errorMessage, data, saveData, loadData, handleValueChange, handleCheckboxChange } = this.props;
- return (
-
-
-
- }
- />
-
- )
- }
-
-}
-
-export default restComponent(OTA_SETTINGS_ENDPOINT, OTASettings);
diff --git a/interface/src/containers/SecuritySettings.js b/interface/src/containers/SecuritySettings.js
deleted file mode 100644
index 9ee2c55..0000000
--- a/interface/src/containers/SecuritySettings.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { Component } from 'react';
-
-import { SECURITY_SETTINGS_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SecuritySettingsForm from '../forms/SecuritySettingsForm';
-import SectionContent from '../components/SectionContent';
-
-class SecuritySettings extends Component {
-
- componentDidMount() {
- this.props.loadData();
- }
-
- render() {
- const { data, fetched, errorMessage, saveData, loadData, handleValueChange } = this.props;
- return (
-
-
-
- }
- />
-
- )
- }
-
-}
-
-export default restComponent(SECURITY_SETTINGS_ENDPOINT, SecuritySettings);
diff --git a/interface/src/containers/WiFiNetworkScanner.js b/interface/src/containers/WiFiNetworkScanner.js
deleted file mode 100644
index 4cdb9be..0000000
--- a/interface/src/containers/WiFiNetworkScanner.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { withSnackbar } from 'notistack';
-
-import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../constants/Endpoints';
-import SectionContent from '../components/SectionContent';
-import WiFiNetworkSelector from '../forms/WiFiNetworkSelector';
-import { redirectingAuthorizedFetch } from '../authentication/Authentication';
-
-const NUM_POLLS = 10
-const POLLING_FREQUENCY = 500
-const RETRY_EXCEPTION_TYPE = "retry"
-
-class WiFiNetworkScanner extends Component {
-
- constructor(props) {
- super(props);
- this.pollCount = 0;
- this.state = {
- scanningForNetworks: true,
- errorMessage: null,
- networkList: null
- };
- this.pollNetworkList = this.pollNetworkList.bind(this);
- this.requestNetworkScan = this.requestNetworkScan.bind(this);
- }
-
- componentDidMount() {
- this.scanNetworks();
- }
-
- requestNetworkScan() {
- const { scanningForNetworks } = this.state;
- if (!scanningForNetworks) {
- this.scanNetworks();
- }
- }
-
- scanNetworks() {
- this.pollCount = 0;
- this.setState({ scanningForNetworks: true, networkList: null, errorMessage: null });
- redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
- if (response.status === 202) {
- this.schedulePollTimeout();
- return;
- }
- throw Error("Scanning for networks returned unexpected response code: " + response.status);
- }).catch(error => {
- this.props.enqueueSnackbar("Problem scanning: " + error.message, {
- variant: 'error',
- });
- this.setState({ scanningForNetworks: false, networkList: null, errorMessage: error.message });
- });
- }
-
- schedulePollTimeout() {
- setTimeout(this.pollNetworkList, POLLING_FREQUENCY);
- }
-
- retryError() {
- return {
- name: RETRY_EXCEPTION_TYPE,
- message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
- };
- }
-
- compareNetworks(network1, network2) {
- if (network1.rssi < network2.rssi)
- return 1;
- if (network1.rssi > network2.rssi)
- return -1;
- return 0;
- }
-
- pollNetworkList() {
- redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
- .then(response => {
- if (response.status === 200) {
- return response.json();
- }
- if (response.status === 202) {
- if (++this.pollCount < NUM_POLLS) {
- this.schedulePollTimeout();
- throw this.retryError();
- } else {
- throw Error("Device did not return network list in timely manner.");
- }
- }
- throw Error("Device returned unexpected response code: " + response.status);
- })
- .then(json => {
- json.networks.sort(this.compareNetworks)
- this.setState({ scanningForNetworks: false, networkList: json, errorMessage: null })
- })
- .catch(error => {
- if (error.name !== RETRY_EXCEPTION_TYPE) {
- this.props.enqueueSnackbar("Problem scanning: " + error.message, {
- variant: 'error',
- });
- this.setState({ scanningForNetworks: false, networkList: null, errorMessage: error.message });
- }
- });
- }
-
- render() {
- const { scanningForNetworks, networkList, errorMessage } = this.state;
- return (
-
-
-
- )
- }
-
-}
-
-WiFiNetworkScanner.propTypes = {
- selectNetwork: PropTypes.func.isRequired
-};
-
-export default withSnackbar(WiFiNetworkScanner);
diff --git a/interface/src/containers/WiFiSettings.js b/interface/src/containers/WiFiSettings.js
deleted file mode 100644
index 9c0802d..0000000
--- a/interface/src/containers/WiFiSettings.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import { WIFI_SETTINGS_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent';
-import WiFiSettingsForm from '../forms/WiFiSettingsForm';
-
-class WiFiSettings extends Component {
-
- constructor(props) {
- super(props);
-
- this.deselectNetworkAndLoadData = this.deselectNetworkAndLoadData.bind(this);
- }
-
- componentDidMount() {
- const { selectedNetwork } = this.props;
- if (selectedNetwork) {
- var wifiSettings = {
- ssid: selectedNetwork.ssid,
- password: "",
- hostname: "esp8266-react",
- static_ip_config: false,
- }
- this.props.setData(wifiSettings);
- } else {
- this.props.loadData();
- }
- }
-
- deselectNetworkAndLoadData() {
- this.props.deselectNetwork();
- this.props.loadData();
- }
-
- render() {
- const { data, fetched, errorMessage, saveData, loadData, handleValueChange, handleCheckboxChange, selectedNetwork, deselectNetwork } = this.props;
- return (
-
-
-
- }
- />
-
- )
- }
-
-}
-
-WiFiSettings.propTypes = {
- deselectNetwork: PropTypes.func,
- selectedNetwork: PropTypes.object
-};
-
-export default restComponent(WIFI_SETTINGS_ENDPOINT, WiFiSettings);
diff --git a/interface/src/forms/APSettingsForm.js b/interface/src/forms/APSettingsForm.js
deleted file mode 100644
index e8d1521..0000000
--- a/interface/src/forms/APSettingsForm.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
-
-import { isAPEnabled } from '../constants/WiFiAPModes';
-import PasswordValidator from '../components/PasswordValidator';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import MenuItem from '@material-ui/core/MenuItem';
-import SaveIcon from '@material-ui/icons/Save';
-
-const styles = theme => ({
- textField: {
- width: "100%"
- },
- selectField: {
- width: "100%",
- marginTop: theme.spacing(2),
- marginBottom: theme.spacing(0.5)
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class APSettingsForm extends React.Component {
-
- render() {
- const { classes, apSettings, handleValueChange, onSubmit, onReset } = this.props;
- return (
-
-
-
-
-
-
- {
- isAPEnabled(apSettings.provision_mode) &&
-
-
-
-
- }
- } variant="contained" color="primary" className={classes.button} type="submit">
- Save
-
-
-
- );
- }
-}
-
-APSettingsForm.propTypes = {
- classes: PropTypes.object.isRequired,
- apSettings: PropTypes.object,
- onSubmit: PropTypes.func.isRequired,
- onReset: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired
-};
-
-export default withStyles(styles)(APSettingsForm);
diff --git a/interface/src/forms/NTPSettingsForm.js b/interface/src/forms/NTPSettingsForm.js
deleted file mode 100644
index 1079b3e..0000000
--- a/interface/src/forms/NTPSettingsForm.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
-
-import { withStyles } from '@material-ui/core/styles';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
-import MenuItem from '@material-ui/core/MenuItem';
-import Switch from '@material-ui/core/Switch';
-import Button from '@material-ui/core/Button';
-import SaveIcon from '@material-ui/icons/Save';
-
-import isIP from '../validators/isIP';
-import isHostname from '../validators/isHostname';
-import or from '../validators/or';
-import { timeZoneSelectItems, selectedTimeZone, TIME_ZONES } from '../constants/TZ';
-
-const styles = theme => ({
- switchControl: {
- width: "100%",
- marginTop: theme.spacing(2),
- marginBottom: theme.spacing(0.5)
- },
- textField: {
- width: "100%"
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class NTPSettingsForm extends React.Component {
-
- componentWillMount() {
- ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
- }
-
- changeTimeZone = (event) => {
- const { ntpSettings, setData } = this.props;
- setData({
- ...ntpSettings,
- tz_label: event.target.value,
- tz_format: TIME_ZONES[event.target.value]
- });
- }
-
- render() {
- const { classes, ntpSettings, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
- return (
-
-
- }
- label="Enable NTP?"
- />
-
-
-
- {timeZoneSelectItems()}
-
- } variant="contained" color="primary" className={classes.button} type="submit">
- Save
-
-
-
- );
- }
-}
-
-NTPSettingsForm.propTypes = {
- classes: PropTypes.object.isRequired,
- ntpSettings: PropTypes.object,
- onSubmit: PropTypes.func.isRequired,
- onReset: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired,
-};
-
-export default withStyles(styles)(NTPSettingsForm);
diff --git a/interface/src/forms/OTASettingsForm.js b/interface/src/forms/OTASettingsForm.js
deleted file mode 100644
index 1d980bf..0000000
--- a/interface/src/forms/OTASettingsForm.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import Switch from '@material-ui/core/Switch';
-import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
-import SaveIcon from '@material-ui/icons/Save';
-
-import isIP from '../validators/isIP';
-import isHostname from '../validators/isHostname';
-import or from '../validators/or';
-import PasswordValidator from '../components/PasswordValidator';
-
-const styles = theme => ({
- switchControl: {
- width: "100%",
- marginTop: theme.spacing(2),
- marginBottom: theme.spacing(0.5)
- },
- textField: {
- width: "100%"
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class OTASettingsForm extends React.Component {
-
- componentWillMount() {
- ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
- }
-
- render() {
- const { classes, otaSettings, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
- return (
-
-
- }
- label="Enable OTA Updates?"
- />
-
-
- } variant="contained" color="primary" className={classes.button} type="submit">
- Save
-
-
-
- );
- }
-}
-
-OTASettingsForm.propTypes = {
- classes: PropTypes.object.isRequired,
- otaSettings: PropTypes.object,
- onSubmit: PropTypes.func.isRequired,
- onReset: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired,
- handleCheckboxChange: PropTypes.func.isRequired,
-};
-
-export default withStyles(styles)(OTASettingsForm);
diff --git a/interface/src/forms/SecuritySettingsForm.js b/interface/src/forms/SecuritySettingsForm.js
deleted file mode 100644
index 7582686..0000000
--- a/interface/src/forms/SecuritySettingsForm.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { ValidatorForm } from 'react-material-ui-form-validator';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import Typography from '@material-ui/core/Typography';
-import Box from '@material-ui/core/Box';
-import SaveIcon from '@material-ui/icons/Save';
-
-import PasswordValidator from '../components/PasswordValidator';
-import { withAuthenticationContext } from '../authentication/Context';
-
-const styles = theme => ({
- textField: {
- width: "100%"
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class SecuritySettingsForm extends React.Component {
-
- onSubmit = () => {
- this.props.onSubmit();
- this.props.authenticationContext.refresh();
- }
-
- render() {
- const { classes, securitySettings, handleValueChange, onReset } = this.props;
- return (
-
-
-
-
- If you modify the JWT Secret, all users will be logged out.
-
-
- } variant="contained" color="primary" className={classes.button} type="submit">
- Save
-
-
-
- );
- }
-}
-
-SecuritySettingsForm.propTypes = {
- classes: PropTypes.object.isRequired,
- securitySettings: PropTypes.object,
- onSubmit: PropTypes.func.isRequired,
- onReset: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired,
- authenticationContext: PropTypes.object.isRequired,
-};
-
-export default withAuthenticationContext(withStyles(styles)(SecuritySettingsForm));
diff --git a/interface/src/forms/UserForm.js b/interface/src/forms/UserForm.js
deleted file mode 100644
index 364feb3..0000000
--- a/interface/src/forms/UserForm.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-
-import FormControlLabel from '@material-ui/core/FormControlLabel';
-import Switch from '@material-ui/core/Switch';
-import FormGroup from '@material-ui/core/FormGroup';
-import DialogTitle from '@material-ui/core/DialogTitle';
-import Dialog from '@material-ui/core/Dialog';
-import DialogContent from '@material-ui/core/DialogContent';
-import DialogActions from '@material-ui/core/DialogActions';
-
-import PasswordValidator from '../components/PasswordValidator';
-
-const styles = theme => ({
- textField: {
- width: "100%"
- },
- button: {
- margin: theme.spacing(0.5)
- }
-});
-
-class UserForm extends React.Component {
-
- constructor(props) {
- super(props);
- this.formRef = React.createRef();
- }
-
- componentWillMount() {
- ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
- }
-
- submit = () => {
- this.formRef.current.submit();
- }
-
- render() {
- const { classes, user, creating, handleValueChange, handleCheckboxChange, onDoneEditing, onCancelEditing } = this.props;
- return (
-
-
-
- );
- }
-}
-
-UserForm.propTypes = {
- classes: PropTypes.object.isRequired,
- user: PropTypes.object.isRequired,
- creating: PropTypes.bool.isRequired,
- onDoneEditing: PropTypes.func.isRequired,
- onCancelEditing: PropTypes.func.isRequired,
- uniqueUsername: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired,
- handleCheckboxChange: PropTypes.func.isRequired
-};
-
-export default withStyles(styles)(UserForm);
diff --git a/interface/src/forms/WiFiNetworkSelector.js b/interface/src/forms/WiFiNetworkSelector.js
deleted file mode 100644
index 5824da5..0000000
--- a/interface/src/forms/WiFiNetworkSelector.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import LinearProgress from '@material-ui/core/LinearProgress';
-import Typography from '@material-ui/core/Typography';
-
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import ListItemText from '@material-ui/core/ListItemText';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-
-import Avatar from '@material-ui/core/Avatar';
-import Badge from '@material-ui/core/Badge';
-
-import WifiIcon from '@material-ui/icons/Wifi';
-import LockIcon from '@material-ui/icons/Lock';
-import LockOpenIcon from '@material-ui/icons/LockOpen';
-import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
-
-import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes';
-
-const styles = theme => ({
- scanningProgress: {
- margin: theme.spacing(4),
- textAlign: "center"
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class WiFiNetworkSelector extends Component {
-
- constructor(props) {
- super(props);
-
- this.renderNetwork = this.renderNetwork.bind(this);
- }
-
- renderNetwork(network) {
- return (
- this.props.selectNetwork(network)}>
-
-
- {isNetworkOpen(network) ? : }
-
-
-
-
-
-
-
-
-
- );
- }
-
- render() {
- const { classes, scanningForNetworks, networkList, errorMessage, requestNetworkScan } = this.props;
- return (
-
- {
- scanningForNetworks ?
-
-
-
- Scanning...
-
-
- :
- networkList ?
-
- {networkList.networks.map(this.renderNetwork)}
-
- :
-
-
- {errorMessage}
-
-
- }
-
-
} variant="contained" color="secondary" className={classes.button} onClick={requestNetworkScan} disabled={scanningForNetworks}>
- Scan again...
-
-
- );
- }
-}
-
-WiFiNetworkSelector.propTypes = {
- classes: PropTypes.object.isRequired,
- selectNetwork: PropTypes.func.isRequired,
- scanningForNetworks: PropTypes.bool.isRequired,
- errorMessage: PropTypes.string,
- networkList: PropTypes.object,
- requestNetworkScan: PropTypes.func.isRequired
-};
-
-export default withStyles(styles)(WiFiNetworkSelector);
diff --git a/interface/src/forms/WiFiSettingsForm.js b/interface/src/forms/WiFiSettingsForm.js
deleted file mode 100644
index 0bf2859..0000000
--- a/interface/src/forms/WiFiSettingsForm.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import Checkbox from '@material-ui/core/Checkbox';
-import FormControlLabel from '@material-ui/core/FormControlLabel';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemText from '@material-ui/core/ListItemText';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
-
-import Avatar from '@material-ui/core/Avatar';
-import IconButton from '@material-ui/core/IconButton';
-import LockIcon from '@material-ui/icons/Lock';
-import LockOpenIcon from '@material-ui/icons/LockOpen';
-import DeleteIcon from '@material-ui/icons/Delete';
-import SaveIcon from '@material-ui/icons/Save';
-
-import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
-import { isNetworkOpen, networkSecurityMode } from '../constants/WiFiSecurityModes';
-
-import isIP from '../validators/isIP';
-import isHostname from '../validators/isHostname';
-import optional from '../validators/optional';
-import PasswordValidator from '../components/PasswordValidator';
-
-const styles = theme => ({
- textField: {
- width: "100%"
- },
- checkboxControl: {
- width: "100%"
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
-
-class WiFiSettingsForm extends React.Component {
-
- componentWillMount() {
- ValidatorForm.addValidationRule('isIP', isIP);
- ValidatorForm.addValidationRule('isHostname', isHostname);
- ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
- }
-
- renderSelectedNetwork() {
- const { selectedNetwork, deselectNetwork } = this.props;
- return (
-
-
-
-
- {isNetworkOpen(selectedNetwork) ? : }
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- render() {
- const { classes, wifiSettings, selectedNetwork, handleValueChange, handleCheckboxChange, onSubmit, onReset } = this.props;
- return (
-
- {
- selectedNetwork ? this.renderSelectedNetwork() :
-
- }
- {
- !isNetworkOpen(selectedNetwork) &&
-
- }
-
-
- }
- label="Static IP Config?"
- />
- {
- wifiSettings.static_ip_config &&
-
-
-
-
-
-
-
- }
- } variant="contained" color="primary" className={classes.button} type="submit">
- Save
-
-
-
- );
- }
-}
-
-WiFiSettingsForm.propTypes = {
- classes: PropTypes.object.isRequired,
- wifiSettings: PropTypes.object,
- deselectNetwork: PropTypes.func,
- selectedNetwork: PropTypes.object,
- onSubmit: PropTypes.func.isRequired,
- onReset: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired,
- handleCheckboxChange: PropTypes.func.isRequired
-};
-
-export default withStyles(styles)(WiFiSettingsForm);
diff --git a/interface/src/history.js b/interface/src/history.ts
similarity index 100%
rename from interface/src/history.js
rename to interface/src/history.ts
diff --git a/interface/src/index.js b/interface/src/index.tsx
similarity index 100%
rename from interface/src/index.js
rename to interface/src/index.tsx
diff --git a/interface/src/ntp/NTPSettingsController.tsx b/interface/src/ntp/NTPSettingsController.tsx
new file mode 100644
index 0000000..78f5f63
--- /dev/null
+++ b/interface/src/ntp/NTPSettingsController.tsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import { NTP_SETTINGS_ENDPOINT } from '../api';
+
+import NTPSettingsForm from './NTPSettingsForm';
+import { NTPSettings } from './types';
+
+type NTPSettingsControllerProps = RestControllerProps;
+
+class NTPSettingsController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ )
+ }
+
+}
+
+export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
diff --git a/interface/src/ntp/NTPSettingsForm.tsx b/interface/src/ntp/NTPSettingsForm.tsx
new file mode 100644
index 0000000..ba82a70
--- /dev/null
+++ b/interface/src/ntp/NTPSettingsForm.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
+
+import { Checkbox, MenuItem } from '@material-ui/core';
+import SaveIcon from '@material-ui/icons/Save';
+
+import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
+import { isIP, isHostname, or } from '../validators';
+
+import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
+import { NTPSettings } from './types';
+
+type NTPSettingsFormProps = RestFormProps;
+
+class NTPSettingsForm extends React.Component {
+
+ componentDidMount() {
+ ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
+ }
+
+ changeTimeZone = (event: React.ChangeEvent) => {
+ const { data, setData } = this.props;
+ setData({
+ ...data,
+ tz_label: event.target.value,
+ tz_format: TIME_ZONES[event.target.value]
+ });
+ }
+
+ render() {
+ const { data, handleValueChange, handleCheckboxChange, saveData, loadData } = this.props;
+ return (
+
+
+ }
+ label="Enable NTP?"
+ />
+
+
+
+ {timeZoneSelectItems()}
+
+
+ } variant="contained" color="primary" type="submit">
+ Save
+
+
+ Reset
+
+
+
+ );
+ }
+}
+
+export default NTPSettingsForm;
diff --git a/interface/src/ntp/NTPStatus.ts b/interface/src/ntp/NTPStatus.ts
new file mode 100644
index 0000000..21601f9
--- /dev/null
+++ b/interface/src/ntp/NTPStatus.ts
@@ -0,0 +1,29 @@
+import { Theme } from "@material-ui/core";
+import { NTPStatus } from "./types";
+
+export const NTP_INACTIVE = 0;
+export const NTP_ACTIVE = 1;
+
+export const isNtpActive = ({ status }: NTPStatus) => status === NTP_ACTIVE;
+
+export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
+ switch (status) {
+ case NTP_INACTIVE:
+ return theme.palette.info.main;
+ case NTP_ACTIVE:
+ return theme.palette.success.main;
+ default:
+ return theme.palette.error.main;
+ }
+}
+
+export const ntpStatus = ({ status }: NTPStatus) => {
+ switch (status) {
+ case NTP_INACTIVE:
+ return "Inactive";
+ case NTP_ACTIVE:
+ return "Active";
+ default:
+ return "Unknown";
+ }
+}
diff --git a/interface/src/ntp/NTPStatusController.tsx b/interface/src/ntp/NTPStatusController.tsx
new file mode 100644
index 0000000..25ea4de
--- /dev/null
+++ b/interface/src/ntp/NTPStatusController.tsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import { NTP_STATUS_ENDPOINT } from '../api';
+
+import NTPStatusForm from './NTPStatusForm';
+import { NTPStatus } from './types';
+
+type NTPStatusControllerProps = RestControllerProps;
+
+class NTPStatusController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+}
+
+export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
diff --git a/interface/src/ntp/NTPStatusForm.tsx b/interface/src/ntp/NTPStatusForm.tsx
new file mode 100644
index 0000000..72b4500
--- /dev/null
+++ b/interface/src/ntp/NTPStatusForm.tsx
@@ -0,0 +1,89 @@
+import React, { Component, Fragment } from 'react';
+import moment from 'moment';
+
+import { WithTheme, withTheme } from '@material-ui/core/styles';
+import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
+
+import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
+import AccessTimeIcon from '@material-ui/icons/AccessTime';
+import DNSIcon from '@material-ui/icons/Dns';
+import UpdateIcon from '@material-ui/icons/Update';
+import AvTimerIcon from '@material-ui/icons/AvTimer';
+import RefreshIcon from '@material-ui/icons/Refresh';
+
+import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
+
+import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
+import { formatIsoDateTime } from './TimeFormat';
+import { NTPStatus } from './types';
+
+type NTPStatusFormProps = RestFormProps & WithTheme;
+
+class NTPStatusForm extends Component {
+
+ render() {
+ const { data, theme } = this.props
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {isNtpActive(data) && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } variant="contained" color="secondary" onClick={this.props.loadData}>
+ Refresh
+
+
+
+ );
+ }
+}
+
+export default withTheme(NTPStatusForm);
diff --git a/interface/src/ntp/NetworkTime.tsx b/interface/src/ntp/NetworkTime.tsx
new file mode 100644
index 0000000..8855c7b
--- /dev/null
+++ b/interface/src/ntp/NetworkTime.tsx
@@ -0,0 +1,39 @@
+import React, { Component } from 'react';
+import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
+
+import { Tabs, Tab } from '@material-ui/core';
+
+import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
+import { MenuAppBar } from '../components';
+
+import NTPStatusController from './NTPStatusController';
+import NTPSettingsController from './NTPSettingsController';
+
+type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
+
+class NetworkTime extends Component {
+
+ handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
+ this.props.history.push(path);
+ };
+
+ render() {
+ const { authenticatedContext } = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+
+}
+
+export default withAuthenticatedContext(NetworkTime)
diff --git a/interface/src/constants/TZ.js b/interface/src/ntp/TZ.tsx
similarity index 99%
rename from interface/src/constants/TZ.js
rename to interface/src/ntp/TZ.tsx
index bb410ff..1f5ea9d 100644
--- a/interface/src/constants/TZ.js
+++ b/interface/src/ntp/TZ.tsx
@@ -1,7 +1,11 @@
import React from 'react';
import MenuItem from '@material-ui/core/MenuItem';
-export const TIME_ZONES = {
+type TimeZones = {
+ [name: string]: string
+};
+
+export const TIME_ZONES: TimeZones = {
"Africa/Abidjan": "GMT0",
"Africa/Accra": "GMT0",
"Africa/Addis_Ababa": "EAT-3",
@@ -464,7 +468,7 @@ export const TIME_ZONES = {
"Etc/Zulu": "UTC0"
}
-export function selectedTimeZone(label, format){
+export function selectedTimeZone(label: string, format: string) {
return TIME_ZONES[label] === format ? label : undefined;
}
diff --git a/interface/src/ntp/TimeFormat.ts b/interface/src/ntp/TimeFormat.ts
new file mode 100644
index 0000000..9319e3a
--- /dev/null
+++ b/interface/src/ntp/TimeFormat.ts
@@ -0,0 +1,3 @@
+import moment from 'moment';
+
+export const formatIsoDateTime = (isoDateString: string) => moment.parseZone(isoDateString).format('ll @ HH:mm:ss');
diff --git a/interface/src/ntp/types.ts b/interface/src/ntp/types.ts
new file mode 100644
index 0000000..2b36802
--- /dev/null
+++ b/interface/src/ntp/types.ts
@@ -0,0 +1,14 @@
+export interface NTPStatus {
+ status: number;
+ time_utc: string;
+ time_local: string;
+ server: string;
+ uptime: number;
+}
+
+export interface NTPSettings {
+ enabled: boolean;
+ server: string;
+ tz_label: string;
+ tz_format: string;
+}
diff --git a/interface/src/project/DemoController.js b/interface/src/project/DemoController.js
deleted file mode 100644
index 0624c3a..0000000
--- a/interface/src/project/DemoController.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import React, { Component } from 'react';
-import { ValidatorForm } from 'react-material-ui-form-validator';
-
-import { ENDPOINT_ROOT } from '../constants/Env';
-import SectionContent from '../components/SectionContent';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-
-import Button from '@material-ui/core/Button';
-import Typography from '@material-ui/core/Typography';
-import Slider from '@material-ui/core/Slider';
-import { makeStyles } from '@material-ui/core/styles';
-import SaveIcon from '@material-ui/icons/Save';
-
-export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings";
-
-const valueToPercentage = (value) => `${Math.round(value / 255 * 100)}%`;
-
-class DemoController extends Component {
- componentDidMount() {
- this.props.loadData();
- }
-
- render() {
- const { data, fetched, errorMessage, saveData, loadData, handleSliderChange } = this.props;
- return (
-
-
-
- }
- />
-
- )
- }
-}
-
-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 (
-
-
- Blink Speed
-
-
- } variant="contained" color="primary" className={classes.button} type="submit">
- Save
-
-
-
- );
-}
-
-export default restComponent(DEMO_SETTINGS_ENDPOINT, DemoController);
diff --git a/interface/src/project/DemoController.tsx b/interface/src/project/DemoController.tsx
new file mode 100644
index 0000000..2ff971a
--- /dev/null
+++ b/interface/src/project/DemoController.tsx
@@ -0,0 +1,75 @@
+import React, { Component } from 'react';
+import { ValidatorForm } from 'react-material-ui-form-validator';
+
+import { Typography, Slider, Box } from '@material-ui/core';
+import SaveIcon from '@material-ui/icons/Save';
+
+import { ENDPOINT_ROOT } from '../api';
+import { restController, RestControllerProps, RestFormLoader, RestFormProps, FormActions, FormButton, SectionContent } from '../components';
+
+export const DEMO_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "demoSettings";
+
+interface DemoSettings {
+ blink_speed: number;
+}
+
+type DemoControllerProps = RestControllerProps;
+
+class DemoController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ (
+
+ )}
+ />
+
+ )
+ }
+
+}
+
+export default restController(DEMO_SETTINGS_ENDPOINT, DemoController);
+
+const valueToPercentage = (value: number) => `${Math.round(value / 255 * 100)}%`;
+
+type DemoControllerFormProps = RestFormProps;
+
+function DemoControllerForm(props: DemoControllerFormProps) {
+ const { data, saveData, loadData, handleSliderChange } = props;
+ return (
+
+
+ Blink Speed
+
+
+
+
+
+ } variant="contained" color="primary" type="submit">
+ Save
+
+
+ Reset
+
+
+
+ );
+}
+
+
diff --git a/interface/src/project/DemoInformation.js b/interface/src/project/DemoInformation.tsx
similarity index 64%
rename from interface/src/project/DemoInformation.js
rename to interface/src/project/DemoInformation.tsx
index f42b81c..fd3fde2 100644
--- a/interface/src/project/DemoInformation.js
+++ b/interface/src/project/DemoInformation.tsx
@@ -1,29 +1,14 @@
import React, { Component } from 'react';
-
-import { withStyles } from '@material-ui/core/styles';
-import Table from '@material-ui/core/Table';
-import TableHead from '@material-ui/core/TableHead';
-import TableCell from '@material-ui/core/TableCell';
-import TableBody from '@material-ui/core/TableBody';
-import TableRow from '@material-ui/core/TableRow';
-import Typography from '@material-ui/core/Typography';
-
-import SectionContent from '../components/SectionContent';
-
-const styles = theme => ({
- fileTable: {
- marginBottom: theme.spacing(2)
- }
-});
+import { Typography, TableRow, TableBody, TableCell, TableHead, Table, Box } from '@material-ui/core';
+import { SectionContent } from '../components';
class DemoInformation extends Component {
render() {
- const { classes } = this.props;
return (
-
+
- This simple demo project allows you to control the blink speed of the built-in LED.
+ This simple demo project allows you to control the blink speed of the built-in LED.
It demonstrates how the esp8266-react framework may be extended for your own IoT project.
@@ -34,7 +19,7 @@ class DemoInformation extends Component {
The demo project interface code stored in the interface/project directory:
-
+
@@ -48,7 +33,7 @@ class DemoInformation extends Component {
- ProjectMenu.js
+ ProjectMenu.tsx
You can add your project's screens to the side bar here.
@@ -56,7 +41,7 @@ class DemoInformation extends Component {
- ProjectRouting.js
+ ProjectRouting.tsx
The routing which controls the screens of your project.
@@ -64,7 +49,7 @@ class DemoInformation extends Component {
- DemoProject.js
+ DemoProject.tsx
This screen, with tabs and tab routing.
@@ -72,29 +57,31 @@ class DemoInformation extends Component {
- DemoInformation.js
+ DemoInformation.tsx
- The demo information tab.
+ The demo information page.
- DemoController.js
+ DemoController.tsx
The demo controller tab, to control the built-in LED.
-
+
-
- See the project README for a full description of the demo project.
-
+
+
+ See the project README for a full description of the demo project.
+
+
)
}
}
-export default withStyles(styles)(DemoInformation);
+export default DemoInformation;
diff --git a/interface/src/project/DemoProject.js b/interface/src/project/DemoProject.tsx
similarity index 57%
rename from interface/src/project/DemoProject.js
rename to interface/src/project/DemoProject.tsx
index 6f201da..99bdd19 100644
--- a/interface/src/project/DemoProject.js
+++ b/interface/src/project/DemoProject.tsx
@@ -1,36 +1,36 @@
import React, { Component } from 'react';
-import { Redirect, Switch } from 'react-router-dom'
+import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
+
+import { Tabs, Tab } from '@material-ui/core';
+
+import { PROJECT_PATH } from '../api';
+import { MenuAppBar } from '../components';
+import { AuthenticatedRoute } from '../authentication';
-import { PROJECT_PATH } from '../constants/Env';
-import MenuAppBar from '../components/MenuAppBar';
-import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
import DemoInformation from './DemoInformation';
import DemoController from './DemoController';
-import Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
+class DemoProject extends Component {
-class DemoProject extends Component {
-
- handleTabChange = (event, path) => {
+ handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
this.props.history.push(path);
};
render() {
return (
-
-
-
+
+
+
-
+
)
- }
+ }
}
diff --git a/interface/src/project/ProjectMenu.js b/interface/src/project/ProjectMenu.tsx
similarity index 60%
rename from interface/src/project/ProjectMenu.js
rename to interface/src/project/ProjectMenu.tsx
index 721c799..b7d2739 100644
--- a/interface/src/project/ProjectMenu.js
+++ b/interface/src/project/ProjectMenu.tsx
@@ -1,15 +1,12 @@
import React, { Component } from 'react';
-import { Link, withRouter } from 'react-router-dom';
+import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
-import { PROJECT_PATH } from '../constants/Env';
-
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemIcon from '@material-ui/core/ListItemIcon';
-import ListItemText from '@material-ui/core/ListItemText';
+import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
import SettingsRemoteIcon from '@material-ui/icons/SettingsRemote';
-class ProjectMenu extends Component {
+import { PROJECT_PATH } from '../api';
+
+class ProjectMenu extends Component {
render() {
const path = this.props.match.url;
diff --git a/interface/src/project/ProjectRouting.js b/interface/src/project/ProjectRouting.tsx
similarity index 86%
rename from interface/src/project/ProjectRouting.js
rename to interface/src/project/ProjectRouting.tsx
index 5086757..fc378e6 100644
--- a/interface/src/project/ProjectRouting.js
+++ b/interface/src/project/ProjectRouting.tsx
@@ -1,8 +1,9 @@
import React, { Component } from 'react';
import { Redirect, Switch } from 'react-router';
-import { PROJECT_PATH } from '../constants/Env';
-import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
+import { PROJECT_PATH } from '../api';
+import { AuthenticatedRoute } from '../authentication';
+
import DemoProject from './DemoProject';
class ProjectRouting extends Component {
diff --git a/interface/src/react-app-env.d.ts b/interface/src/react-app-env.d.ts
new file mode 100644
index 0000000..6431bc5
--- /dev/null
+++ b/interface/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/interface/src/sections/AccessPoint.js b/interface/src/sections/AccessPoint.js
deleted file mode 100644
index 011b673..0000000
--- a/interface/src/sections/AccessPoint.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React, { Component } from 'react';
-import { Redirect, Switch } from 'react-router-dom'
-
-import Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
-
-import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
-import MenuAppBar from '../components/MenuAppBar';
-import APSettings from '../containers/APSettings';
-import APStatus from '../containers/APStatus';
-import { withAuthenticationContext } from '../authentication/Context.js';
-
-class AccessPoint extends Component {
-
- handleTabChange = (event, path) => {
- this.props.history.push(path);
- };
-
- render() {
- const { authenticationContext } = this.props;
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
- }
-}
-
-export default withAuthenticationContext(AccessPoint);
diff --git a/interface/src/sections/NetworkTime.js b/interface/src/sections/NetworkTime.js
deleted file mode 100644
index 4b11c25..0000000
--- a/interface/src/sections/NetworkTime.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { Component } from 'react';
-import { Redirect, Switch } from 'react-router-dom'
-
-import Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
-
-import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
-import MenuAppBar from '../components/MenuAppBar';
-import NTPSettings from '../containers/NTPSettings';
-import NTPStatus from '../containers/NTPStatus';
-import { withAuthenticationContext } from '../authentication/Context.js';
-
-class NetworkTime extends Component {
-
- handleTabChange = (event, path) => {
- this.props.history.push(path);
- };
-
- render() {
- const { authenticationContext } = this.props;
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
- }
-
-}
-
-export default withAuthenticationContext(NetworkTime)
diff --git a/interface/src/sections/Security.js b/interface/src/sections/Security.js
deleted file mode 100644
index c2f619c..0000000
--- a/interface/src/sections/Security.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { Component } from 'react';
-import { Redirect, Switch } from 'react-router-dom'
-
-import Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
-
-import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
-import MenuAppBar from '../components/MenuAppBar';
-import ManageUsers from '../containers/ManageUsers';
-import SecuritySettings from '../containers/SecuritySettings';
-
-class Security extends Component {
-
- handleTabChange = (event, path) => {
- this.props.history.push(path);
- };
-
- render() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
- }
-}
-
-export default Security;
diff --git a/interface/src/sections/System.js b/interface/src/sections/System.js
deleted file mode 100644
index 2aca1cd..0000000
--- a/interface/src/sections/System.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React, { Component } from 'react';
-import { Redirect, Switch } from 'react-router-dom'
-
-import Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
-
-import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
-import MenuAppBar from '../components/MenuAppBar';
-import OTASettings from '../containers/OTASettings';
-import SystemStatus from '../containers/SystemStatus';
-import { withAuthenticationContext } from '../authentication/Context.js';
-
-class System extends Component {
-
- handleTabChange = (event, path) => {
- this.props.history.push(path);
- };
-
- render() {
- const { authenticationContext } = this.props;
- return (
-
-
-
-
-
-
-
-
-
-
-
- )
- }
-}
-
-export default withAuthenticationContext(System);
diff --git a/interface/src/sections/WiFiConnection.js b/interface/src/sections/WiFiConnection.js
deleted file mode 100644
index 1bb2b9b..0000000
--- a/interface/src/sections/WiFiConnection.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { Component } from 'react';
-import { Redirect, Switch } from 'react-router-dom'
-
-import Tabs from '@material-ui/core/Tabs';
-import Tab from '@material-ui/core/Tab';
-
-import AuthenticatedRoute from '../authentication/AuthenticatedRoute';
-import MenuAppBar from '../components/MenuAppBar';
-import WiFiNetworkScanner from '../containers/WiFiNetworkScanner';
-import WiFiSettings from '../containers/WiFiSettings';
-import WiFiStatus from '../containers/WiFiStatus';
-import { withAuthenticationContext } from '../authentication/Context.js';
-
-class WiFiConnection extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- selectedNetwork: null
- };
- this.selectNetwork = this.selectNetwork.bind(this);
- this.deselectNetwork = this.deselectNetwork.bind(this);
- }
-
- selectNetwork(network) {
- this.setState({ selectedNetwork: network });
- this.props.history.push('/wifi/settings');
- }
-
- deselectNetwork(network) {
- this.setState({ selectedNetwork: null });
- }
-
- handleTabChange = (event, path) => {
- this.props.history.push(path);
- };
-
- render() {
- const { authenticationContext } = this.props;
- const ConfiguredWiFiNetworkScanner = (props) => {
- return (
-
- );
- };
- const ConfiguredWiFiSettings = (props) => {
- return (
-
- );
- };
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
- }
-}
-
-export default withAuthenticationContext(WiFiConnection);
diff --git a/interface/src/security/ManageUsersController.tsx b/interface/src/security/ManageUsersController.tsx
new file mode 100644
index 0000000..ccd3cde
--- /dev/null
+++ b/interface/src/security/ManageUsersController.tsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import { SECURITY_SETTINGS_ENDPOINT } from '../api';
+
+import ManageUsersForm from './ManageUsersForm';
+import { SecuritySettings } from './types';
+
+type ManageUsersControllerProps = RestControllerProps;
+
+class ManageUsersController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ )
+ }
+
+}
+
+export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
diff --git a/interface/src/forms/ManageUsersForm.js b/interface/src/security/ManageUsersForm.tsx
similarity index 55%
rename from interface/src/forms/ManageUsersForm.js
rename to interface/src/security/ManageUsersForm.tsx
index 5368a87..c6fc783 100644
--- a/interface/src/forms/ManageUsersForm.js
+++ b/interface/src/security/ManageUsersForm.tsx
@@ -1,18 +1,9 @@
import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-
import { ValidatorForm } from 'react-material-ui-form-validator';
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import Typography from '@material-ui/core/Typography';
-import Table from '@material-ui/core/Table';
-import TableBody from '@material-ui/core/TableBody';
-import TableCell from '@material-ui/core/TableCell';
-import TableFooter from '@material-ui/core/TableFooter';
-import TableHead from '@material-ui/core/TableHead';
-import TableRow from '@material-ui/core/TableRow';
-import Box from '@material-ui/core/Box';
+import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow } from '@material-ui/core';
+import { Box, Button, Typography, } from '@material-ui/core';
+
import EditIcon from '@material-ui/icons/Edit';
import DeleteIcon from '@material-ui/icons/Delete';
import CloseIcon from '@material-ui/icons/Close';
@@ -21,20 +12,13 @@ import IconButton from '@material-ui/core/IconButton';
import SaveIcon from '@material-ui/icons/Save';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
+import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
+import { RestFormProps, FormActions, FormButton } from '../components';
+
import UserForm from './UserForm';
-import { withAuthenticationContext } from '../authentication/Context';
+import { SecuritySettings, User } from './types';
-const styles = theme => ({
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- },
- table: {
- '& td, & th': { padding: theme.spacing(0.5) }
- }
-});
-
-function compareUsers(a, b) {
+function compareUsers(a: User, b: User) {
if (a.username < b.username) {
return -1;
}
@@ -44,12 +28,18 @@ function compareUsers(a, b) {
return 0;
}
-class ManageUsersForm extends React.Component {
+type ManageUsersFormProps = RestFormProps & AuthenticatedContextProps;
- constructor(props) {
- super(props);
- this.state = {};
- }
+type ManageUsersFormState = {
+ creating: boolean;
+ user?: User;
+}
+
+class ManageUsersForm extends React.Component {
+
+ state: ManageUsersFormState = {
+ creating: false
+ };
createUser = () => {
this.setState({
@@ -62,21 +52,21 @@ class ManageUsersForm extends React.Component {
});
};
- uniqueUsername = username => {
- return !this.props.userData.users.find(u => u.username === username);
+ uniqueUsername = (username: string) => {
+ return !this.props.data.users.find(u => u.username === username);
}
noAdminConfigured = () => {
- return !this.props.userData.users.find(u => u.admin);
+ return !this.props.data.users.find(u => u.admin);
}
- removeUser = user => {
- const { userData } = this.props;
- const users = userData.users.filter(u => u.username !== user.username);
- this.props.setData({ ...userData, users });
+ removeUser = (user: User) => {
+ const { data } = this.props;
+ const users = data.users.filter(u => u.username !== user.username);
+ this.props.setData({ ...data, users });
}
- startEditingUser = user => {
+ startEditingUser = (user: User) => {
this.setState({
creating: false,
user
@@ -91,45 +81,37 @@ class ManageUsersForm extends React.Component {
doneEditingUser = () => {
const { user } = this.state;
- const { userData } = this.props;
- const users = userData.users.filter(u => u.username !== user.username);
- users.push(user);
- this.props.setData({ ...userData, users });
- this.setState({
- user: undefined
- });
+ if (user) {
+ const { data } = this.props;
+ const users = data.users.filter(u => u.username !== user.username);
+ users.push(user);
+ this.props.setData({ ...data, users });
+ this.setState({
+ user: undefined
+ });
+ }
};
- handleUserValueChange = name => event => {
- const { user } = this.state;
- this.setState({
- user: {
- ...user, [name]: event.target.value
- }
- });
+ handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent) => {
+ this.setState({ user: { ...this.state.user!, [name]: event.target.value } });
};
- handleUserCheckboxChange = name => event => {
- const { user } = this.state;
- this.setState({
- user: {
- ...user, [name]: event.target.checked
- }
- });
+ handleUserCheckboxChange = (name: keyof User) => (event: React.ChangeEvent) => {
+ this.setState({ user: { ...this.state.user!, [name]: event.target.checked } });
}
onSubmit = () => {
- this.props.onSubmit();
- this.props.authenticationContext.refresh();
+ this.props.saveData();
+ this.props.authenticatedContext.refresh();
}
render() {
- const { classes, userData, onReset } = this.props;
+ const { data, loadData } = this.props;
const { user, creating } = this.state;
return (
-
+
Username
@@ -138,7 +120,7 @@ class ManageUsersForm extends React.Component {
- {userData.users.sort(compareUsers).map(user => (
+ {data.users.sort(compareUsers).map(user => (
{user.username}
@@ -178,12 +160,14 @@ class ManageUsersForm extends React.Component {
}
- } variant="contained" color="primary" className={classes.button} type="submit" disabled={this.noAdminConfigured()}>
- Save
-
-
+
+ } variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}>
+ Save
+
+
+ Reset
+
+
{
user &&
@@ -203,14 +187,4 @@ class ManageUsersForm extends React.Component {
}
-ManageUsersForm.propTypes = {
- classes: PropTypes.object.isRequired,
- userData: PropTypes.object,
- onSubmit: PropTypes.func.isRequired,
- onReset: PropTypes.func.isRequired,
- setData: PropTypes.func.isRequired,
- handleValueChange: PropTypes.func.isRequired,
- authenticationContext: PropTypes.object.isRequired,
-};
-
-export default withAuthenticationContext(withStyles(styles)(ManageUsersForm));
+export default withAuthenticatedContext(ManageUsersForm);
diff --git a/interface/src/security/Security.tsx b/interface/src/security/Security.tsx
new file mode 100644
index 0000000..4371efa
--- /dev/null
+++ b/interface/src/security/Security.tsx
@@ -0,0 +1,37 @@
+import React, { Component } from 'react';
+import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
+
+import { Tabs, Tab } from '@material-ui/core';
+
+import { AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
+import { MenuAppBar } from '../components';
+
+import ManageUsersController from './ManageUsersController';
+import SecuritySettingsController from './SecuritySettingsController';
+
+type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
+
+class Security extends Component {
+
+ handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
+ this.props.history.push(path);
+ };
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default Security;
diff --git a/interface/src/security/SecuritySettingsController.tsx b/interface/src/security/SecuritySettingsController.tsx
new file mode 100644
index 0000000..d42328a
--- /dev/null
+++ b/interface/src/security/SecuritySettingsController.tsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import { SECURITY_SETTINGS_ENDPOINT } from '../api';
+
+import SecuritySettingsForm from './SecuritySettingsForm';
+import { SecuritySettings } from './types';
+
+type SecuritySettingsControllerProps = RestControllerProps;
+
+class SecuritySettingsController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+}
+
+export default restController(SECURITY_SETTINGS_ENDPOINT, SecuritySettingsController);
diff --git a/interface/src/security/SecuritySettingsForm.tsx b/interface/src/security/SecuritySettingsForm.tsx
new file mode 100644
index 0000000..22d23b5
--- /dev/null
+++ b/interface/src/security/SecuritySettingsForm.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { ValidatorForm } from 'react-material-ui-form-validator';
+
+import { Box, Typography } from '@material-ui/core';
+import SaveIcon from '@material-ui/icons/Save';
+
+import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
+import { RestFormProps, PasswordValidator, FormActions, FormButton } from '../components';
+
+import { SecuritySettings } from './types';
+
+type SecuritySettingsFormProps = RestFormProps & AuthenticatedContextProps;
+
+class SecuritySettingsForm extends React.Component {
+
+ onSubmit = () => {
+ this.props.saveData();
+ this.props.authenticatedContext.refresh();
+ }
+
+ render() {
+ const { data, handleValueChange, loadData } = this.props;
+ return (
+
+
+
+
+ If you modify the JWT Secret, all users will be logged out.
+
+
+
+ } variant="contained" color="primary" type="submit">
+ Save
+
+
+ Reset
+
+
+
+ );
+ }
+
+}
+
+export default withAuthenticatedContext(SecuritySettingsForm);
diff --git a/interface/src/security/UserForm.tsx b/interface/src/security/UserForm.tsx
new file mode 100644
index 0000000..92b7c67
--- /dev/null
+++ b/interface/src/security/UserForm.tsx
@@ -0,0 +1,87 @@
+import React, { RefObject } from 'react';
+import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
+
+import { Dialog, DialogTitle, DialogContent, DialogActions, Checkbox } from '@material-ui/core';
+
+import { PasswordValidator, BlockFormControlLabel, FormButton } from '../components';
+
+import { User } from './types';
+
+interface UserFormProps {
+ creating: boolean;
+ user: User;
+ uniqueUsername: (value: any) => boolean;
+ handleValueChange: (name: keyof User) => (event: React.ChangeEvent) => void;
+ handleCheckboxChange: (name: keyof User) => (event: React.ChangeEvent, checked: boolean) => void;
+ onDoneEditing: () => void;
+ onCancelEditing: () => void;
+}
+
+class UserForm extends React.Component {
+
+ formRef: RefObject = React.createRef();
+
+ componentDidMount() {
+ ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
+ }
+
+ submit = () => {
+ this.formRef.current.submit();
+ }
+
+ render() {
+ const { user, creating, handleValueChange, handleCheckboxChange, onDoneEditing, onCancelEditing } = this.props;
+ return (
+
+
+
+ );
+ }
+}
+
+export default UserForm;
diff --git a/interface/src/security/types.ts b/interface/src/security/types.ts
new file mode 100644
index 0000000..99d54de
--- /dev/null
+++ b/interface/src/security/types.ts
@@ -0,0 +1,11 @@
+export interface User {
+ username: string;
+ password: string;
+ admin: boolean;
+}
+
+export interface SecuritySettings {
+ users: User[];
+ jwt_secret: string;
+}
+
diff --git a/interface/src/serviceWorker.ts b/interface/src/serviceWorker.ts
new file mode 100644
index 0000000..d5f0275
--- /dev/null
+++ b/interface/src/serviceWorker.ts
@@ -0,0 +1,145 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://bit.ly/CRA-PWA
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.0/8 are considered localhost for IPv4.
+ window.location.hostname.match(
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+ )
+);
+
+type Config = {
+ onSuccess?: (registration: ServiceWorkerRegistration) => void;
+ onUpdate?: (registration: ServiceWorkerRegistration) => void;
+};
+
+export function register(config?: Config) {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(
+ process.env.PUBLIC_URL,
+ window.location.href
+ );
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://bit.ly/CRA-PWA'
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl: string, config?: Config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then(registration => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ 'New content is available and will be used when all ' +
+ 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
+ );
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch(error => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl: string, config?: Config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { 'Service-Worker': 'script' }
+ })
+ .then(response => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get('content-type');
+ if (
+ response.status === 404 ||
+ (contentType != null && contentType.indexOf('javascript') === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log(
+ 'No internet connection found. App is running in offline mode.'
+ );
+ });
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister();
+ });
+ }
+}
diff --git a/interface/src/system/OTASettingsController.tsx b/interface/src/system/OTASettingsController.tsx
new file mode 100644
index 0000000..2f683b3
--- /dev/null
+++ b/interface/src/system/OTASettingsController.tsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import { OTA_SETTINGS_ENDPOINT } from '../api';
+
+import OTASettingsForm from './OTASettingsForm';
+import { OTASettings } from './types';
+
+type OTASettingsControllerProps = RestControllerProps;
+
+class OTASettingsController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+}
+
+export default restController(OTA_SETTINGS_ENDPOINT, OTASettingsController);
diff --git a/interface/src/system/OTASettingsForm.tsx b/interface/src/system/OTASettingsForm.tsx
new file mode 100644
index 0000000..22947e6
--- /dev/null
+++ b/interface/src/system/OTASettingsForm.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
+
+import { Checkbox } from '@material-ui/core';
+import SaveIcon from '@material-ui/icons/Save';
+
+import { RestFormProps, BlockFormControlLabel, PasswordValidator, FormButton, FormActions } from '../components';
+import {isIP,isHostname,or} from '../validators';
+
+import { OTASettings } from './types';
+
+type OTASettingsFormProps = RestFormProps;
+
+class OTASettingsForm extends React.Component {
+
+ componentDidMount() {
+ ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
+ }
+
+ render() {
+ const { data, handleValueChange, handleCheckboxChange, saveData, loadData } = this.props;
+ return (
+
+
+ }
+ label="Enable OTA Updates?"
+ />
+
+
+
+ } variant="contained" color="primary" type="submit">
+ Save
+
+
+ Reset
+
+
+
+ );
+ }
+}
+
+export default OTASettingsForm;
diff --git a/interface/src/system/System.tsx b/interface/src/system/System.tsx
new file mode 100644
index 0000000..c425e9b
--- /dev/null
+++ b/interface/src/system/System.tsx
@@ -0,0 +1,38 @@
+import React, { Component } from 'react';
+import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
+
+import { Tabs, Tab } from '@material-ui/core';
+
+import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
+import { MenuAppBar } from '../components';
+
+import SystemStatusController from './SystemStatusController';
+import OTASettingsController from './OTASettingsController';
+
+type SystemProps = AuthenticatedContextProps & RouteComponentProps;
+
+class System extends Component {
+
+ handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
+ this.props.history.push(path);
+ };
+
+ render() {
+ const { authenticatedContext } = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default withAuthenticatedContext(System);
diff --git a/interface/src/system/SystemStatusController.tsx b/interface/src/system/SystemStatusController.tsx
new file mode 100644
index 0000000..abee0b5
--- /dev/null
+++ b/interface/src/system/SystemStatusController.tsx
@@ -0,0 +1,30 @@
+import React, { Component } from 'react';
+
+import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import { SYSTEM_STATUS_ENDPOINT } from '../api';
+
+import SystemStatusForm from './SystemStatusForm';
+import { SystemStatus } from './types';
+
+type SystemStatusControllerProps = RestControllerProps;
+
+class SystemStatusController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+}
+
+export default restController(SYSTEM_STATUS_ENDPOINT, SystemStatusController);
diff --git a/interface/src/containers/SystemStatus.js b/interface/src/system/SystemStatusForm.tsx
similarity index 61%
rename from interface/src/containers/SystemStatus.js
rename to interface/src/system/SystemStatusForm.tsx
index b502cfd..25d2744 100644
--- a/interface/src/containers/SystemStatus.js
+++ b/interface/src/system/SystemStatusForm.tsx
@@ -1,18 +1,7 @@
import React, { Component, Fragment } from 'react';
-import { withSnackbar } from 'notistack';
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-import ListItemText from '@material-ui/core/ListItemText';
-import Avatar from '@material-ui/core/Avatar';
-import Divider from '@material-ui/core/Divider';
-import Dialog from '@material-ui/core/Dialog';
-import DialogActions from '@material-ui/core/DialogActions';
-import DialogTitle from '@material-ui/core/DialogTitle';
-import DialogContent from '@material-ui/core/DialogContent';
+import { Avatar, Button, Divider, Dialog, DialogTitle, DialogContent, DialogActions } from '@material-ui/core';
+import { List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import DevicesIcon from '@material-ui/icons/Devices';
import MemoryIcon from '@material-ui/icons/Memory';
@@ -22,36 +11,28 @@ import DataUsageIcon from '@material-ui/icons/DataUsage';
import AutorenewIcon from '@material-ui/icons/Autorenew';
import RefreshIcon from '@material-ui/icons/Refresh';
-import { SYSTEM_STATUS_ENDPOINT, RESTART_ENDPOINT } from '../constants/Endpoints';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
-import SectionContent from '../components/SectionContent';
-import { redirectingAuthorizedFetch } from '../authentication/Authentication';
+import { redirectingAuthorizedFetch } from '../authentication';
+import { RestFormProps, FormButton, FormActions } from '../components';
+import { RESTART_ENDPOINT } from '../api';
-const styles = theme => ({
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
+import { SystemStatus } from './types';
-class SystemStatus extends Component {
+interface SystemStatusFormState {
+ confirmRestart: boolean;
+ processing: boolean;
+}
+type SystemStatusFormProps = RestFormProps;
- constructor(props) {
- super(props);
+class SystemStatusForm extends Component {
- this.state = {
- confirmRestart: false,
- processing: false
- }
+ state: SystemStatusFormState = {
+ confirmRestart: false,
+ processing: false
}
- componentDidMount() {
- this.props.loadData();
- }
-
- createListItems(data, classes) {
+ createListItems() {
+ const { data } = this.props
return (
@@ -103,20 +84,26 @@ class SystemStatus extends Component {
);
}
- renderSystemStatus(data, classes) {
+ renderRestartDialog() {
return (
-
-
- {this.createListItems(data, classes)}
-
- } variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
- Refresh
-
- } variant="contained" color="secondary" className={classes.button} onClick={this.onRestart}>
- Restart
-
-
- );
+
+ )
}
onRestart = () => {
@@ -144,45 +131,25 @@ class SystemStatus extends Component {
});
}
- renderRestartDialog() {
- return (
-
- )
- }
-
render() {
- const { data, fetched, errorMessage, loadData, classes } = this.props;
return (
-
- this.renderSystemStatus(data, classes)
- }
- />
+
+
+ {this.createListItems()}
+
+
+ } variant="contained" color="secondary" onClick={this.props.loadData}>
+ Refresh
+
+ } variant="contained" color="primary" onClick={this.onRestart}>
+ Restart
+
+
{this.renderRestartDialog()}
-
- )
+
+ );
}
}
-export default withSnackbar(restComponent(SYSTEM_STATUS_ENDPOINT, withStyles(styles)(SystemStatus)));
+export default SystemStatusForm;
diff --git a/interface/src/system/types.ts b/interface/src/system/types.ts
new file mode 100644
index 0000000..7159839
--- /dev/null
+++ b/interface/src/system/types.ts
@@ -0,0 +1,14 @@
+export interface SystemStatus {
+ esp_platform: string;
+ cpu_freq_mhz: number;
+ free_heap: number;
+ sketch_size: number;
+ free_sketch_space: number;
+ flash_chip_size: number;
+}
+
+export interface OTASettings {
+ enabled: boolean;
+ port: number;
+ password: string;
+}
diff --git a/interface/src/validators/index.ts b/interface/src/validators/index.ts
new file mode 100644
index 0000000..82aafa8
--- /dev/null
+++ b/interface/src/validators/index.ts
@@ -0,0 +1,4 @@
+export { default as isHostname } from './isHostname';
+export { default as isIP } from './isIP';
+export { default as optional } from './optional';
+export { default as or } from './or';
diff --git a/interface/src/validators/isHostname.js b/interface/src/validators/isHostname.ts
similarity index 82%
rename from interface/src/validators/isHostname.js
rename to interface/src/validators/isHostname.ts
index 557903e..7c1ca25 100644
--- a/interface/src/validators/isHostname.js
+++ b/interface/src/validators/isHostname.ts
@@ -1,6 +1,6 @@
const hostnameLengthRegex = /^.{0,32}$/
const hostnamePatternRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/
-export default function isHostname(hostname) {
+export default function isHostname(hostname: string) {
return hostnameLengthRegex.test(hostname) && hostnamePatternRegex.test(hostname);
}
diff --git a/interface/src/validators/isIP.js b/interface/src/validators/isIP.ts
similarity index 81%
rename from interface/src/validators/isIP.js
rename to interface/src/validators/isIP.ts
index 1225154..439c57c 100644
--- a/interface/src/validators/isIP.js
+++ b/interface/src/validators/isIP.ts
@@ -1,5 +1,5 @@
const ipAddressRegexp = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
-export default function isIp(ipAddress) {
+export default function isIp(ipAddress: string) {
return ipAddressRegexp.test(ipAddress);
}
\ No newline at end of file
diff --git a/interface/src/validators/optional.js b/interface/src/validators/optional.js
deleted file mode 100644
index 583bc45..0000000
--- a/interface/src/validators/optional.js
+++ /dev/null
@@ -1 +0,0 @@
-export default validator => value => !value || validator(value);
diff --git a/interface/src/validators/optional.ts b/interface/src/validators/optional.ts
new file mode 100644
index 0000000..841c57b
--- /dev/null
+++ b/interface/src/validators/optional.ts
@@ -0,0 +1 @@
+export default (validator: (value: any) => boolean) => (value: any) => !value || validator(value);
diff --git a/interface/src/validators/or.js b/interface/src/validators/or.js
deleted file mode 100644
index 96543f7..0000000
--- a/interface/src/validators/or.js
+++ /dev/null
@@ -1 +0,0 @@
-export default (validator1, validator2) => value => validator1(value) || validator2(value);
diff --git a/interface/src/validators/or.ts b/interface/src/validators/or.ts
new file mode 100644
index 0000000..047ecf0
--- /dev/null
+++ b/interface/src/validators/or.ts
@@ -0,0 +1,3 @@
+export default (validator1: (value: any) => boolean, validator2: (value: any) => boolean) => {
+ return (value: any) => validator1(value) || validator2(value);
+}
diff --git a/interface/src/wifi/WiFiConnection.tsx b/interface/src/wifi/WiFiConnection.tsx
new file mode 100644
index 0000000..506aefa
--- /dev/null
+++ b/interface/src/wifi/WiFiConnection.tsx
@@ -0,0 +1,62 @@
+import React, { Component } from 'react';
+import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
+
+import { Tabs, Tab } from '@material-ui/core';
+
+import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
+import { MenuAppBar } from '../components';
+
+import WiFiStatusController from './WiFiStatusController';
+import WiFiSettingsController from './WiFiSettingsController';
+import WiFiNetworkScanner from './WiFiNetworkScanner';
+import { WiFiConnectionContext } from './WiFiConnectionContext';
+import { WiFiNetwork } from './types';
+
+type WiFiConnectionProps = AuthenticatedContextProps & RouteComponentProps;
+
+class WiFiConnection extends Component {
+
+ constructor(props: WiFiConnectionProps) {
+ super(props);
+ this.state = {
+ selectNetwork: this.selectNetwork,
+ deselectNetwork: this.deselectNetwork
+ };
+ }
+
+ selectNetwork = (network: WiFiNetwork) => {
+ this.setState({ selectedNetwork: network });
+ this.props.history.push('/wifi/settings');
+ }
+
+ deselectNetwork = () => {
+ this.setState({ selectedNetwork: undefined });
+ }
+
+ handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
+ this.props.history.push(path);
+ };
+
+ render() {
+ const { authenticatedContext } = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default withAuthenticatedContext(WiFiConnection);
diff --git a/interface/src/wifi/WiFiConnectionContext.tsx b/interface/src/wifi/WiFiConnectionContext.tsx
new file mode 100644
index 0000000..85b0c17
--- /dev/null
+++ b/interface/src/wifi/WiFiConnectionContext.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { WiFiNetwork } from './types';
+
+export interface WiFiConnectionContext {
+ selectedNetwork?: WiFiNetwork;
+ selectNetwork: (network: WiFiNetwork) => void;
+ deselectNetwork: () => void;
+}
+
+const WiFiConnectionContextDefaultValue = {} as WiFiConnectionContext
+export const WiFiConnectionContext = React.createContext(
+ WiFiConnectionContextDefaultValue
+);
diff --git a/interface/src/wifi/WiFiNetworkScanner.tsx b/interface/src/wifi/WiFiNetworkScanner.tsx
new file mode 100644
index 0000000..c1be649
--- /dev/null
+++ b/interface/src/wifi/WiFiNetworkScanner.tsx
@@ -0,0 +1,168 @@
+import React, { Component } from 'react';
+import { withSnackbar, WithSnackbarProps } from 'notistack';
+
+import { createStyles, WithStyles, Theme, withStyles, Typography, LinearProgress } from '@material-ui/core';
+import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
+
+import { FormActions, FormButton, SectionContent } from '../components';
+import { redirectingAuthorizedFetch } from '../authentication';
+import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
+
+import WiFiNetworkSelector from './WiFiNetworkSelector';
+import { WiFiNetworkList, WiFiNetwork } from './types';
+
+const NUM_POLLS = 10
+const POLLING_FREQUENCY = 500
+const RETRY_EXCEPTION_TYPE = "retry"
+
+interface WiFiNetworkScannerState {
+ scanningForNetworks: boolean;
+ errorMessage?: string;
+ networkList?: WiFiNetworkList;
+}
+
+const styles = (theme: Theme) => createStyles({
+ scanningSettings: {
+ margin: theme.spacing(0.5),
+ },
+ scanningSettingsDetails: {
+ margin: theme.spacing(4),
+ textAlign: "center"
+ },
+ scanningProgress: {
+ margin: theme.spacing(4),
+ textAlign: "center"
+ }
+});
+
+type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles;
+
+class WiFiNetworkScanner extends Component {
+
+ pollCount: number = 0;
+
+ state: WiFiNetworkScannerState = {
+ scanningForNetworks: false,
+ };
+
+ componentDidMount() {
+ this.scanNetworks();
+ }
+
+ requestNetworkScan = () => {
+ const { scanningForNetworks } = this.state;
+ if (!scanningForNetworks) {
+ this.scanNetworks();
+ }
+ }
+
+ scanNetworks() {
+ this.pollCount = 0;
+ this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
+ redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
+ if (response.status === 202) {
+ this.schedulePollTimeout();
+ return;
+ }
+ throw Error("Scanning for networks returned unexpected response code: " + response.status);
+ }).catch(error => {
+ this.props.enqueueSnackbar("Problem scanning: " + error.message, {
+ variant: 'error',
+ });
+ this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
+ });
+ }
+
+ schedulePollTimeout() {
+ setTimeout(this.pollNetworkList, POLLING_FREQUENCY);
+ }
+
+ retryError() {
+ return {
+ name: RETRY_EXCEPTION_TYPE,
+ message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
+ };
+ }
+
+ compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
+ if (network1.rssi < network2.rssi)
+ return 1;
+ if (network1.rssi > network2.rssi)
+ return -1;
+ return 0;
+ }
+
+ pollNetworkList = () => {
+ redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
+ .then(response => {
+ if (response.status === 200) {
+ return response.json();
+ }
+ if (response.status === 202) {
+ if (++this.pollCount < NUM_POLLS) {
+ this.schedulePollTimeout();
+ throw this.retryError();
+ } else {
+ throw Error("Device did not return network list in timely manner.");
+ }
+ }
+ throw Error("Device returned unexpected response code: " + response.status);
+ })
+ .then(json => {
+ json.networks.sort(this.compareNetworks)
+ this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
+ })
+ .catch(error => {
+ if (error.name !== RETRY_EXCEPTION_TYPE) {
+ this.props.enqueueSnackbar("Problem scanning: " + error.message, {
+ variant: 'error',
+ });
+ this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
+ }
+ });
+ }
+
+ renderNetworkScanner() {
+ const { classes } = this.props;
+ const { scanningForNetworks, networkList, errorMessage } = this.state;
+ if (scanningForNetworks || !networkList) {
+ return (
+
+
+
+ Scanning...
+
+
+ );
+ }
+ if (errorMessage) {
+ return (
+
+
+ {errorMessage}
+
+
+ );
+ }
+ return (
+
+ );
+ }
+
+ render() {
+ const { scanningForNetworks } = this.state;
+ return (
+
+ {this.renderNetworkScanner()}
+
+ } variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}>
+ Scan again...
+
+
+
+ );
+ }
+
+}
+
+export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
diff --git a/interface/src/wifi/WiFiNetworkSelector.tsx b/interface/src/wifi/WiFiNetworkSelector.tsx
new file mode 100644
index 0000000..2043651
--- /dev/null
+++ b/interface/src/wifi/WiFiNetworkSelector.tsx
@@ -0,0 +1,54 @@
+import React, { Component } from 'react';
+
+import { Avatar, Badge } from '@material-ui/core';
+import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
+
+import WifiIcon from '@material-ui/icons/Wifi';
+import LockIcon from '@material-ui/icons/Lock';
+import LockOpenIcon from '@material-ui/icons/LockOpen';
+
+import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
+import { WiFiConnectionContext } from './WiFiConnectionContext';
+import { WiFiNetwork, WiFiNetworkList } from './types';
+
+interface WiFiNetworkSelectorProps {
+ networkList: WiFiNetworkList;
+}
+
+class WiFiNetworkSelector extends Component {
+
+ static contextType = WiFiConnectionContext;
+ context!: React.ContextType;
+
+ renderNetwork = (network: WiFiNetwork) => {
+ return (
+ this.context.selectNetwork(network)}>
+
+
+ {isNetworkOpen(network) ? : }
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ return (
+
+ {this.props.networkList.networks.map(this.renderNetwork)}
+
+ );
+ }
+
+}
+
+export default WiFiNetworkSelector;
diff --git a/interface/src/constants/WiFiSecurityModes.js b/interface/src/wifi/WiFiSecurityModes.ts
similarity index 65%
rename from interface/src/constants/WiFiSecurityModes.js
rename to interface/src/wifi/WiFiSecurityModes.ts
index db33d4c..06c7bdb 100644
--- a/interface/src/constants/WiFiSecurityModes.js
+++ b/interface/src/wifi/WiFiSecurityModes.ts
@@ -1,3 +1,5 @@
+import { WiFiNetwork } from "./types";
+
export const WIFI_AUTH_OPEN = 0;
export const WIFI_AUTH_WEP = 1;
export const WIFI_AUTH_WEP_PSK = 2;
@@ -5,10 +7,10 @@ export const WIFI_AUTH_WEP2_PSK = 3;
export const WIFI_AUTH_WPA_WPA2_PSK = 4;
export const WIFI_AUTH_WPA2_ENTERPRISE = 5;
-export const isNetworkOpen = selectedNetwork => selectedNetwork && selectedNetwork.encryption_type === WIFI_AUTH_OPEN;
+export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) => encryption_type === WIFI_AUTH_OPEN;
-export const networkSecurityMode = selectedNetwork => {
- switch (selectedNetwork.encryption_type){
+export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
+ switch (encryption_type) {
case WIFI_AUTH_WEP:
case WIFI_AUTH_WEP_PSK:
return "WEP";
@@ -17,7 +19,7 @@ export const networkSecurityMode = selectedNetwork => {
case WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WEP2";
case WIFI_AUTH_WPA2_ENTERPRISE:
- return "WEP2 Enterprise";
+ return "WEP2 Enterprise";
case WIFI_AUTH_OPEN:
return "None";
default:
diff --git a/interface/src/wifi/WiFiSettingsController.tsx b/interface/src/wifi/WiFiSettingsController.tsx
new file mode 100644
index 0000000..b5ad0f8
--- /dev/null
+++ b/interface/src/wifi/WiFiSettingsController.tsx
@@ -0,0 +1,54 @@
+import React, { Component } from 'react';
+
+import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import WiFiSettingsForm from './WiFiSettingsForm';
+import { WiFiConnectionContext } from './WiFiConnectionContext';
+import { WIFI_SETTINGS_ENDPOINT } from '../api';
+import { WiFiSettings } from './types';
+
+type WiFiSettingsControllerProps = RestControllerProps;
+
+class WiFiSettingsController extends Component {
+
+ static contextType = WiFiConnectionContext;
+ context!: React.ContextType;
+
+ componentDidMount() {
+ const { selectedNetwork } = this.context;
+ if (selectedNetwork) {
+ const wifiSettings: WiFiSettings = {
+ ssid: selectedNetwork.ssid,
+ password: "",
+ hostname: "esp8266-react",
+ static_ip_config: false,
+ }
+ this.props.setData(wifiSettings);
+ } else {
+ this.props.loadData();
+ }
+ }
+
+ deselectNetworkAndLoadData = () => {
+ this.context.deselectNetwork();
+ this.props.loadData();
+ }
+
+ componentWillUnmount() {
+ this.context.deselectNetwork();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+}
+
+export default restController(WIFI_SETTINGS_ENDPOINT, WiFiSettingsController);
diff --git a/interface/src/wifi/WiFiSettingsForm.tsx b/interface/src/wifi/WiFiSettingsForm.tsx
new file mode 100644
index 0000000..ddaea91
--- /dev/null
+++ b/interface/src/wifi/WiFiSettingsForm.tsx
@@ -0,0 +1,179 @@
+import React, { Fragment } from 'react';
+import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
+
+import { Checkbox, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction } from '@material-ui/core';
+
+import Avatar from '@material-ui/core/Avatar';
+import IconButton from '@material-ui/core/IconButton';
+import LockIcon from '@material-ui/icons/Lock';
+import LockOpenIcon from '@material-ui/icons/LockOpen';
+import DeleteIcon from '@material-ui/icons/Delete';
+import SaveIcon from '@material-ui/icons/Save';
+
+import { RestFormProps, PasswordValidator, BlockFormControlLabel, FormActions, FormButton } from '../components';
+import { isIP, isHostname, optional } from '../validators';
+
+import { WiFiConnectionContext } from './WiFiConnectionContext';
+import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
+import { WiFiSettings } from './types';
+
+type WiFiStatusFormProps = RestFormProps;
+
+class WiFiSettingsForm extends React.Component {
+
+ static contextType = WiFiConnectionContext;
+ context!: React.ContextType;
+
+ componentWillMount() {
+ ValidatorForm.addValidationRule('isIP', isIP);
+ ValidatorForm.addValidationRule('isHostname', isHostname);
+ ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
+ }
+
+ render() {
+ const { selectedNetwork, deselectNetwork } = this.context;
+ const { data, handleValueChange, handleCheckboxChange, saveData, loadData } = this.props;
+ return (
+
+ {
+ selectedNetwork ?
+
+
+
+
+ {isNetworkOpen(selectedNetwork) ? : }
+
+
+
+
+
+
+
+
+
+
+ :
+
+ }
+ {
+ (!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
+
+ }
+
+
+ }
+ label="Static IP Config?"
+ />
+ {
+ data.static_ip_config &&
+
+
+
+
+
+
+
+ }
+
+ } variant="contained" color="primary" type="submit">
+ Save
+
+
+ Reset
+
+
+
+ );
+ }
+}
+
+export default WiFiSettingsForm;
diff --git a/interface/src/constants/WiFiConnectionStatus.js b/interface/src/wifi/WiFiStatus.ts
similarity index 63%
rename from interface/src/constants/WiFiConnectionStatus.js
rename to interface/src/wifi/WiFiStatus.ts
index b7ba819..bbab81f 100644
--- a/interface/src/constants/WiFiConnectionStatus.js
+++ b/interface/src/wifi/WiFiStatus.ts
@@ -1,4 +1,5 @@
-import * as Highlight from '../constants/Highlight';
+import { Theme } from '@material-ui/core';
+import { WiFiStatus } from './types';
export const WIFI_STATUS_IDLE = 0;
export const WIFI_STATUS_NO_SSID_AVAIL = 1;
@@ -7,25 +8,25 @@ export const WIFI_STATUS_CONNECT_FAILED = 4;
export const WIFI_STATUS_CONNECTION_LOST = 5;
export const WIFI_STATUS_DISCONNECTED = 6;
-export const isConnected = wifiStatus => wifiStatus && wifiStatus.status === WIFI_STATUS_CONNECTED;
+export const isConnected = ({ status }: WiFiStatus) => status === WIFI_STATUS_CONNECTED;
-export const connectionStatusHighlight = wifiStatus => {
- switch (wifiStatus.status){
+export const wifiStatusHighlight = ({ status }: WiFiStatus, theme: Theme) => {
+ switch (status) {
case WIFI_STATUS_IDLE:
case WIFI_STATUS_DISCONNECTED:
- return Highlight.IDLE;
+ return theme.palette.info.main;
case WIFI_STATUS_CONNECTED:
- return Highlight.SUCCESS;
+ return theme.palette.success.main;
case WIFI_STATUS_CONNECT_FAILED:
case WIFI_STATUS_CONNECTION_LOST:
- return Highlight.ERROR;
+ return theme.palette.error.main;
default:
- return Highlight.WARN;
+ return theme.palette.warning.main;
}
}
-export const connectionStatus = wifiStatus => {
- switch (wifiStatus.status){
+export const wifiStatus = ({ status }: WiFiStatus) => {
+ switch (status) {
case WIFI_STATUS_IDLE:
return "Idle";
case WIFI_STATUS_NO_SSID_AVAIL:
diff --git a/interface/src/wifi/WiFiStatusController.tsx b/interface/src/wifi/WiFiStatusController.tsx
new file mode 100644
index 0000000..2220595
--- /dev/null
+++ b/interface/src/wifi/WiFiStatusController.tsx
@@ -0,0 +1,29 @@
+import React, { Component } from 'react';
+
+import {restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
+import WiFiStatusForm from './WiFiStatusForm';
+import { WIFI_STATUS_ENDPOINT } from '../api';
+import { WiFiStatus } from './types';
+
+type WiFiStatusControllerProps = RestControllerProps;
+
+class WiFiStatusController extends Component {
+
+ componentDidMount() {
+ this.props.loadData();
+ }
+
+ render() {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+}
+
+export default restController(WIFI_STATUS_ENDPOINT, WiFiStatusController);
diff --git a/interface/src/containers/WiFiStatus.js b/interface/src/wifi/WiFiStatusForm.tsx
similarity index 54%
rename from interface/src/containers/WiFiStatus.js
rename to interface/src/wifi/WiFiStatusForm.tsx
index 419105c..5b60dc6 100644
--- a/interface/src/containers/WiFiStatus.js
+++ b/interface/src/wifi/WiFiStatusForm.tsx
@@ -1,69 +1,41 @@
import React, { Component, Fragment } from 'react';
-import { withStyles } from '@material-ui/core/styles';
-import Button from '@material-ui/core/Button';
-import List from '@material-ui/core/List';
-import ListItem from '@material-ui/core/ListItem';
-import ListItemText from '@material-ui/core/ListItemText';
-import ListItemAvatar from '@material-ui/core/ListItemAvatar';
-import Avatar from '@material-ui/core/Avatar';
-import Divider from '@material-ui/core/Divider';
-import WifiIcon from '@material-ui/icons/Wifi';
+import { WithTheme, withTheme } from '@material-ui/core/styles';
+import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
+
import DNSIcon from '@material-ui/icons/Dns';
+import WifiIcon from '@material-ui/icons/Wifi';
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import RefreshIcon from '@material-ui/icons/Refresh';
-import SectionContent from '../components/SectionContent';
-import { WIFI_STATUS_ENDPOINT } from '../constants/Endpoints';
-import { isConnected, connectionStatus, connectionStatusHighlight } from '../constants/WiFiConnectionStatus';
-import * as Highlight from '../constants/Highlight';
-import { restComponent } from '../components/RestComponent';
-import LoadingNotification from '../components/LoadingNotification';
+import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
+import { wifiStatus, wifiStatusHighlight, isConnected } from './WiFiStatus';
+import { WiFiStatus } from './types';
-const styles = theme => ({
- ["wifiStatus_" + Highlight.IDLE]: {
- backgroundColor: theme.palette.highlight_idle
- },
- ["wifiStatus_" + Highlight.SUCCESS]: {
- backgroundColor: theme.palette.highlight_success
- },
- ["wifiStatus_" + Highlight.ERROR]: {
- backgroundColor: theme.palette.highlight_error
- },
- ["wifiStatus_" + Highlight.WARN]: {
- backgroundColor: theme.palette.highlight_warn
- },
- button: {
- marginRight: theme.spacing(2),
- marginTop: theme.spacing(2),
- }
-});
+type WiFiStatusFormProps = RestFormProps & WithTheme;
-class WiFiStatus extends Component {
+class WiFiStatusForm extends Component {
- componentDidMount() {
- this.props.loadData();
- }
-
- dnsServers(status) {
+ dnsServers(status: WiFiStatus) {
if (!status.dns_ip_1) {
return "none";
}
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
}
- createListItems(data, classes) {
+ createListItems() {
+ const { data, theme } = this.props
return (
-
+
-
+
-
+
{
@@ -125,35 +97,21 @@ class WiFiStatus extends Component {
);
}
- renderWiFiStatus(data, classes) {
- return (
-
-
- {this.createListItems(data, classes)}
-
- } variant="contained" color="secondary" className={classes.button} onClick={this.props.loadData}>
- Refresh
-
-
- );
- }
-
render() {
- const { data, fetched, errorMessage, loadData, classes } = this.props;
return (
-
- this.renderWiFiStatus(data, classes)
- }
- />
-
+
+
+ {this.createListItems()}
+
+
+ } variant="contained" color="secondary" onClick={this.props.loadData}>
+ Refresh
+
+
+
);
}
}
-export default restComponent(WIFI_STATUS_ENDPOINT, withStyles(styles)(WiFiStatus));
+export default withTheme(WiFiStatusForm);
diff --git a/interface/src/wifi/types.ts b/interface/src/wifi/types.ts
new file mode 100644
index 0000000..2e5ec9f
--- /dev/null
+++ b/interface/src/wifi/types.ts
@@ -0,0 +1,37 @@
+export interface WiFiStatus {
+ status: number;
+ local_ip: string;
+ mac_address: string;
+ rssi: number;
+ ssid: string;
+ bssid: string;
+ channel: number;
+ subnet_mask: string;
+ gateway_ip: string;
+ dns_ip_1: string;
+ dns_ip_2: string;
+}
+
+export interface WiFiSettings {
+ ssid: string;
+ password: string;
+ hostname: string;
+ static_ip_config: boolean;
+ local_ip?: string;
+ gateway_ip?: string;
+ subnet_mask?: string;
+ dns_ip_1?: string;
+ dns_ip_2?: string;
+}
+
+export interface WiFiNetworkList {
+ networks: WiFiNetwork[];
+}
+
+export interface WiFiNetwork {
+ rssi: number;
+ ssid: string;
+ bssid: string;
+ channel: number;
+ encryption_type: number;
+}
diff --git a/interface/tsconfig.json b/interface/tsconfig.json
new file mode 100644
index 0000000..f2850b7
--- /dev/null
+++ b/interface/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react"
+ },
+ "include": [
+ "src"
+ ]
+}
diff --git a/media/dark.png b/media/dark.png
new file mode 100644
index 0000000..93d2132
Binary files /dev/null and b/media/dark.png differ
diff --git a/media/screenshots.png b/media/screenshots.png
index 27a504e..9b6d17c 100644
Binary files a/media/screenshots.png and b/media/screenshots.png differ