This commit is contained in:
proddy
2021-05-14 12:45:57 +02:00
parent 15df0c0552
commit fec5ff3132
108 changed files with 3508 additions and 2455 deletions

View File

@@ -1,42 +1,42 @@
const ManifestPlugin = require('webpack-manifest-plugin')
const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const ProgmemGenerator = require('./progmem-generator.js')
const ManifestPlugin = require('webpack-manifest-plugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const ProgmemGenerator = require('./progmem-generator.js');
module.exports = function override(config, env) {
const hosted = process.env.REACT_APP_HOSTED
const hosted = process.env.REACT_APP_HOSTED;
if (env === 'production' && !hosted) {
console.log('Custom webpack...')
console.log('Custom webpack...');
// rename the output file, we need it's path to be short for LittleFS
config.output.filename = 'js/[id].[chunkhash:4].js'
config.output.chunkFilename = 'js/[id].[chunkhash:4].js'
config.output.filename = 'js/[id].[chunkhash:4].js';
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
// take out the manifest and service worker plugins
config.plugins = config.plugins.filter(
(plugin) => !(plugin instanceof ManifestPlugin),
)
(plugin) => !(plugin instanceof ManifestPlugin)
);
config.plugins = config.plugins.filter(
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW),
)
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW)
);
// shorten css filenames
const miniCssExtractPlugin = config.plugins.find(
(plugin) => plugin instanceof MiniCssExtractPlugin,
)
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css'
(plugin) => plugin instanceof MiniCssExtractPlugin
);
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
miniCssExtractPlugin.options.chunkFilename =
'css/[id].[contenthash:4].c.css'
'css/[id].[contenthash:4].c.css';
// build progmem data files
config.plugins.push(
new ProgmemGenerator({
outputPath: '../lib/framework/WWWData.h',
bytesPerLine: 20,
}),
)
bytesPerLine: 20
})
);
// add compression plugin, compress javascript
config.plugins.push(
@@ -44,9 +44,9 @@ module.exports = function override(config, env) {
filename: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js)$/,
deleteOriginalAssets: true,
}),
)
deleteOriginalAssets: true
})
);
}
return config
}
return config;
};

View File

@@ -1,37 +1,37 @@
const { resolve, relative, sep } = require('path')
const { resolve, relative, sep } = require('path');
const {
readdirSync,
existsSync,
unlinkSync,
readFileSync,
createWriteStream,
} = require('fs')
var zlib = require('zlib')
var mime = require('mime-types')
createWriteStream
} = require('fs');
var zlib = require('zlib');
var mime = require('mime-types');
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n'
const ARDUINO_INCLUDES = '#include <Arduino.h>\n\n';
function getFilesSync(dir, files = []) {
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
const entryPath = resolve(dir, entry.name)
const entryPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
getFilesSync(entryPath, files)
getFilesSync(entryPath, files);
} else {
files.push(entryPath)
files.push(entryPath);
}
})
return files
});
return files;
}
function coherseToBuffer(input) {
return Buffer.isBuffer(input) ? input : Buffer.from(input)
return Buffer.isBuffer(input) ? input : Buffer.from(input);
}
function cleanAndOpen(path) {
if (existsSync(path)) {
unlinkSync(path)
unlinkSync(path);
}
return createWriteStream(path, { flags: 'w+' })
return createWriteStream(path, { flags: 'w+' });
}
class ProgmemGenerator {
@@ -40,70 +40,70 @@ class ProgmemGenerator {
outputPath,
bytesPerLine = 20,
indent = ' ',
includes = ARDUINO_INCLUDES,
} = options
this.options = { outputPath, bytesPerLine, indent, includes }
includes = ARDUINO_INCLUDES
} = options;
this.options = { outputPath, bytesPerLine, indent, includes };
}
apply(compiler) {
compiler.hooks.emit.tapAsync(
{ name: 'ProgmemGenerator' },
(compilation, callback) => {
const { outputPath, bytesPerLine, indent, includes } = this.options
const fileInfo = []
const { outputPath, bytesPerLine, indent, includes } = this.options;
const fileInfo = [];
const writeStream = cleanAndOpen(
resolve(compilation.options.context, outputPath),
)
resolve(compilation.options.context, outputPath)
);
try {
const writeIncludes = () => {
writeStream.write(includes)
}
writeStream.write(includes);
};
const writeFile = (relativeFilePath, buffer) => {
const variable = 'ESP_REACT_DATA_' + fileInfo.length
const mimeType = mime.lookup(relativeFilePath)
var size = 0
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {')
const zipBuffer = zlib.gzipSync(buffer)
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
const mimeType = mime.lookup(relativeFilePath);
var size = 0;
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
const zipBuffer = zlib.gzipSync(buffer);
zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) {
writeStream.write('\n')
writeStream.write(indent)
writeStream.write('\n');
writeStream.write(indent);
}
writeStream.write(
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ',',
)
size++
})
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ','
);
size++;
});
if (size % bytesPerLine) {
writeStream.write('\n')
writeStream.write('\n');
}
writeStream.write('};\n\n')
writeStream.write('};\n\n');
fileInfo.push({
uri: '/' + relativeFilePath.replace(sep, '/'),
mimeType,
variable,
size,
})
}
size
});
};
const writeFiles = () => {
// process static files
const buildPath = compilation.options.output.path
const buildPath = compilation.options.output.path;
for (const filePath of getFilesSync(buildPath)) {
const readStream = readFileSync(filePath)
const relativeFilePath = relative(buildPath, filePath)
writeFile(relativeFilePath, readStream)
const readStream = readFileSync(filePath);
const relativeFilePath = relative(buildPath, filePath);
writeFile(relativeFilePath, readStream);
}
// process assets
const { assets } = compilation
const { assets } = compilation;
Object.keys(assets).forEach((relativeFilePath) => {
writeFile(
relativeFilePath,
coherseToBuffer(assets[relativeFilePath].source()),
)
})
}
coherseToBuffer(assets[relativeFilePath].source())
);
});
};
const generateWWWClass = () => {
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
@@ -111,38 +111,38 @@ class ProgmemGenerator {
class WWWData {
${indent}public:
${indent.repeat(
2,
2
)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo
.map(
(file) =>
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${
file.variable
}, ${file.size});`,
}, ${file.size});`
)
.join('\n')}
${indent.repeat(2)}}
};
`
}
`;
};
const writeWWWClass = () => {
writeStream.write(generateWWWClass())
}
writeStream.write(generateWWWClass());
};
writeIncludes()
writeFiles()
writeWWWClass()
writeIncludes();
writeFiles();
writeWWWClass();
writeStream.on('finish', () => {
callback()
})
callback();
});
} finally {
writeStream.end()
writeStream.end();
}
},
)
}
);
}
}
module.exports = ProgmemGenerator
module.exports = ProgmemGenerator;

View File

@@ -14,7 +14,6 @@ import FeaturesWrapper from './features/FeaturesWrapper';
const unauthorizedRedirect = () => <Redirect to="/" />;
class App extends Component {
notistackRef: RefObject<any> = React.createRef();
componentDidMount() {
@@ -23,21 +22,29 @@ class App extends Component {
onClickDismiss = (key: string | number | undefined) => () => {
this.notistackRef.current.closeSnackbar(key);
}
};
render() {
return (
<CustomMuiTheme>
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
<SnackbarProvider
autoHideDuration={3000}
maxSnack={3}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
ref={this.notistackRef}
action={(key) => (
<IconButton onClick={this.onClickDismiss(key)} size="small">
<CloseIcon />
</IconButton>
)}>
)}
>
<FeaturesWrapper>
<Switch>
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
<Route
exact
path="/unauthorized"
component={unauthorizedRedirect}
/>
<Route component={AppRouting} />
</Switch>
</FeaturesWrapper>
@@ -47,4 +54,4 @@ class App extends Component {
}
}
export default App
export default App;

View File

@@ -19,9 +19,9 @@ import Mqtt from './mqtt/Mqtt';
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
import { Features } from './features/types';
export const getDefaultRoute = (features: Features) => features.project ? `/${PROJECT_PATH}/` : "/network/";
export const getDefaultRoute = (features: Features) =>
features.project ? `/${PROJECT_PATH}/` : '/network/';
class AppRouting extends Component<WithFeaturesProps> {
componentDidMount() {
Authentication.clearLoginRedirect();
}
@@ -35,9 +35,17 @@ class AppRouting extends Component<WithFeaturesProps> {
<UnauthenticatedRoute exact path="/" component={SignIn} />
)}
{features.project && (
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/*`} component={ProjectRouting} />
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/*`}
component={ProjectRouting}
/>
)}
<AuthenticatedRoute exact path="/network/*" component={NetworkConnection} />
<AuthenticatedRoute
exact
path="/network/*"
component={NetworkConnection}
/>
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
{features.ntp && (
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
@@ -52,7 +60,7 @@ class AppRouting extends Component<WithFeaturesProps> {
<Redirect to={getDefaultRoute(features)} />
</Switch>
</AuthenticationWrapper>
)
);
}
}

View File

@@ -1,17 +1,21 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { CssBaseline } from '@material-ui/core';
import { MuiThemeProvider, createMuiTheme, StylesProvider } from '@material-ui/core/styles';
import {
MuiThemeProvider,
createMuiTheme,
StylesProvider
} from '@material-ui/core/styles';
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
const theme = createMuiTheme({
palette: {
type: "dark",
type: 'dark',
primary: {
main: '#33bfff',
main: '#33bfff'
},
secondary: {
main: '#3d5afe',
main: '#3d5afe'
},
info: {
main: blueGrey[500]
@@ -29,7 +33,6 @@ const theme = createMuiTheme({
});
export default class CustomMuiTheme extends Component {
render() {
return (
<StylesProvider>
@@ -40,5 +43,4 @@ export default class CustomMuiTheme extends Component {
</StylesProvider>
);
}
}

View File

@@ -2,53 +2,63 @@ import React, { Component } from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { withStyles, createStyles, Theme, WithStyles } from '@material-ui/core/styles';
import {
withStyles,
createStyles,
Theme,
WithStyles
} from '@material-ui/core/styles';
import { Paper, Typography, Fab } from '@material-ui/core';
import ForwardIcon from '@material-ui/icons/Forward';
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
import {
withAuthenticationContext,
AuthenticationContextProps
} from './authentication/AuthenticationContext';
import { PasswordValidator } from './components';
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
const styles = (theme: Theme) => createStyles({
const styles = (theme: Theme) =>
createStyles({
signInPage: {
display: "flex",
height: "100vh",
margin: "auto",
display: 'flex',
height: '100vh',
margin: 'auto',
padding: theme.spacing(2),
justifyContent: "center",
flexDirection: "column",
justifyContent: 'center',
flexDirection: 'column',
maxWidth: theme.breakpoints.values.sm
},
signInPanel: {
textAlign: "center",
textAlign: 'center',
padding: theme.spacing(2),
paddingTop: "200px",
paddingTop: '200px',
backgroundImage: 'url("/app/icon.png")',
backgroundRepeat: "no-repeat",
backgroundPosition: "50% " + theme.spacing(2) + "px",
backgroundSize: "auto 150px",
width: "100%"
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% ' + theme.spacing(2) + 'px',
backgroundSize: 'auto 150px',
width: '100%'
},
extendedIcon: {
marginRight: theme.spacing(0.5),
marginRight: theme.spacing(0.5)
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
marginTop: theme.spacing(2)
}
});
});
type SignInProps = WithSnackbarProps & WithStyles<typeof styles> & AuthenticationContextProps;
type SignInProps = WithSnackbarProps &
WithStyles<typeof styles> &
AuthenticationContextProps;
interface SignInState {
username: string,
password: string,
processing: boolean
username: string;
password: string;
processing: boolean;
}
class SignIn extends Component<SignInProps, SignInState> {
constructor(props: SignInProps) {
super(props);
this.state = {
@@ -60,10 +70,10 @@ class SignIn extends Component<SignInProps, SignInState> {
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
const { name, value } = event.currentTarget;
this.setState(prevState => ({
this.setState((prevState) => ({
...prevState,
[name]: value,
}))
[name]: value
}));
};
onSubmit = () => {
@@ -77,20 +87,21 @@ class SignIn extends Component<SignInProps, SignInState> {
'Content-Type': 'application/json'
})
})
.then(response => {
.then((response) => {
if (response.status === 200) {
return response.json();
} else if (response.status === 401) {
throw Error("Invalid credentials.");
throw Error('Invalid credentials.');
} else {
throw Error("Invalid status code: " + response.status);
throw Error('Invalid status code: ' + response.status);
}
}).then(json => {
})
.then((json) => {
authenticationContext.signIn(json.access_token);
})
.catch(error => {
.catch((error) => {
this.props.enqueueSnackbar(error.message, {
variant: 'warning',
variant: 'warning'
});
this.setState({ processing: false });
});
@@ -116,8 +127,8 @@ class SignIn extends Component<SignInProps, SignInState> {
onChange={this.updateInputElement}
margin="normal"
inputProps={{
autoCapitalize: "none",
autoCorrect: "off",
autoCapitalize: 'none',
autoCorrect: 'off'
}}
/>
<PasswordValidator
@@ -132,7 +143,13 @@ class SignIn extends Component<SignInProps, SignInState> {
onChange={this.updateInputElement}
margin="normal"
/>
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
<Fab
variant="extended"
color="primary"
className={classes.button}
type="submit"
disabled={processing}
>
<ForwardIcon className={classes.extendedIcon} />
Sign In
</Fab>
@@ -141,7 +158,8 @@ class SignIn extends Component<SignInProps, SignInState> {
</div>
);
}
}
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
export default withAuthenticationContext(
withSnackbar(withStyles(styles)(SignIn))
);

View File

@@ -1,8 +1,8 @@
import { APSettings, APProvisionMode } from './types'
import { APSettings, APProvisionMode } from './types';
export const isAPEnabled = ({ provision_mode }: APSettings) => {
return (
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED
)
}
);
};

View File

@@ -1,7 +1,12 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { AP_SETTINGS_ENDPOINT } from '../api';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import APSettingsForm from './APSettingsForm';
import { APSettings } from './types';
@@ -9,7 +14,6 @@ import { APSettings } from './types';
type APSettingsControllerProps = RestControllerProps<APSettings>;
class APSettingsController extends Component<APSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class APSettingsController extends Component<APSettingsControllerProps> {
<SectionContent title="Access Point Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <APSettingsForm {...formProps} />}
render={(formProps) => <APSettingsForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);

View File

@@ -1,10 +1,19 @@
import React, { Fragment } from 'react';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
import {
TextValidator,
ValidatorForm,
SelectValidator
} from 'react-material-ui-form-validator';
import MenuItem from '@material-ui/core/MenuItem';
import SaveIcon from '@material-ui/icons/Save';
import { PasswordValidator, RestFormProps, FormActions, FormButton } from '../components';
import {
PasswordValidator,
RestFormProps,
FormActions,
FormButton
} from '../components';
import { isAPEnabled } from './APModes';
import { APSettings, APProvisionMode } from './types';
@@ -13,7 +22,6 @@ import { isIP } from '../validators';
type APSettingsFormProps = RestFormProps<APSettings>;
class APSettingsForm extends React.Component<APSettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isIP', isIP);
}
@@ -22,23 +30,29 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
const { data, handleValueChange, saveData } = this.props;
return (
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
<SelectValidator name="provision_mode"
<SelectValidator
name="provision_mode"
label="Provide Access Point&hellip;"
value={data.provision_mode}
fullWidth
variant="outlined"
onChange={handleValueChange('provision_mode')}
margin="normal">
margin="normal"
>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When Network Disconnected</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
When Network Disconnected
</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
</SelectValidator>
{
isAPEnabled(data) &&
{isAPEnabled(data) && (
<Fragment>
<TextValidator
validators={['required', 'matchRegexp:^.{1,32}$']}
errorMessages={['Access Point SSID is required', 'Access Point SSID must be 32 characters or less']}
errorMessages={[
'Access Point SSID is required',
'Access Point SSID must be 32 characters or less'
]}
name="ssid"
label="Access Point SSID"
fullWidth
@@ -49,7 +63,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{8,64}$']}
errorMessages={['Access Point Password is required', 'Access Point Password must be 8-64 characters']}
errorMessages={[
'Access Point Password is required',
'Access Point Password must be 8-64 characters'
]}
name="password"
label="Access Point Password"
fullWidth
@@ -71,7 +88,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Gateway IP is required', 'Must be an IP address']}
errorMessages={[
'Gateway IP is required',
'Must be an IP address'
]}
name="gateway_ip"
label="Gateway"
fullWidth
@@ -82,7 +102,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Subnet mask is required', 'Must be an IP address']}
errorMessages={[
'Subnet mask is required',
'Must be an IP address'
]}
name="subnet_mask"
label="Subnet"
fullWidth
@@ -92,9 +115,14 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
margin="normal"
/>
</Fragment>
}
)}
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -1,28 +1,28 @@
import { Theme } from '@material-ui/core'
import { APStatus, APNetworkStatus } from './types'
import { Theme } from '@material-ui/core';
import { APStatus, APNetworkStatus } from './types';
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return theme.palette.success.main
return theme.palette.success.main;
case APNetworkStatus.INACTIVE:
return theme.palette.info.main
return theme.palette.info.main;
case APNetworkStatus.LINGERING:
return theme.palette.warning.main
return theme.palette.warning.main;
default:
return theme.palette.warning.main
return theme.palette.warning.main;
}
}
};
export const apStatus = ({ status }: APStatus) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return 'Active'
return 'Active';
case APNetworkStatus.INACTIVE:
return 'Inactive'
return 'Inactive';
case APNetworkStatus.LINGERING:
return 'Lingering until idle'
return 'Lingering until idle';
default:
return 'Unknown'
return 'Unknown';
}
}
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { AP_STATUS_ENDPOINT } from '../api';
import APStatusForm from './APStatusForm';
@@ -9,7 +14,6 @@ import { APStatus } from './types';
type APStatusControllerProps = RestControllerProps<APStatus>;
class APStatusController extends Component<APStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,10 +23,10 @@ class APStatusController extends Component<APStatusControllerProps> {
<SectionContent title="Access Point Status">
<RestFormLoader
{...this.props}
render={formProps => <APStatusForm {...formProps} />}
render={(formProps) => <APStatusForm {...formProps} />}
/>
</SectionContent>
)
);
}
}

View File

@@ -1,23 +1,34 @@
import React, { Component, Fragment } from 'react';
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@material-ui/core';
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import ComputerIcon from '@material-ui/icons/Computer';
import RefreshIcon from '@material-ui/icons/Refresh';
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
import {
RestFormProps,
FormActions,
FormButton,
HighlightAvatar
} from '../components';
import { apStatusHighlight, apStatus } from './APStatus';
import { APStatus } from './types';
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
class APStatusForm extends Component<APStatusFormProps> {
createListItems() {
const { data, theme } = this.props
const { data, theme } = this.props;
return (
<Fragment>
<ListItem>
@@ -61,18 +72,20 @@ class APStatusForm extends Component<APStatusFormProps> {
render() {
return (
<Fragment>
<List>
{this.createListItems()}
</List>
<List>{this.createListItems()}</List>
<FormActions>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
Refresh
</FormButton>
</FormActions>
</Fragment>
);
}
}
export default withTheme(APStatusForm);

View File

@@ -1,9 +1,13 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
import {
AuthenticatedContextProps,
withAuthenticatedContext,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import APSettingsController from './APSettingsController';
@@ -12,8 +16,7 @@ import APStatusController from './APStatusController';
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
class AccessPoint extends Component<AccessPointProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -21,17 +24,33 @@ class AccessPoint extends Component<AccessPointProps> {
const { authenticatedContext } = this.props;
return (
<MenuAppBar sectionTitle="Access Point">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/ap/status" label="Access Point Status" />
<Tab value="/ap/settings" label="Access Point Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/ap/settings"
label="Access Point Settings"
disabled={!authenticatedContext.me.admin}
/>
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
<AuthenticatedRoute
exact
path="/ap/status"
component={APStatusController}
/>
<AuthenticatedRoute
exact
path="/ap/settings"
component={APSettingsController}
/>
<Redirect to="/ap/status" />
</Switch>
</MenuAppBar>
)
);
}
}

View File

@@ -1,27 +1,27 @@
export enum APProvisionMode {
AP_MODE_ALWAYS = 0,
AP_MODE_DISCONNECTED = 1,
AP_NEVER = 2,
AP_NEVER = 2
}
export enum APNetworkStatus {
ACTIVE = 0,
INACTIVE = 1,
LINGERING = 2,
LINGERING = 2
}
export interface APStatus {
status: APNetworkStatus
ip_address: string
mac_address: string
station_num: number
status: APNetworkStatus;
ip_address: string;
mac_address: string;
station_num: number;
}
export interface APSettings {
provision_mode: APProvisionMode
ssid: string
password: string
local_ip: string
gateway_ip: string
subnet_mask: string
provision_mode: APProvisionMode;
ssid: string;
password: string;
local_ip: string;
gateway_ip: string;
subnet_mask: string;
}

View File

@@ -1,24 +1,24 @@
import { ENDPOINT_ROOT } from './Env'
import { ENDPOINT_ROOT } from './Env';
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features'
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus'
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings'
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time'
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings'
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus'
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks'
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks'
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings'
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus'
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings'
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware'
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings'
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus'
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus'
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn'
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features';
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus';
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings';
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time';
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings';
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus';
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks';
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks';
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings';
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus';
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings';
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware';
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings';
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus';
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus';
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn';
export const VERIFY_AUTHORIZATION_ENDPOINT =
ENDPOINT_ROOT + 'verifyAuthorization'
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings'
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken'
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart'
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset'
ENDPOINT_ROOT + 'verifyAuthorization';
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings';
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken';
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart';
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset';

View File

@@ -1,24 +1,24 @@
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/')
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/')
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
function calculateEndpointRoot(endpointPath: string) {
const httpRoot = process.env.REACT_APP_HTTP_ROOT
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
if (httpRoot) {
return httpRoot + endpointPath
return httpRoot + endpointPath;
}
const location = window.location
return location.protocol + '//' + location.host + endpointPath
const location = window.location;
return location.protocol + '//' + location.host + endpointPath;
}
function calculateWebSocketRoot(webSocketPath: string) {
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
if (webSocketRoot) {
return webSocketRoot + webSocketPath
return webSocketRoot + webSocketPath;
}
const location = window.location
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
return webProtocol + '//' + location.host + webSocketPath
const location = window.location;
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
return webProtocol + '//' + location.host + webSocketPath;
}

View File

@@ -1,2 +1,2 @@
export * from './Env'
export * from './Endpoints'
export * from './Env';
export * from './Endpoints';

View File

@@ -1,40 +1,56 @@
import * as React from 'react';
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
import {
Redirect,
Route,
RouteProps,
RouteComponentProps
} from 'react-router-dom';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import * as Authentication from './Authentication';
import { withAuthenticationContext, AuthenticationContextProps, AuthenticatedContext, AuthenticatedContextValue } from './AuthenticationContext';
import {
withAuthenticationContext,
AuthenticationContextProps,
AuthenticatedContext,
AuthenticatedContextValue
} from './AuthenticationContext';
interface AuthenticatedRouteProps extends RouteProps, WithSnackbarProps, AuthenticationContextProps {
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
interface AuthenticatedRouteProps
extends RouteProps,
WithSnackbarProps,
AuthenticationContextProps {
component:
| React.ComponentType<RouteComponentProps<any>>
| React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
render() {
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
const {
enqueueSnackbar,
authenticationContext,
component: Component,
...rest
} = this.props;
const { location } = this.props;
const renderComponent: RenderComponent = (props) => {
if (authenticationContext.me) {
return (
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
<AuthenticatedContext.Provider
value={authenticationContext as AuthenticatedContextValue}
>
<Component {...props} />
</AuthenticatedContext.Provider>
);
}
Authentication.storeLoginRedirect(location);
enqueueSnackbar("Please sign in to continue", { variant: 'info' });
return (
<Redirect to='/' />
);
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
return <Redirect to="/" />;
};
return <Route {...rest} render={renderComponent} />;
}
return (
<Route {...rest} render={renderComponent} />
);
}
}
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));

View File

@@ -1,42 +1,42 @@
import * as H from 'history'
import * as H from 'history';
import history from '../history'
import { Features } from '../features/types'
import { getDefaultRoute } from '../AppRouting'
import history from '../history';
import { Features } from '../features/types';
import { getDefaultRoute } from '../AppRouting';
export const ACCESS_TOKEN = 'access_token'
export const SIGN_IN_PATHNAME = 'signInPathname'
export const SIGN_IN_SEARCH = 'signInSearch'
export const ACCESS_TOKEN = 'access_token';
export const SIGN_IN_PATHNAME = 'signInPathname';
export const SIGN_IN_SEARCH = 'signInSearch';
/**
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
*/
export function getStorage() {
return localStorage || sessionStorage
return localStorage || sessionStorage;
}
export function storeLoginRedirect(location?: H.Location) {
if (location) {
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname)
getStorage().setItem(SIGN_IN_SEARCH, location.search)
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
getStorage().setItem(SIGN_IN_SEARCH, location.search);
}
}
export function clearLoginRedirect() {
getStorage().removeItem(SIGN_IN_PATHNAME)
getStorage().removeItem(SIGN_IN_SEARCH)
getStorage().removeItem(SIGN_IN_PATHNAME);
getStorage().removeItem(SIGN_IN_SEARCH);
}
export function fetchLoginRedirect(
features: Features,
features: Features
): H.LocationDescriptorObject {
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME)
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH)
clearLoginRedirect()
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
clearLoginRedirect();
return {
pathname: signInPathname || getDefaultRoute(features),
search: (signInPathname && signInSearch) || undefined,
}
search: (signInPathname && signInSearch) || undefined
};
}
/**
@@ -44,18 +44,18 @@ export function fetchLoginRedirect(
*/
export function authorizedFetch(
url: RequestInfo,
params?: RequestInit,
params?: RequestInit
): Promise<Response> {
const accessToken = getStorage().getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
params = params || {}
params.credentials = 'include'
params = params || {};
params.credentials = 'include';
params.headers = {
...params.headers,
Authorization: 'Bearer ' + accessToken,
Authorization: 'Bearer ' + accessToken
};
}
}
return fetch(url, params)
return fetch(url, params);
}
/**
@@ -67,33 +67,33 @@ export function redirectingAuthorizedUpload(
xhr: XMLHttpRequest,
url: string,
file: File,
onProgress: (event: ProgressEvent<EventTarget>) => void,
onProgress: (event: ProgressEvent<EventTarget>) => void
): Promise<void> {
return new Promise((resolve, reject) => {
xhr.open('POST', url, true)
const accessToken = getStorage().getItem(ACCESS_TOKEN)
xhr.open('POST', url, true);
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
xhr.withCredentials = true
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken)
xhr.withCredentials = true;
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
}
xhr.upload.onprogress = onProgress
xhr.upload.onprogress = onProgress;
xhr.onload = function () {
if (xhr.status === 401 || xhr.status === 403) {
history.push('/unauthorized')
history.push('/unauthorized');
} else {
resolve()
}
}
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
reject(new DOMException('Error', 'UploadError'))
resolve();
}
};
xhr.onerror = function () {
reject(new DOMException('Error', 'UploadError'));
};
xhr.onabort = function () {
reject(new DOMException('Aborted', 'AbortError'))
}
const formData = new FormData()
formData.append('file', file)
xhr.send(formData)
})
reject(new DOMException('Aborted', 'AbortError'));
};
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
});
}
/**
@@ -101,29 +101,29 @@ export function redirectingAuthorizedUpload(
*/
export function redirectingAuthorizedFetch(
url: RequestInfo,
params?: RequestInit,
params?: RequestInit
): Promise<Response> {
return new Promise<Response>((resolve, reject) => {
authorizedFetch(url, params)
.then((response) => {
if (response.status === 401 || response.status === 403) {
history.push('/unauthorized')
history.push('/unauthorized');
} else {
resolve(response)
resolve(response);
}
})
.catch((error) => {
reject(error)
})
})
reject(error);
});
});
}
export function addAccessTokenParameter(url: string) {
const accessToken = getStorage().getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (!accessToken) {
return url
return url;
}
const parsedUrl = new URL(url)
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken)
return parsedUrl.toString()
const parsedUrl = new URL(url);
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
return parsedUrl.toString();
}

View File

@@ -1,9 +1,8 @@
import * as React from "react";
import * as React from 'react';
export interface Me {
username: string;
admin: boolean;
version: string; // proddy added
}
export interface AuthenticationContextValue {
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
me?: Me;
}
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
export const AuthenticationContext = React.createContext(
AuthenticationContextDefaultValue
);
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
authenticationContext: AuthenticationContextValue;
}
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
export function withAuthenticationContext<T extends AuthenticationContextProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<
Omit<T, keyof AuthenticationContextProps>
> {
render() {
return (
<AuthenticationContext.Consumer>
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
{(authenticationContext) => (
<Component
{...(this.props as T)}
authenticationContext={authenticationContext}
/>
)}
</AuthenticationContext.Consumer>
);
}
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
me: Me;
}
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
export const AuthenticatedContext = React.createContext(
AuthenticatedContextDefaultValue
);
@@ -47,12 +55,21 @@ export interface AuthenticatedContextProps {
authenticatedContext: AuthenticatedContextValue;
}
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<
Omit<T, keyof AuthenticatedContextProps>
> {
render() {
return (
<AuthenticatedContext.Consumer>
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
{(authenticatedContext) => (
<Component
{...(this.props as T)}
authenticatedContext={authenticatedContext}
/>
)}
</AuthenticatedContext.Consumer>
);
}

View File

@@ -2,14 +2,19 @@ import * as React from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import jwtDecode from 'jwt-decode';
import history from '../history'
import history from '../history';
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
import { AuthenticationContext, AuthenticationContextValue, Me } from './AuthenticationContext';
import {
AuthenticationContext,
AuthenticationContextValue,
Me
} from './AuthenticationContext';
import FullScreenLoading from '../components/FullScreenLoading';
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
export const decodeMeJWT = (accessToken: string): Me => jwtDecode(accessToken) as Me;
export const decodeMeJWT = (accessToken: string): Me =>
jwtDecode(accessToken) as Me;
interface AuthenticationWrapperState {
context: AuthenticationContextValue;
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
class AuthenticationWrapper extends React.Component<
AuthenticationWrapperProps,
AuthenticationWrapperState
> {
constructor(props: AuthenticationWrapperProps) {
super(props);
this.state = {
context: {
refresh: this.refresh,
signIn: this.signIn,
signOut: this.signOut,
signOut: this.signOut
},
initialized: false
};
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
render() {
return (
<React.Fragment>
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
{this.state.initialized
? this.renderContent()
: this.renderContentLoading()}
</React.Fragment>
);
}
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
}
renderContentLoading() {
return (
<FullScreenLoading />
);
return <FullScreenLoading />;
}
refresh = () => {
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
// return;
// }
const accessToken = getStorage().getItem(ACCESS_TOKEN)
const accessToken = getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
.then(response => {
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
this.setState({ initialized: true, context: { ...this.state.context, me } });
}).catch(error => {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
variant: 'error',
.then((response) => {
const me =
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
this.setState({
initialized: true,
context: { ...this.state.context, me }
});
})
.catch((error) => {
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
this.props.enqueueSnackbar(
'Error verifying authorization: ' + error.message,
{
variant: 'error'
}
);
});
} else {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
}
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
}
};
signIn = (accessToken: string) => {
try {
getStorage().setItem(ACCESS_TOKEN, accessToken);
const me: Me = decodeMeJWT(accessToken);
this.setState({ context: { ...this.state.context, me } });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, { variant: 'success' });
this.props.enqueueSnackbar(`Logged in as ${me.username}`, {
variant: 'success'
});
} catch (err) {
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
throw new Error("Failed to parse JWT " + err.message);
}
this.setState({
initialized: true,
context: { ...this.state.context, me: undefined }
});
throw new Error('Failed to parse JWT ' + err.message);
}
};
signOut = () => {
getStorage().removeItem(ACCESS_TOKEN);
@@ -101,10 +127,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
me: undefined
}
});
this.props.enqueueSnackbar("You have signed out", { variant: 'success', });
this.props.enqueueSnackbar('You have signed out', { variant: 'success' });
history.push('/');
}
};
}
export default withFeatures(withSnackbar(AuthenticationWrapper))
export default withFeatures(withSnackbar(AuthenticationWrapper));

View File

@@ -1,31 +1,46 @@
import * as React from 'react';
import { Redirect, Route, RouteProps, RouteComponentProps } from "react-router-dom";
import {
Redirect,
Route,
RouteProps,
RouteComponentProps
} from 'react-router-dom';
import { withAuthenticationContext, AuthenticationContextProps } from './AuthenticationContext';
import {
withAuthenticationContext,
AuthenticationContextProps
} from './AuthenticationContext';
import * as Authentication from './Authentication';
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
interface UnauthenticatedRouteProps
extends RouteProps,
AuthenticationContextProps,
WithFeaturesProps {
component:
| React.ComponentType<RouteComponentProps<any>>
| React.ComponentType<any>;
}
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
public render() {
const { authenticationContext, component: Component, features, ...rest } = this.props;
const {
authenticationContext,
component: Component,
features,
...rest
} = this.props;
const renderComponent: RenderComponent = (props) => {
if (authenticationContext.me) {
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
}
if (Component) {
return (<Component {...props} />);
return <Component {...props} />;
}
}
return (
<Route {...rest} render={renderComponent} />
);
};
return <Route {...rest} render={renderComponent} />;
}
}

View File

@@ -1,6 +1,6 @@
export { default as AuthenticatedRoute } from './AuthenticatedRoute'
export { default as AuthenticationWrapper } from './AuthenticationWrapper'
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute'
export { default as AuthenticatedRoute } from './AuthenticatedRoute';
export { default as AuthenticationWrapper } from './AuthenticationWrapper';
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute';
export * from './Authentication'
export * from './AuthenticationContext'
export * from './Authentication';
export * from './AuthenticationContext';

View File

@@ -1,27 +1,25 @@
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/styles';
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
import WarningIcon from "@material-ui/icons/Warning"
import { Paper, Typography, Box, CssBaseline } from '@material-ui/core';
import WarningIcon from '@material-ui/icons/Warning';
const styles = makeStyles(
{
const styles = makeStyles({
siteErrorPage: {
display: "flex",
height: "100vh",
justifyContent: "center",
flexDirection: "column"
display: 'flex',
height: '100vh',
justifyContent: 'center',
flexDirection: 'column'
},
siteErrorPagePanel: {
textAlign: "center",
padding: "280px 0 40px 0",
textAlign: 'center',
padding: '280px 0 40px 0',
backgroundImage: 'url("/app/icon.png")',
backgroundRepeat: "no-repeat",
backgroundPosition: "50% 40px",
backgroundSize: "200px auto",
width: "100%",
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% 40px',
backgroundSize: '200px auto',
width: '100%'
}
}
);
});
interface ApplicationErrorProps {
error?: string;
@@ -33,27 +31,29 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
<div className={classes.siteErrorPage}>
<CssBaseline />
<Paper className={classes.siteErrorPagePanel} elevation={10}>
<Box display="flex" flexDirection="row" justifyContent="center" alignItems="center" mb={2}>
<Box
display="flex"
flexDirection="row"
justifyContent="center"
alignItems="center"
mb={2}
>
<WarningIcon fontSize="large" color="error" />
<Box ml={2}>
<Typography variant="h4">
Application error
</Typography>
<Typography variant="h4">Application error</Typography>
</Box>
</Box>
<Typography variant="subtitle1" gutterBottom>
Failed to configure the application, please refresh to try again.
</Typography>
{error &&
(
{error && (
<Typography variant="subtitle2" gutterBottom>
Error: {error}
</Typography>
)
}
)}
</Paper>
</div>
);
}
};
export default ApplicationError;

View File

@@ -1,10 +1,10 @@
import React, { FC } from "react";
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
import { FC } from 'react';
import { FormControlLabel, FormControlLabelProps } from '@material-ui/core';
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
<div>
<FormControlLabel {...props} />
</div>
)
);
export default BlockFormControlLabel;

View File

@@ -1,10 +1,10 @@
import { Button, styled } from "@material-ui/core";
import { Button, styled } from '@material-ui/core';
const ErrorButton = styled(Button)(({ theme }) => ({
color: theme.palette.getContrastText(theme.palette.error.main),
backgroundColor: theme.palette.error.main,
'&:hover': {
backgroundColor: theme.palette.error.dark,
backgroundColor: theme.palette.error.dark
}
}));

View File

@@ -1,4 +1,4 @@
import { styled, Box } from "@material-ui/core";
import { styled, Box } from '@material-ui/core';
const FormActions = styled(Box)(({ theme }) => ({
marginTop: theme.spacing(1)

View File

@@ -1,12 +1,12 @@
import { Button, styled } from "@material-ui/core";
import { Button, styled } from '@material-ui/core';
const FormButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(0, 1),
'&:last-child': {
marginRight: 0,
marginRight: 0
},
'&:first-child': {
marginLeft: 0,
marginLeft: 0
}
}));

View File

@@ -3,30 +3,30 @@ import CircularProgress from '@material-ui/core/CircularProgress';
import { Typography, Theme } from '@material-ui/core';
import { makeStyles, createStyles } from '@material-ui/styles';
const useStyles = makeStyles((theme: Theme) => createStyles({
const useStyles = makeStyles((theme: Theme) =>
createStyles({
fullScreenLoading: {
padding: theme.spacing(2),
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100vh",
flexDirection: "column"
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
flexDirection: 'column'
},
progress: {
margin: theme.spacing(4),
margin: theme.spacing(4)
}
}));
})
);
const FullScreenLoading = () => {
const classes = useStyles();
return (
<div className={classes.fullScreenLoading}>
<CircularProgress className={classes.progress} size={100} />
<Typography variant="h4">
Loading&hellip;
</Typography>
<Typography variant="h4">Loading&hellip;</Typography>
</div>
)
}
);
};
export default FullScreenLoading;

View File

@@ -1,5 +1,5 @@
import { Avatar, makeStyles } from "@material-ui/core";
import React, { FC } from "react";
import { Avatar, makeStyles } from '@material-ui/core';
import { FC } from 'react';
interface HighlightAvatarProps {
color: string;
@@ -13,11 +13,7 @@ const useStyles = makeStyles({
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
const classes = useStyles(props);
return (
<Avatar className={classes.root}>
{props.children}
</Avatar>
);
}
return <Avatar className={classes.root}>{props.children}</Avatar>;
};
export default HighlightAvatar;

View File

@@ -1,5 +1,8 @@
import React from 'react';
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
import {
TextValidator,
ValidatorComponentProps
} from 'react-material-ui-form-validator';
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
import { InputAdornment, IconButton } from '@material-ui/core';
@@ -7,20 +10,23 @@ import { Visibility, VisibilityOff } from '@material-ui/icons';
const styles = createStyles({
input: {
"&::-ms-reveal": {
display: "none"
'&::-ms-reveal': {
display: 'none'
}
}
});
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
type PasswordValidatorProps = WithStyles<typeof styles> &
Exclude<ValidatorComponentProps, 'type' | 'InputProps'>;
interface PasswordValidatorState {
showPassword: boolean;
}
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
class PasswordValidator extends React.Component<
PasswordValidatorProps,
PasswordValidatorState
> {
state = {
showPassword: false
};
@@ -29,7 +35,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
this.setState({
showPassword: !this.state.showPassword
});
}
};
render() {
const { classes, ...rest } = this.props;
@@ -39,7 +45,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
type={this.state.showPassword ? 'text' : 'password'}
InputProps={{
classes,
endAdornment:
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
@@ -48,11 +54,11 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
}}
/>
);
}
}
export default withStyles(styles)(PasswordValidator);

View File

@@ -4,7 +4,9 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
import { redirectingAuthorizedFetch } from '../authentication';
export interface RestControllerProps<D> extends WithSnackbarProps {
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
handleValueChange: (
name: keyof D
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
setData: (data: D, callback?: () => void) => void;
saveData: () => void;
@@ -15,16 +17,18 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
errorMessage?: string;
}
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
export const extractEventValue = (
event: React.ChangeEvent<HTMLInputElement>
) => {
switch (event.target.type) {
case "number":
case 'number':
return event.target.valueAsNumber;
case "checkbox":
case 'checkbox':
return event.target.checked;
default:
return event.target.value
return event.target.value;
}
}
};
interface RestControllerState<D> {
data?: D;
@@ -32,10 +36,15 @@ interface RestControllerState<D> {
errorMessage?: string;
}
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
export function restController<D, P extends RestControllerProps<D>>(
endpointUrl: string,
RestController: React.ComponentType<P & RestControllerProps<D>>
) {
return withSnackbar(
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
class extends React.Component<
Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps,
RestControllerState<D>
> {
state: RestControllerState<D> = {
data: undefined,
loading: false,
@@ -43,12 +52,15 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
};
setData = (data: D, callback?: () => void) => {
this.setState({
this.setState(
{
data,
loading: false,
errorMessage: undefined
}, callback);
}
},
callback
);
};
loadData = () => {
this.setState({
@@ -56,19 +68,24 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
loading: true,
errorMessage: undefined
});
redirectingAuthorizedFetch(endpointUrl).then(response => {
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' });
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 });
@@ -78,36 +95,47 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
})
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Invalid status code: " + response.status);
}).then(json => {
this.props.enqueueSnackbar("Update successful.", { variant: 'success' });
throw Error('Invalid status code: ' + response.status);
})
.then((json) => {
this.props.enqueueSnackbar('Update successful.', {
variant: 'success'
});
this.setState({ data: json, loading: false });
}).catch(error => {
const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
})
.catch((error) => {
const errorMessage = error.message || 'Unknown error';
this.props.enqueueSnackbar('Problem updating: ' + errorMessage, {
variant: 'error'
});
this.setState({ data: undefined, loading: false, errorMessage });
});
}
};
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
handleValueChange = (name: keyof D) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
const data = { ...this.state.data!, [name]: extractEventValue(event) };
this.setState({ data });
}
};
render() {
return <RestController
return (
<RestController
{...this.state}
{...this.props as P}
{...(this.props as P)}
handleValueChange={this.handleValueChange}
setData={this.setData}
saveData={this.saveData}
loadData={this.loadData}
/>;
/>
);
}
});
}
);
}

View File

@@ -8,20 +8,23 @@ import { RestControllerProps } from '.';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
loadingSettings: {
margin: theme.spacing(0.5),
margin: theme.spacing(0.5)
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
marginTop: theme.spacing(2)
}
})
);
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
export type RestFormProps<D> = Omit<
RestControllerProps<D>,
'loading' | 'errorMessage'
> & { data: D };
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
render: (props: RestFormProps<D>) => JSX.Element;
@@ -46,7 +49,12 @@ export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
<Typography variant="h6" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
<Button
variant="contained"
color="secondary"
className={classes.button}
onClick={loadData}
>
Retry
</Button>
</div>

View File

@@ -7,7 +7,7 @@ const useStyles = makeStyles((theme: Theme) =>
createStyles({
content: {
padding: theme.spacing(2),
margin: theme.spacing(3),
margin: theme.spacing(3)
}
})
);

View File

@@ -4,13 +4,20 @@ import { useDropzone, DropzoneState } from 'react-dropzone';
import { makeStyles, createStyles } from '@material-ui/styles';
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
import CancelIcon from '@material-ui/icons/Cancel';
import { Theme, Box, Typography, LinearProgress, Button } from '@material-ui/core';
import {
Theme,
Box,
Typography,
LinearProgress,
Button
} from '@material-ui/core';
interface SingleUploadStyleProps extends DropzoneState {
uploading: boolean;
}
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
const progressPercentage = (progress: ProgressEvent) =>
Math.round((progress.loaded * 100) / progress.total);
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
if (props.isDragAccept) {
@@ -23,9 +30,10 @@ const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
return theme.palette.info.main;
}
return theme.palette.grey[700];
}
};
const useStyles = makeStyles((theme: Theme) => createStyles({
const useStyles = makeStyles((theme: Theme) =>
createStyles({
dropzone: {
padding: theme.spacing(8, 2),
borderWidth: 2,
@@ -33,11 +41,14 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
borderStyle: 'dashed',
color: theme.palette.grey[700],
transition: 'border .24s ease-in-out',
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
cursor: (props: SingleUploadStyleProps) =>
props.uploading ? 'default' : 'pointer',
width: '100%',
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
borderColor: (props: SingleUploadStyleProps) =>
getBorderColor(theme, props)
}
}));
})
);
export interface SingleUploadProps {
onDrop: (acceptedFiles: File[]) => void;
@@ -47,26 +58,44 @@ export interface SingleUploadProps {
progress?: ProgressEvent;
}
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
const SingleUpload: FC<SingleUploadProps> = ({
onDrop,
onCancel,
accept,
uploading,
progress
}) => {
const dropzoneState = useDropzone({
onDrop,
accept,
disabled: uploading,
multiple: false
});
const { getRootProps, getInputProps } = dropzoneState;
const classes = useStyles({ ...dropzoneState, uploading });
const renderProgressText = () => {
if (uploading) {
if (progress?.lengthComputable) {
return `Uploading: ${progressPercentage(progress)}%`;
}
return "Uploading\u2026";
}
return "Drop file or click here";
return 'Uploading\u2026';
}
return 'Drop file or click here';
};
const renderProgress = (progress?: ProgressEvent) => (
<LinearProgress
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
variant={
!progress || progress.lengthComputable ? 'determinate' : 'indeterminate'
}
value={
!progress
? 0
: progress.lengthComputable
? progressPercentage(progress)
: 0
}
/>
);
@@ -74,16 +103,19 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
<div {...getRootProps({ className: classes.dropzone })}>
<input {...getInputProps()} />
<Box flexDirection="column" display="flex" alignItems="center">
<CloudUploadIcon fontSize='large' />
<Typography variant="h6">
{renderProgressText()}
</Typography>
<CloudUploadIcon fontSize="large" />
<Typography variant="h6">{renderProgressText()}</Typography>
{uploading && (
<Fragment>
<Box width="100%" p={2}>
{renderProgress(progress)}
</Box>
<Button startIcon={<CancelIcon />} variant="contained" color="secondary" onClick={onCancel}>
<Button
startIcon={<CancelIcon />}
variant="contained"
color="secondary"
onClick={onCancel}
>
Cancel
</Button>
</Fragment>
@@ -91,6 +123,6 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
</Box>
</div>
);
}
};
export default SingleUpload;

View File

@@ -7,7 +7,9 @@ import { addAccessTokenParameter } from '../authentication';
import { extractEventValue } from '.';
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
handleValueChange: (
name: keyof D
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
setData: (data: D, callback?: () => void) => void;
saveData: () => void;
@@ -25,8 +27,8 @@ interface WebSocketControllerState<D> {
}
enum WebSocketMessageType {
ID = "id",
PAYLOAD = "payload"
ID = 'id',
PAYLOAD = 'payload'
}
interface WebSocketIdMessage {
@@ -40,21 +42,32 @@ interface WebSocketPayloadMessage<D> {
payload: D;
}
export type WebSocketMessage<D> = WebSocketIdMessage | WebSocketPayloadMessage<D>;
export type WebSocketMessage<D> =
| WebSocketIdMessage
| WebSocketPayloadMessage<D>;
export function webSocketController<D, P extends WebSocketControllerProps<D>>(wsUrl: string, wsThrottle: number, WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>) {
export function webSocketController<D, P extends WebSocketControllerProps<D>>(
wsUrl: string,
wsThrottle: number,
WebSocketController: React.ComponentType<P & WebSocketControllerProps<D>>
) {
return withSnackbar(
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
class extends React.Component<
Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps,
WebSocketControllerState<D>
> {
constructor(
props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps
) {
super(props);
this.state = {
ws: new Sockette(addAccessTokenParameter(wsUrl), {
onmessage: this.onMessage,
onopen: this.onOpen,
onclose: this.onClose,
onclose: this.onClose
}),
connected: false
}
};
}
componentWillUnmount() {
@@ -64,37 +77,42 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
onMessage = (event: MessageEvent) => {
const rawData = event.data;
if (typeof rawData === 'string' || rawData instanceof String) {
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
}
this.handleMessage(
JSON.parse(rawData as string) as WebSocketMessage<D>
);
}
};
handleMessage = (message: WebSocketMessage<D>) => {
const { clientId, data } = this.state;
switch (message.type) {
case WebSocketMessageType.ID:
this.setState({ clientId: message.id });
break;
case WebSocketMessageType.PAYLOAD:
const { clientId, data } = this.state;
if (clientId && (!data || clientId !== message.origin_id)) {
this.setState(
{ data: message.payload }
);
this.setState({ data: message.payload });
}
break;
}
}
};
onOpen = () => {
this.setState({ connected: true });
}
};
onClose = () => {
this.setState({ connected: false, clientId: undefined, data: undefined });
}
this.setState({
connected: false,
clientId: undefined,
data: undefined
});
};
setData = (data: D, callback?: () => void) => {
this.setState({ data }, callback);
}
};
saveData = throttle(() => {
const { ws, connected, data } = this.state;
@@ -106,28 +124,35 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
saveDataAndClear = throttle(() => {
const { ws, connected, data } = this.state;
if (connected) {
this.setState({
this.setState(
{
data: undefined
}, () => ws.json(data));
},
() => ws.json(data)
);
}
}, wsThrottle);
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
handleValueChange = (name: keyof D) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
const data = { ...this.state.data!, [name]: extractEventValue(event) };
this.setState({ data });
}
};
render() {
return <WebSocketController
{...this.props as P}
return (
<WebSocketController
{...(this.props as P)}
handleValueChange={this.handleValueChange}
setData={this.setData}
saveData={this.saveData}
saveDataAndClear={this.saveDataAndClear}
connected={this.state.connected}
data={this.state.data}
/>;
/>
);
}
});
}
);
}

View File

@@ -1,5 +1,3 @@
import React from 'react';
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import { LinearProgress, Typography } from '@material-ui/core';
@@ -8,22 +6,27 @@ import { WebSocketControllerProps } from '.';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
loadingSettings: {
margin: theme.spacing(0.5),
margin: theme.spacing(0.5)
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
}
})
);
export type WebSocketFormProps<D> = Omit<WebSocketControllerProps<D>, "connected"> & { data: D };
export type WebSocketFormProps<D> = Omit<
WebSocketControllerProps<D>,
'connected'
> & { data: D };
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
render: (props: WebSocketFormProps<D>) => JSX.Element;
}
export default function WebSocketFormLoader<D>(props: WebSocketFormLoaderProps<D>) {
export default function WebSocketFormLoader<D>(
props: WebSocketFormLoaderProps<D>
) {
const { connected, render, data, ...rest } = props;
const classes = useStyles();
if (!connected || !data) {

View File

@@ -1,17 +1,17 @@
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 { default as WebSocketFormLoader } from './WebSocketFormLoader'
export { default as ErrorButton } from './ErrorButton'
export { default as SingleUpload } from './SingleUpload'
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 { default as WebSocketFormLoader } from './WebSocketFormLoader';
export { default as ErrorButton } from './ErrorButton';
export { default as SingleUpload } from './SingleUpload';
export * from './RestFormLoader'
export * from './RestController'
export * from './RestFormLoader';
export * from './RestController';
export * from './WebSocketFormLoader'
export * from './WebSocketController'
export * from './WebSocketFormLoader';
export * from './WebSocketController';

View File

@@ -5,21 +5,26 @@ export interface FeaturesContextValue {
features: Features;
}
const FeaturesContextDefaultValue = {} as FeaturesContextValue
export const FeaturesContext = React.createContext(
FeaturesContextDefaultValue
);
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
export const FeaturesContext = React.createContext(FeaturesContextDefaultValue);
export interface WithFeaturesProps {
features: Features;
}
export function withFeatures<T extends WithFeaturesProps>(Component: React.ComponentType<T>) {
export function withFeatures<T extends WithFeaturesProps>(
Component: React.ComponentType<T>
) {
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
render() {
return (
<FeaturesContext.Consumer>
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
{(featuresContext) => (
<Component
{...(this.props as T)}
features={featuresContext.features}
/>
)}
</FeaturesContext.Consumer>
);
}

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { Features } from './types';
import { FeaturesContext } from './FeaturesContext';
@@ -9,10 +9,9 @@ import { FEATURES_ENDPOINT } from '../api';
interface FeaturesWrapperState {
features?: Features;
error?: string;
};
}
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
state: FeaturesWrapperState = {};
componentDidMount() {
@@ -21,41 +20,39 @@ class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
fetchFeaturesDetails = () => {
fetch(FEATURES_ENDPOINT)
.then(response => {
.then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw Error("Unexpected status code: " + response.status);
throw Error('Unexpected status code: ' + response.status);
}
}).then(features => {
})
.then((features) => {
this.setState({ features });
})
.catch(error => {
.catch((error) => {
this.setState({ error: error.message });
});
}
};
render() {
const { features, error } = this.state;
if (features) {
return (
<FeaturesContext.Provider value={{
<FeaturesContext.Provider
value={{
features
}}>
}}
>
{this.props.children}
</FeaturesContext.Provider>
);
}
if (error) {
return (
<ApplicationError error={error} />
);
return <ApplicationError error={error} />;
}
return (
<FullScreenLoading />
);
return <FullScreenLoading />;
}
}
export default FeaturesWrapper;

View File

@@ -1,8 +1,8 @@
export interface Features {
project: boolean
security: boolean
mqtt: boolean
ntp: boolean
ota: boolean
upload_firmware: boolean
project: boolean;
security: boolean;
mqtt: boolean;
ntp: boolean;
ota: boolean;
upload_firmware: boolean;
}

View File

@@ -1,5 +1,5 @@
import { createBrowserHistory } from 'history'
import { createBrowserHistory } from 'history';
export default createBrowserHistory({
/* pass a configuration object here if needed */
})
});

View File

@@ -6,8 +6,9 @@ import { Router } from 'react-router';
import App from './App';
render((
render(
<Router history={history}>
<App />
</Router>
), document.getElementById("root"))
</Router>,
document.getElementById('root')
);

View File

@@ -1,9 +1,13 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
import {
AuthenticatedContextProps,
withAuthenticatedContext,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import MqttStatusController from './MqttStatusController';
import MqttSettingsController from './MqttSettingsController';
@@ -11,8 +15,7 @@ import MqttSettingsController from './MqttSettingsController';
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
class Mqtt extends Component<MqttProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -20,17 +23,33 @@ class Mqtt extends Component<MqttProps> {
const { authenticatedContext } = this.props;
return (
<MenuAppBar sectionTitle="MQTT">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/mqtt/status" label="MQTT Status" />
<Tab value="/mqtt/settings" label="MQTT Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/mqtt/settings"
label="MQTT Settings"
disabled={!authenticatedContext.me.admin}
/>
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
<AuthenticatedRoute
exact
path="/mqtt/status"
component={MqttStatusController}
/>
<AuthenticatedRoute
exact
path="/mqtt/settings"
component={MqttSettingsController}
/>
<Redirect to="/mqtt/status" />
</Switch>
</MenuAppBar>
)
);
}
}

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { MQTT_SETTINGS_ENDPOINT } from '../api';
import MqttSettingsForm from './MqttSettingsForm';
@@ -9,7 +14,6 @@ import { MqttSettings } from './types';
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class MqttSettingsController extends Component<MqttSettingsControllerProps> {
<SectionContent title="MQTT Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <MqttSettingsForm {...formProps} />}
render={(formProps) => <MqttSettingsForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);

View File

@@ -1,31 +1,31 @@
import React from "react";
import React from 'react';
import {
TextValidator,
ValidatorForm,
SelectValidator,
} from "react-material-ui-form-validator";
SelectValidator
} from 'react-material-ui-form-validator';
import { Checkbox, TextField, Typography } from "@material-ui/core";
import SaveIcon from "@material-ui/icons/Save";
import MenuItem from "@material-ui/core/MenuItem";
import { Checkbox, TextField, Typography } from '@material-ui/core';
import SaveIcon from '@material-ui/icons/Save';
import MenuItem from '@material-ui/core/MenuItem';
import {
RestFormProps,
FormActions,
FormButton,
BlockFormControlLabel,
PasswordValidator,
} from "../components";
import { isIP, isHostname, or, isPath } from "../validators";
PasswordValidator
} from '../components';
import { isIP, isHostname, or, isPath } from '../validators';
import { MqttSettings } from "./types";
import { MqttSettings } from './types';
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule("isIPOrHostname", or(isIP, isHostname));
ValidatorForm.addValidationRule("isPath", isPath);
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
ValidatorForm.addValidationRule('isPath', isPath);
}
render() {
@@ -36,38 +36,38 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.enabled}
onChange={handleValueChange("enabled")}
onChange={handleValueChange('enabled')}
value="enabled"
/>
}
label="Enable MQTT"
/>
<TextValidator
validators={["required", "isIPOrHostname"]}
validators={['required', 'isIPOrHostname']}
errorMessages={[
"Host is required",
"Not a valid IP address or hostname",
'Host is required',
'Not a valid IP address or hostname'
]}
name="host"
label="Host"
fullWidth
variant="outlined"
value={data.host}
onChange={handleValueChange("host")}
onChange={handleValueChange('host')}
margin="normal"
/>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Port is required",
"Must be a number",
"Must be greater than 0 ",
"Max value is 65535",
'Port is required',
'Must be a number',
'Must be greater than 0 ',
'Max value is 65535'
]}
name="port"
label="Port"
@@ -75,18 +75,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.port}
type="number"
onChange={handleValueChange("port")}
onChange={handleValueChange('port')}
margin="normal"
/>
<TextValidator
validators={["required", "isPath"]}
errorMessages={["Base is required", "Not a valid Path"]}
validators={['required', 'isPath']}
errorMessages={['Base is required', 'Not a valid Path']}
name="base"
label="Base"
fullWidth
variant="outlined"
value={data.base}
onChange={handleValueChange("base")}
onChange={handleValueChange('base')}
margin="normal"
/>
<TextField
@@ -95,7 +95,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.username}
onChange={handleValueChange("username")}
onChange={handleValueChange('username')}
margin="normal"
/>
<PasswordValidator
@@ -104,7 +104,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.password}
onChange={handleValueChange("password")}
onChange={handleValueChange('password')}
margin="normal"
/>
<TextField
@@ -113,21 +113,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
fullWidth
variant="outlined"
value={data.client_id}
onChange={handleValueChange("client_id")}
onChange={handleValueChange('client_id')}
margin="normal"
/>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:1",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:1',
'maxNumber:65535'
]}
errorMessages={[
"Keep alive is required",
"Must be a number",
"Must be greater than 0",
"Max value is 65535",
'Keep alive is required',
'Must be a number',
'Must be greater than 0',
'Max value is 65535'
]}
name="keep_alive"
label="Keep Alive (seconds)"
@@ -135,7 +135,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.keep_alive}
type="number"
onChange={handleValueChange("keep_alive")}
onChange={handleValueChange('keep_alive')}
margin="normal"
/>
<SelectValidator
@@ -144,7 +144,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.mqtt_qos}
fullWidth
variant="outlined"
onChange={handleValueChange("mqtt_qos")}
onChange={handleValueChange('mqtt_qos')}
margin="normal"
>
<MenuItem value={0}>0 (default)</MenuItem>
@@ -155,7 +155,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.clean_session}
onChange={handleValueChange("clean_session")}
onChange={handleValueChange('clean_session')}
value="clean_session"
/>
}
@@ -165,7 +165,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.mqtt_retain}
onChange={handleValueChange("mqtt_retain")}
onChange={handleValueChange('mqtt_retain')}
value="mqtt_retain"
/>
}
@@ -181,7 +181,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.nested_format}
fullWidth
variant="outlined"
onChange={handleValueChange("nested_format")}
onChange={handleValueChange('nested_format')}
margin="normal"
>
<MenuItem value={1}>nested on a single topic</MenuItem>
@@ -193,7 +193,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.dallas_format}
fullWidth
variant="outlined"
onChange={handleValueChange("dallas_format")}
onChange={handleValueChange('dallas_format')}
margin="normal"
>
<MenuItem value={1}>by Sensor ID</MenuItem>
@@ -205,7 +205,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.bool_format}
fullWidth
variant="outlined"
onChange={handleValueChange("bool_format")}
onChange={handleValueChange('bool_format')}
margin="normal"
>
<MenuItem value={1}>"on"/"off"</MenuItem>
@@ -219,7 +219,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.subscribe_format}
fullWidth
variant="outlined"
onChange={handleValueChange("subscribe_format")}
onChange={handleValueChange('subscribe_format')}
margin="normal"
>
<MenuItem value={0}>general device topic</MenuItem>
@@ -230,7 +230,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
control={
<Checkbox
checked={data.ha_enabled}
onChange={handleValueChange("ha_enabled")}
onChange={handleValueChange('ha_enabled')}
value="ha_enabled"
/>
}
@@ -243,7 +243,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
value={data.ha_climate_format}
fullWidth
variant="outlined"
onChange={handleValueChange("ha_climate_format")}
onChange={handleValueChange('ha_climate_format')}
margin="normal"
>
<MenuItem value={1}>use Current temperature (default)</MenuItem>
@@ -257,16 +257,16 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
</Typography>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'Publish time is required',
'Must be a number',
'Must be 0 or greater',
'Max value is 65535'
]}
name="publish_time_boiler"
label="Boiler Publish Interval (seconds, 0=on change)"
@@ -274,21 +274,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.publish_time_boiler}
type="number"
onChange={handleValueChange("publish_time_boiler")}
onChange={handleValueChange('publish_time_boiler')}
margin="normal"
/>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'Publish time is required',
'Must be a number',
'Must be 0 or greater',
'Max value is 65535'
]}
name="publish_time_thermostat"
label="Thermostat Publish Interval (seconds, 0=on change)"
@@ -296,21 +296,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.publish_time_thermostat}
type="number"
onChange={handleValueChange("publish_time_thermostat")}
onChange={handleValueChange('publish_time_thermostat')}
margin="normal"
/>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'Publish time is required',
'Must be a number',
'Must be 0 or greater',
'Max value is 65535'
]}
name="publish_time_solar"
label="Solar Publish Interval (seconds, 0=on change)"
@@ -318,21 +318,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.publish_time_solar}
type="number"
onChange={handleValueChange("publish_time_solar")}
onChange={handleValueChange('publish_time_solar')}
margin="normal"
/>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'Publish time is required',
'Must be a number',
'Must be 0 or greater',
'Max value is 65535'
]}
name="publish_time_mixer"
label="Mixer Publish Interval (seconds, 0=on change)"
@@ -340,21 +340,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.publish_time_mixer}
type="number"
onChange={handleValueChange("publish_time_mixer")}
onChange={handleValueChange('publish_time_mixer')}
margin="normal"
/>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'Publish time is required',
'Must be a number',
'Must be 0 or greater',
'Max value is 65535'
]}
name="publish_time_sensor"
label="Sensors Publish Interval (seconds, 0=on change)"
@@ -362,21 +362,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.publish_time_sensor}
type="number"
onChange={handleValueChange("publish_time_sensor")}
onChange={handleValueChange('publish_time_sensor')}
margin="normal"
/>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Publish time is required",
"Must be a number",
"Must be 0 or greater",
"Max value is 65535",
'Publish time is required',
'Must be a number',
'Must be 0 or greater',
'Max value is 65535'
]}
name="publish_time_other"
label="All other Modules Publish Interval (seconds, 0=on change)"
@@ -384,7 +384,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
variant="outlined"
value={data.publish_time_other}
type="number"
onChange={handleValueChange("publish_time_other")}
onChange={handleValueChange('publish_time_other')}
margin="normal"
/>
<FormActions>

View File

@@ -1,59 +1,59 @@
import { Theme } from '@material-ui/core'
import { MqttStatus, MqttDisconnectReason } from './types'
import { Theme } from '@material-ui/core';
import { MqttStatus, MqttDisconnectReason } from './types';
export const mqttStatusHighlight = (
{ enabled, connected }: MqttStatus,
theme: Theme,
theme: Theme
) => {
if (!enabled) {
return theme.palette.info.main
return theme.palette.info.main;
}
if (connected) {
return theme.palette.success.main
return theme.palette.success.main;
}
return theme.palette.error.main
}
return theme.palette.error.main;
};
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
if (!enabled) {
return 'Not enabled'
return 'Not enabled';
}
if (connected) {
return 'Connected'
return 'Connected';
}
return 'Disconnected'
}
return 'Disconnected';
};
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
switch (disconnect_reason) {
case MqttDisconnectReason.TCP_DISCONNECTED:
return 'TCP disconnected'
return 'TCP disconnected';
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
return 'Unacceptable protocol version'
return 'Unacceptable protocol version';
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
return 'Client ID rejected'
return 'Client ID rejected';
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
return 'Server unavailable'
return 'Server unavailable';
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
return 'Malformed credentials'
return 'Malformed credentials';
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
return 'Not authorized'
return 'Not authorized';
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
return 'Device out of memory'
return 'Device out of memory';
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
return 'Server fingerprint invalid'
return 'Server fingerprint invalid';
default:
return 'Unknown'
return 'Unknown';
}
}
};
export const mqttPublishHighlight = (
{ mqtt_fails }: MqttStatus,
theme: Theme,
theme: Theme
) => {
if (mqtt_fails === 0) return theme.palette.success.main
if (mqtt_fails === 0) return theme.palette.success.main;
if (mqtt_fails < 10) return theme.palette.warning.main
if (mqtt_fails < 10) return theme.palette.warning.main;
return theme.palette.error.main
}
return theme.palette.error.main;
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { MQTT_STATUS_ENDPOINT } from '../api';
import MqttStatusForm from './MqttStatusForm';
@@ -9,7 +14,6 @@ import { MqttStatus } from './types';
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
class MqttStatusController extends Component<MqttStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,10 +23,10 @@ class MqttStatusController extends Component<MqttStatusControllerProps> {
<SectionContent title="MQTT Status">
<RestFormLoader
{...this.props}
render={formProps => <MqttStatusForm {...formProps} />}
render={(formProps) => <MqttStatusForm {...formProps} />}
/>
</SectionContent>
)
);
}
}

View File

@@ -1,23 +1,39 @@
import React, { Component, Fragment } from 'react';
import { Component, Fragment } from 'react';
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core';
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText
} from '@material-ui/core';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import RefreshIcon from '@material-ui/icons/Refresh';
import ReportIcon from '@material-ui/icons/Report';
import SpeakerNotesOffIcon from "@material-ui/icons/SpeakerNotesOff";
import SpeakerNotesOffIcon from '@material-ui/icons/SpeakerNotesOff';
import { RestFormProps, FormActions, FormButton, HighlightAvatar } from '../components';
import { mqttStatusHighlight, mqttStatus, mqttPublishHighlight, disconnectReason } from './MqttStatus';
import {
RestFormProps,
FormActions,
FormButton,
HighlightAvatar
} from '../components';
import {
mqttStatusHighlight,
mqttStatus,
mqttPublishHighlight,
disconnectReason
} from './MqttStatus';
import { MqttStatus } from './types';
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
class MqttStatusForm extends Component<MqttStatusFormProps> {
renderConnectionStatus() {
const { data, theme } = this.props
const { data, theme } = this.props;
if (data.connected) {
return (
<Fragment>
@@ -50,7 +66,10 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
<ReportIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
<ListItemText
primary="Disconnect Reason"
secondary={disconnectReason(data)}
/>
</ListItem>
<Divider variant="inset" component="li" />
</Fragment>
@@ -58,7 +77,7 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
}
createListItems() {
const { data, theme } = this.props
const { data, theme } = this.props;
return (
<Fragment>
<ListItem>
@@ -78,18 +97,20 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
render() {
return (
<Fragment>
<List>
{this.createListItems()}
</List>
<List>{this.createListItems()}</List>
<FormActions>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
Refresh
</FormButton>
</FormActions>
</Fragment>
);
}
}
export default withTheme(MqttStatusForm);

View File

@@ -6,40 +6,40 @@ export enum MqttDisconnectReason {
MQTT_MALFORMED_CREDENTIALS = 4,
MQTT_NOT_AUTHORIZED = 5,
ESP8266_NOT_ENOUGH_SPACE = 6,
TLS_BAD_FINGERPRINT = 7,
TLS_BAD_FINGERPRINT = 7
}
export interface MqttStatus {
enabled: boolean
connected: boolean
client_id: string
disconnect_reason: MqttDisconnectReason
mqtt_fails: number
enabled: boolean;
connected: boolean;
client_id: string;
disconnect_reason: MqttDisconnectReason;
mqtt_fails: number;
}
export interface MqttSettings {
enabled: boolean
host: string
port: number
base: string
username: string
password: string
client_id: string
keep_alive: number
clean_session: boolean
max_topic_length: number
publish_time_boiler: number
publish_time_thermostat: number
publish_time_solar: number
publish_time_mixer: number
publish_time_other: number
publish_time_sensor: number
dallas_format: number
bool_format: number
mqtt_qos: number
mqtt_retain: boolean
ha_enabled: boolean
ha_climate_format: number
nested_format: number
subscribe_format: number
enabled: boolean;
host: string;
port: number;
base: string;
username: string;
password: string;
client_id: string;
keep_alive: number;
clean_session: boolean;
max_topic_length: number;
publish_time_boiler: number;
publish_time_thermostat: number;
publish_time_solar: number;
publish_time_mixer: number;
publish_time_other: number;
publish_time_sensor: number;
dallas_format: number;
bool_format: number;
mqtt_qos: number;
mqtt_retain: boolean;
ha_enabled: boolean;
ha_climate_format: number;
nested_format: number;
subscribe_format: number;
}

View File

@@ -1,22 +1,31 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { 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 {
withAuthenticatedContext,
AuthenticatedContextProps,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import NetworkStatusController from './NetworkStatusController';
import NetworkSettingsController from './NetworkSettingsController';
import WiFiNetworkScanner from './WiFiNetworkScanner';
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
import {
NetworkConnectionContext,
NetworkConnectionContextValue
} from './NetworkConnectionContext';
import { WiFiNetwork } from './types';
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnectionContextValue> {
class NetworkConnection extends Component<
NetworkConnectionProps,
NetworkConnectionContextValue
> {
constructor(props: NetworkConnectionProps) {
super(props);
this.state = {
@@ -28,13 +37,13 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
selectNetwork = (network: WiFiNetwork) => {
this.setState({ selectedNetwork: network });
this.props.history.push('/network/settings');
}
};
deselectNetwork = () => {
this.setState({ selectedNetwork: undefined });
}
};
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -43,20 +52,44 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
return (
<NetworkConnectionContext.Provider value={this.state}>
<MenuAppBar sectionTitle="Network Connection">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/network/status" label="Network Status" />
<Tab value="/network/scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
<Tab value="/network/settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/network/scan"
label="Scan WiFi Networks"
disabled={!authenticatedContext.me.admin}
/>
<Tab
value="/network/settings"
label="Network Settings"
disabled={!authenticatedContext.me.admin}
/>
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/network/status" component={NetworkStatusController} />
<AuthenticatedRoute exact path="/network/scan" component={WiFiNetworkScanner} />
<AuthenticatedRoute exact path="/network/settings" component={NetworkSettingsController} />
<AuthenticatedRoute
exact
path="/network/status"
component={NetworkStatusController}
/>
<AuthenticatedRoute
exact
path="/network/scan"
component={WiFiNetworkScanner}
/>
<AuthenticatedRoute
exact
path="/network/settings"
component={NetworkSettingsController}
/>
<Redirect to="/network/status" />
</Switch>
</MenuAppBar>
</NetworkConnectionContext.Provider>
)
);
}
}

View File

@@ -7,7 +7,7 @@ export interface NetworkConnectionContextValue {
deselectNetwork: () => void;
}
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue;
export const NetworkConnectionContext = React.createContext(
NetworkConnectionContextDefaultValue
);

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import NetworkSettingsForm from './NetworkSettingsForm';
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
import { NetworkSettings } from './types';
@@ -8,7 +13,6 @@ import { NetworkSettings } from './types';
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -18,12 +22,14 @@ class NetworkSettingsController extends Component<NetworkSettingsControllerProps
<SectionContent title="Network Settings">
<RestFormLoader
{...this.props}
render={formProps => <NetworkSettingsForm {...formProps} />}
render={(formProps) => <NetworkSettingsForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(NETWORK_SETTINGS_ENDPOINT, NetworkSettingsController);
export default restController(
NETWORK_SETTINGS_ENDPOINT,
NetworkSettingsController
);

View File

@@ -1,7 +1,14 @@
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 {
Checkbox,
List,
ListItem,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction
} from '@material-ui/core';
import Avatar from '@material-ui/core/Avatar';
import IconButton from '@material-ui/core/IconButton';
@@ -10,31 +17,42 @@ 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 {
RestFormProps,
PasswordValidator,
BlockFormControlLabel,
FormActions,
FormButton
} from '../components';
import { isIP, isHostname, optional } from '../validators';
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
import {
NetworkConnectionContext,
NetworkConnectionContextValue
} from './NetworkConnectionContext';
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
import { NetworkSettings } from './types';
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
static contextType = NetworkConnectionContext;
context!: React.ContextType<typeof NetworkConnectionContext>;
constructor(props: NetworkStatusFormProps, context: NetworkConnectionContextValue) {
constructor(
props: NetworkStatusFormProps,
context: NetworkConnectionContextValue
) {
super(props);
const { selectedNetwork } = context;
if (selectedNetwork) {
const networkSettings: NetworkSettings = {
ssid: selectedNetwork.ssid,
password: "",
password: '',
hostname: props.data.hostname,
static_ip_config: false,
}
static_ip_config: false
};
props.setData(networkSettings);
}
}
@@ -48,7 +66,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
deselectNetworkAndLoadData = () => {
this.context.deselectNetwork();
this.props.loadData();
}
};
componentWillUnmount() {
this.context.deselectNetwork();
@@ -59,27 +77,38 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
const { data, handleValueChange, saveData } = this.props;
return (
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
{
selectedNetwork ?
{selectedNetwork ? (
<List>
<ListItem>
<ListItemAvatar>
<Avatar>
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
{isNetworkOpen(selectedNetwork) ? (
<LockOpenIcon />
) : (
<LockIcon />
)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={selectedNetwork.ssid}
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
secondary={
'Security: ' +
networkSecurityMode(selectedNetwork) +
', Ch: ' +
selectedNetwork.channel
}
/>
<ListItemSecondaryAction>
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
<IconButton
aria-label="Manual Config"
onClick={deselectNetwork}
>
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</List>
:
) : (
<TextValidator
validators={['matchRegexp:^.{0,32}$']}
errorMessages={['SSID must be 32 characters or less']}
@@ -91,9 +120,8 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
onChange={handleValueChange('ssid')}
margin="normal"
/>
}
{
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
)}
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
<PasswordValidator
validators={['matchRegexp:^.{0,64}$']}
errorMessages={['Password must be 64 characters or less']}
@@ -105,10 +133,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
onChange={handleValueChange('password')}
margin="normal"
/>
}
)}
<TextValidator
validators={['required', 'isHostname']}
errorMessages={['Hostname is required', "Not a valid hostname"]}
errorMessages={['Hostname is required', 'Not a valid hostname']}
name="hostname"
label="Hostname"
fullWidth
@@ -122,13 +150,12 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
<Checkbox
value="static_ip_config"
checked={data.static_ip_config}
onChange={handleValueChange("static_ip_config")}
onChange={handleValueChange('static_ip_config')}
/>
}
label="Static IP Config"
/>
{
data.static_ip_config &&
{data.static_ip_config && (
<Fragment>
<TextValidator
validators={['required', 'isIP']}
@@ -154,7 +181,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
/>
<TextValidator
validators={['required', 'isIP']}
errorMessages={['Subnet mask is required', 'Must be an IP address']}
errorMessages={[
'Subnet mask is required',
'Must be an IP address'
]}
name="subnet_mask"
label="Subnet"
fullWidth
@@ -186,9 +216,14 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
margin="normal"
/>
</Fragment>
}
)}
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -1,57 +1,57 @@
import { Theme } from '@material-ui/core'
import { NetworkStatus, NetworkConnectionStatus } from './types'
import { Theme } from '@material-ui/core';
import { NetworkStatus, NetworkConnectionStatus } from './types';
export const isConnected = ({ status }: NetworkStatus) => {
return (
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
)
}
);
};
export const isWiFi = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
export const isEthernet = ({ status }: NetworkStatus) =>
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
export const networkStatusHighlight = (
{ status }: NetworkStatus,
theme: Theme,
theme: Theme
) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return theme.palette.info.main
return theme.palette.info.main;
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return theme.palette.success.main
return theme.palette.success.main;
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return theme.palette.error.main
return theme.palette.error.main;
default:
return theme.palette.warning.main
return theme.palette.warning.main;
}
}
};
export const networkStatus = ({ status }: NetworkStatus) => {
switch (status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return 'Inactive'
return 'Inactive';
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
return 'Idle'
return 'Idle';
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
return 'No SSID Available'
return 'No SSID Available';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
return 'Connected (WiFi)'
return 'Connected (WiFi)';
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
return 'Connected (Ethernet)'
return 'Connected (Ethernet)';
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
return 'Connection Failed'
return 'Connection Failed';
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
return 'Connection Lost'
return 'Connection Lost';
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
return 'Disconnected'
return 'Disconnected';
default:
return 'Unknown'
return 'Unknown';
}
}
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import NetworkStatusForm from './NetworkStatusForm';
import { NETWORK_STATUS_ENDPOINT } from '../api';
import { NetworkStatus } from './types';
@@ -8,7 +13,6 @@ import { NetworkStatus } from './types';
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -18,12 +22,11 @@ class NetworkStatusController extends Component<NetworkStatusControllerProps> {
<SectionContent title="Network Status">
<RestFormLoader
{...this.props}
render={formProps => <NetworkStatusForm {...formProps} />}
render={(formProps) => <NetworkStatusForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);

View File

@@ -1,46 +1,46 @@
import React, { Component, Fragment } from "react";
import { Component, Fragment } from 'react';
import { WithTheme, withTheme } from "@material-ui/core/styles";
import { WithTheme, withTheme } from '@material-ui/core/styles';
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
} from "@material-ui/core";
ListItemText
} from '@material-ui/core';
import DNSIcon from "@material-ui/icons/Dns";
import WifiIcon from "@material-ui/icons/Wifi";
import RouterIcon from "@material-ui/icons/Router";
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 DNSIcon from '@material-ui/icons/Dns';
import WifiIcon from '@material-ui/icons/Wifi';
import RouterIcon from '@material-ui/icons/Router';
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 {
RestFormProps,
FormActions,
FormButton,
HighlightAvatar,
} from "../components";
HighlightAvatar
} from '../components';
import {
networkStatus,
networkStatusHighlight,
isConnected,
isWiFi,
isEthernet,
} from "./NetworkStatus";
import { NetworkStatus } from "./types";
isEthernet
} from './NetworkStatus';
import { NetworkStatus } from './types';
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
dnsServers(status: NetworkStatus) {
if (!status.dns_ip_1) {
return "none";
return 'none';
}
return status.dns_ip_1 + (status.dns_ip_2 ? "," + status.dns_ip_2 : "");
return status.dns_ip_1 + (status.dns_ip_2 ? ',' + status.dns_ip_2 : '');
}
createListItems() {
@@ -110,7 +110,7 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
</ListItemAvatar>
<ListItemText
primary="Gateway IP"
secondary={data.gateway_ip || "none"}
secondary={data.gateway_ip || 'none'}
/>
</ListItem>
<Divider variant="inset" component="li" />

View File

@@ -1,7 +1,14 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import { createStyles, WithStyles, Theme, withStyles, Typography, LinearProgress } from '@material-ui/core';
import {
createStyles,
WithStyles,
Theme,
withStyles,
Typography,
LinearProgress
} from '@material-ui/core';
import PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
import { FormActions, FormButton, SectionContent } from '../components';
@@ -11,9 +18,9 @@ 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"
const NUM_POLLS = 10;
const POLLING_FREQUENCY = 500;
const RETRY_EXCEPTION_TYPE = 'retry';
interface WiFiNetworkScannerState {
scanningForNetworks: boolean;
@@ -21,28 +28,31 @@ interface WiFiNetworkScannerState {
networkList?: WiFiNetworkList;
}
const styles = (theme: Theme) => createStyles({
const styles = (theme: Theme) =>
createStyles({
scanningSettings: {
margin: theme.spacing(0.5),
margin: theme.spacing(0.5)
},
scanningSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
},
scanningProgress: {
margin: theme.spacing(4),
textAlign: "center"
textAlign: 'center'
}
});
});
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
pollCount: number = 0;
class WiFiNetworkScanner extends Component<
WiFiNetworkScannerProps,
WiFiNetworkScannerState
> {
pollCount = 0;
state: WiFiNetworkScannerState = {
scanningForNetworks: false,
scanningForNetworks: false
};
componentDidMount() {
@@ -54,22 +64,35 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
if (!scanningForNetworks) {
this.scanNetworks();
}
}
};
scanNetworks() {
this.pollCount = 0;
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
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',
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
});
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
});
}
@@ -80,21 +103,20 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
retryError() {
return {
name: RETRY_EXCEPTION_TYPE,
message: "Network list not ready, will retry in " + POLLING_FREQUENCY + "ms."
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;
if (network1.rssi < network2.rssi) return 1;
if (network1.rssi > network2.rssi) return -1;
return 0;
}
pollNetworkList = () => {
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
.then(response => {
.then((response) => {
if (response.status === 200) {
return response.json();
}
@@ -103,24 +125,34 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
this.schedulePollTimeout();
throw this.retryError();
} else {
throw Error("Device did not return network list in timely manner.");
throw Error('Device did not return network list in timely manner.');
}
}
throw Error("Device returned unexpected response code: " + response.status);
throw Error(
'Device returned unexpected response code: ' + response.status
);
})
.then(json => {
json.networks.sort(this.compareNetworks)
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
.then((json) => {
json.networks.sort(this.compareNetworks);
this.setState({
scanningForNetworks: false,
networkList: json,
errorMessage: undefined
});
})
.catch(error => {
.catch((error) => {
if (error.name !== RETRY_EXCEPTION_TYPE) {
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
variant: 'error',
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
variant: 'error'
});
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
}
this.setState({
scanningForNetworks: false,
networkList: undefined,
errorMessage: error.message
});
}
});
};
renderNetworkScanner() {
const { classes } = this.props;
@@ -144,9 +176,7 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
</div>
);
}
return (
<WiFiNetworkSelector networkList={networkList} />
);
return <WiFiNetworkSelector networkList={networkList} />;
}
render() {
@@ -155,14 +185,19 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
<SectionContent title="Network Scanner">
{this.renderNetworkScanner()}
<FormActions>
<FormButton startIcon={<PermScanWifiIcon />} variant="contained" color="secondary" onClick={this.requestNetworkScan} disabled={scanningForNetworks}>
<FormButton
startIcon={<PermScanWifiIcon />}
variant="contained"
color="secondary"
onClick={this.requestNetworkScan}
disabled={scanningForNetworks}
>
Scan again&hellip;
</FormButton>
</FormActions>
</SectionContent>
);
}
}
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));

View File

@@ -1,7 +1,13 @@
import React, { Component } from 'react';
import { Avatar, Badge } from '@material-ui/core';
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } 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';
@@ -16,13 +22,16 @@ interface WiFiNetworkSelectorProps {
}
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
static contextType = NetworkConnectionContext;
context!: React.ContextType<typeof NetworkConnectionContext>;
renderNetwork = (network: WiFiNetwork) => {
return (
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
<ListItem
key={network.bssid}
button
onClick={() => this.context.selectNetwork(network)}
>
<ListItemAvatar>
<Avatar>
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
@@ -30,25 +39,27 @@ class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
</ListItemAvatar>
<ListItemText
primary={network.ssid}
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
secondary={
'Security: ' +
networkSecurityMode(network) +
', Ch: ' +
network.channel
}
/>
<ListItemIcon>
<Badge badgeContent={network.rssi + "db"}>
<Badge badgeContent={network.rssi + 'db'}>
<WifiIcon />
</Badge>
</ListItemIcon>
</ListItem>
);
}
};
render() {
return (
<List>
{this.props.networkList.networks.map(this.renderNetwork)}
</List>
<List>{this.props.networkList.networks.map(this.renderNetwork)}</List>
);
}
}
export default WiFiNetworkSelector;

View File

@@ -1,23 +1,23 @@
import { WiFiNetwork, WiFiEncryptionType } from './types'
import { WiFiNetwork, WiFiEncryptionType } from './types';
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
switch (encryption_type) {
case WiFiEncryptionType.WIFI_AUTH_WEP:
return 'WEP'
return 'WEP';
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
return 'WPA'
return 'WPA';
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
return 'WPA2'
return 'WPA2';
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
return 'WPA/WPA2'
return 'WPA/WPA2';
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
return 'WPA2 Enterprise'
return 'WPA2 Enterprise';
case WiFiEncryptionType.WIFI_AUTH_OPEN:
return 'None'
return 'None';
default:
return 'Unknown'
return 'Unknown';
}
}
};

View File

@@ -6,7 +6,7 @@ export enum NetworkConnectionStatus {
WIFI_STATUS_CONNECTION_LOST = 5,
WIFI_STATUS_DISCONNECTED = 6,
ETHERNET_STATUS_CONNECTED = 10,
WIFI_STATUS_NO_SHIELD = 255,
WIFI_STATUS_NO_SHIELD = 255
}
export enum WiFiEncryptionType {
@@ -15,43 +15,43 @@ export enum WiFiEncryptionType {
WIFI_AUTH_WPA_PSK = 2,
WIFI_AUTH_WPA2_PSK = 3,
WIFI_AUTH_WPA_WPA2_PSK = 4,
WIFI_AUTH_WPA2_ENTERPRISE = 5,
WIFI_AUTH_WPA2_ENTERPRISE = 5
}
export interface NetworkStatus {
status: NetworkConnectionStatus
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
status: NetworkConnectionStatus;
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 NetworkSettings {
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
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[]
networks: WiFiNetwork[];
}
export interface WiFiNetwork {
rssi: number
ssid: string
bssid: string
channel: number
encryption_type: WiFiEncryptionType
rssi: number;
ssid: string;
bssid: string;
channel: number;
encryption_type: WiFiEncryptionType;
}

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { NTP_SETTINGS_ENDPOINT } from '../api';
import NTPSettingsForm from './NTPSettingsForm';
@@ -9,7 +14,6 @@ import { NTPSettings } from './types';
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class NTPSettingsController extends Component<NTPSettingsControllerProps> {
<SectionContent title="NTP Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <NTPSettingsForm {...formProps} />}
render={(formProps) => <NTPSettingsForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);

View File

@@ -1,10 +1,19 @@
import React from 'react';
import { TextValidator, ValidatorForm, SelectValidator } from 'react-material-ui-form-validator';
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 {
RestFormProps,
FormActions,
FormButton,
BlockFormControlLabel
} from '../components';
import { isIP, isHostname, or } from '../validators';
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
@@ -13,7 +22,6 @@ import { NTPSettings } from './types';
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
}
@@ -25,7 +33,7 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value]
});
}
};
render() {
const { data, handleValueChange, saveData } = this.props;
@@ -43,7 +51,10 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
/>
<TextValidator
validators={['required', 'isIPOrHostname']}
errorMessages={['Server is required', "Not a valid IP address or hostname"]}
errorMessages={[
'Server is required',
'Not a valid IP address or hostname'
]}
name="server"
label="Server"
fullWidth
@@ -68,7 +79,12 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
{timeZoneSelectItems()}
</SelectValidator>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -1,27 +1,27 @@
import { Theme } from '@material-ui/core'
import { NTPStatus, NTPSyncStatus } from './types'
import { Theme } from '@material-ui/core';
import { NTPStatus, NTPSyncStatus } from './types';
export const isNtpActive = ({ status }: NTPStatus) =>
status === NTPSyncStatus.NTP_ACTIVE
status === NTPSyncStatus.NTP_ACTIVE;
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
switch (status) {
case NTPSyncStatus.NTP_INACTIVE:
return theme.palette.info.main
return theme.palette.info.main;
case NTPSyncStatus.NTP_ACTIVE:
return theme.palette.success.main
return theme.palette.success.main;
default:
return theme.palette.error.main
return theme.palette.error.main;
}
}
};
export const ntpStatus = ({ status }: NTPStatus) => {
switch (status) {
case NTPSyncStatus.NTP_INACTIVE:
return 'Inactive'
return 'Inactive';
case NTPSyncStatus.NTP_ACTIVE:
return 'Active'
return 'Active';
default:
return 'Unknown'
return 'Unknown';
}
}
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { NTP_STATUS_ENDPOINT } from '../api';
import NTPStatusForm from './NTPStatusForm';
@@ -9,7 +14,6 @@ import { NTPStatus } from './types';
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
class NTPStatusController extends Component<NTPStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class NTPStatusController extends Component<NTPStatusControllerProps> {
<SectionContent title="NTP Status">
<RestFormLoader
{...this.props}
render={formProps => <NTPStatusForm {...formProps} />}
render={(formProps) => <NTPStatusForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);

View File

@@ -1,8 +1,23 @@
import React, { Component, Fragment } from 'react';
import { WithTheme, withTheme } from '@material-ui/core/styles';
import { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
import {
Avatar,
Divider,
List,
ListItem,
ListItemAvatar,
ListItemText,
Button
} from '@material-ui/core';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
TextField
} from '@material-ui/core';
import SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
import AccessTimeIcon from '@material-ui/icons/AccessTime';
@@ -13,12 +28,22 @@ import RefreshIcon from '@material-ui/icons/Refresh';
import { RestFormProps, FormButton, HighlightAvatar } from '../components';
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
import { formatDuration, formatDateTime, formatLocalDateTime } from './TimeFormat';
import {
formatDuration,
formatDateTime,
formatLocalDateTime
} from './TimeFormat';
import { NTPStatus, Time } from './types';
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import {
redirectingAuthorizedFetch,
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import { TIME_ENDPOINT } from '../api';
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
type NTPStatusFormProps = RestFormProps<NTPStatus> &
WithTheme &
AuthenticatedContextProps;
interface NTPStatusFormState {
settingTime: boolean;
@@ -27,7 +52,6 @@ interface NTPStatusFormState {
}
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
constructor(props: NTPStatusFormProps) {
super(props);
this.state = {
@@ -41,20 +65,20 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
this.setState({
localTime: event.target.value
});
}
};
openSetTime = () => {
this.setState({
localTime: formatLocalDateTime(new Date()),
settingTime: true
});
}
};
closeSetTime = () => {
this.setState({
settingTime: false
});
}
};
createTime = (): Time => ({
local_time: formatLocalDateTime(new Date(this.state.localTime))
@@ -62,27 +86,34 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
configureTime = () => {
this.setState({ processing: true });
redirectingAuthorizedFetch(TIME_ENDPOINT,
{
redirectingAuthorizedFetch(TIME_ENDPOINT, {
method: 'POST',
body: JSON.stringify(this.createTime()),
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
this.setState({ processing: false, settingTime: false }, this.props.loadData);
this.props.enqueueSnackbar('Time set successfully', {
variant: 'success'
});
this.setState(
{ processing: false, settingTime: false },
this.props.loadData
);
} else {
throw Error("Error setting time, status code: " + response.status);
throw Error('Error setting time, status code: ' + response.status);
}
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem setting the time',
{ variant: 'error' }
);
this.setState({ processing: false, settingTime: false });
});
}
};
renderSetTimeDialog() {
return (
@@ -94,7 +125,9 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
>
<DialogTitle>Set Time</DialogTitle>
<DialogContent dividers>
<Box mb={2}>Enter local date and time below to set the device's time.</Box>
<Box mb={2}>
Enter local date and time below to set the device's time.
</Box>
<TextField
label="Local Time"
type="datetime-local"
@@ -104,24 +137,35 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
variant="outlined"
fullWidth
InputLabelProps={{
shrink: true,
shrink: true
}}
/>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
<Button
variant="contained"
onClick={this.closeSetTime}
color="secondary"
>
Cancel
</Button>
<Button startIcon={<AccessTimeIcon />} variant="contained" onClick={this.configureTime} disabled={this.state.processing} color="primary" autoFocus>
<Button
startIcon={<AccessTimeIcon />}
variant="contained"
onClick={this.configureTime}
disabled={this.state.processing}
color="primary"
autoFocus
>
Set Time
</Button>
</DialogActions>
</Dialog>
)
);
}
render() {
const { data, theme } = this.props
const { data, theme } = this.props;
const me = this.props.authenticatedContext.me;
return (
<Fragment>
@@ -154,7 +198,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
<AccessTimeIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
<ListItemText
primary="Local Time"
secondary={formatDateTime(data.local_time)}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -163,7 +210,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
<SwapVerticalCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
<ListItemText
primary="UTC Time"
secondary={formatDateTime(data.utc_time)}
/>
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
@@ -172,19 +222,32 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
<AvTimerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Uptime" secondary={formatDuration(data.uptime)} />
<ListItemText
primary="Uptime"
secondary={formatDuration(data.uptime)}
/>
</ListItem>
<Divider variant="inset" component="li" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} padding={1}>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
Refresh
</FormButton>
</Box>
{me.admin && !isNtpActive(data) && (
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<Button onClick={this.openSetTime} variant="contained" color="primary" startIcon={<AccessTimeIcon />}>
<Button
onClick={this.openSetTime}
variant="contained"
color="primary"
startIcon={<AccessTimeIcon />}
>
Set Time
</Button>
</Box>

View File

@@ -1,9 +1,13 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { 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 {
withAuthenticatedContext,
AuthenticatedContextProps,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import NTPStatusController from './NTPStatusController';
@@ -12,8 +16,7 @@ import NTPSettingsController from './NTPSettingsController';
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
class NetworkTime extends Component<NetworkTimeProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -21,19 +24,34 @@ class NetworkTime extends Component<NetworkTimeProps> {
const { authenticatedContext } = this.props;
return (
<MenuAppBar sectionTitle="Network Time">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/ntp/status" label="NTP Status" />
<Tab value="/ntp/settings" label="NTP Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/ntp/settings"
label="NTP Settings"
disabled={!authenticatedContext.me.admin}
/>
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/ntp/status" component={NTPStatusController} />
<AuthenticatedRoute exact path="/ntp/settings" component={NTPSettingsController} />
<AuthenticatedRoute
exact
path="/ntp/status"
component={NTPStatusController}
/>
<AuthenticatedRoute
exact
path="/ntp/settings"
component={NTPSettingsController}
/>
<Redirect to="/ntp/status" />
</Switch>
</MenuAppBar>
)
);
}
}
export default withAuthenticatedContext(NetworkTime)
export default withAuthenticatedContext(NetworkTime);

View File

@@ -1,479 +1,480 @@
import React from 'react';
import MenuItem from '@material-ui/core/MenuItem';
type TimeZones = {
[name: string]: string
[name: string]: string;
};
export const TIME_ZONES: TimeZones = {
"Africa/Abidjan": "GMT0",
"Africa/Accra": "GMT0",
"Africa/Addis_Ababa": "EAT-3",
"Africa/Algiers": "CET-1",
"Africa/Asmara": "EAT-3",
"Africa/Bamako": "GMT0",
"Africa/Bangui": "WAT-1",
"Africa/Banjul": "GMT0",
"Africa/Bissau": "GMT0",
"Africa/Blantyre": "CAT-2",
"Africa/Brazzaville": "WAT-1",
"Africa/Bujumbura": "CAT-2",
"Africa/Cairo": "EET-2",
"Africa/Casablanca": "UNK-1",
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Africa/Conakry": "GMT0",
"Africa/Dakar": "GMT0",
"Africa/Dar_es_Salaam": "EAT-3",
"Africa/Djibouti": "EAT-3",
"Africa/Douala": "WAT-1",
"Africa/El_Aaiun": "UNK-1",
"Africa/Freetown": "GMT0",
"Africa/Gaborone": "CAT-2",
"Africa/Harare": "CAT-2",
"Africa/Johannesburg": "SAST-2",
"Africa/Juba": "EAT-3",
"Africa/Kampala": "EAT-3",
"Africa/Khartoum": "CAT-2",
"Africa/Kigali": "CAT-2",
"Africa/Kinshasa": "WAT-1",
"Africa/Lagos": "WAT-1",
"Africa/Libreville": "WAT-1",
"Africa/Lome": "GMT0",
"Africa/Luanda": "WAT-1",
"Africa/Lubumbashi": "CAT-2",
"Africa/Lusaka": "CAT-2",
"Africa/Malabo": "WAT-1",
"Africa/Maputo": "CAT-2",
"Africa/Maseru": "SAST-2",
"Africa/Mbabane": "SAST-2",
"Africa/Mogadishu": "EAT-3",
"Africa/Monrovia": "GMT0",
"Africa/Nairobi": "EAT-3",
"Africa/Ndjamena": "WAT-1",
"Africa/Niamey": "WAT-1",
"Africa/Nouakchott": "GMT0",
"Africa/Ouagadougou": "GMT0",
"Africa/Porto-Novo": "WAT-1",
"Africa/Sao_Tome": "GMT0",
"Africa/Tripoli": "EET-2",
"Africa/Tunis": "CET-1",
"Africa/Windhoek": "CAT-2",
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Anguilla": "AST4",
"America/Antigua": "AST4",
"America/Araguaina": "UNK3",
"America/Argentina/Buenos_Aires": "UNK3",
"America/Argentina/Catamarca": "UNK3",
"America/Argentina/Cordoba": "UNK3",
"America/Argentina/Jujuy": "UNK3",
"America/Argentina/La_Rioja": "UNK3",
"America/Argentina/Mendoza": "UNK3",
"America/Argentina/Rio_Gallegos": "UNK3",
"America/Argentina/Salta": "UNK3",
"America/Argentina/San_Juan": "UNK3",
"America/Argentina/San_Luis": "UNK3",
"America/Argentina/Tucuman": "UNK3",
"America/Argentina/Ushuaia": "UNK3",
"America/Aruba": "AST4",
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
"America/Atikokan": "EST5",
"America/Bahia": "UNK3",
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
"America/Barbados": "AST4",
"America/Belem": "UNK3",
"America/Belize": "CST6",
"America/Blanc-Sablon": "AST4",
"America/Boa_Vista": "UNK4",
"America/Bogota": "UNK5",
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
"America/Campo_Grande": "UNK4",
"America/Cancun": "EST5",
"America/Caracas": "UNK4",
"America/Cayenne": "UNK3",
"America/Cayman": "EST5",
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
"America/Costa_Rica": "CST6",
"America/Creston": "MST7",
"America/Cuiaba": "UNK4",
"America/Curacao": "AST4",
"America/Danmarkshavn": "GMT0",
"America/Dawson": "MST7",
"America/Dawson_Creek": "MST7",
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
"America/Dominica": "AST4",
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
"America/Eirunepe": "UNK5",
"America/El_Salvador": "CST6",
"America/Fort_Nelson": "MST7",
"America/Fortaleza": "UNK3",
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
"America/Grenada": "AST4",
"America/Guadeloupe": "AST4",
"America/Guatemala": "CST6",
"America/Guayaquil": "UNK5",
"America/Guyana": "UNK4",
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
"America/Hermosillo": "MST7",
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
"America/Jamaica": "EST5",
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
"America/Kralendijk": "AST4",
"America/La_Paz": "UNK4",
"America/Lima": "UNK5",
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
"America/Lower_Princes": "AST4",
"America/Maceio": "UNK3",
"America/Managua": "CST6",
"America/Manaus": "UNK4",
"America/Marigot": "AST4",
"America/Martinique": "AST4",
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
"America/Montevideo": "UNK3",
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
"America/Montserrat": "AST4",
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Noronha": "UNK2",
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
"America/Panama": "EST5",
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
"America/Paramaribo": "UNK3",
"America/Phoenix": "MST7",
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
"America/Port_of_Spain": "AST4",
"America/Porto_Velho": "UNK4",
"America/Puerto_Rico": "AST4",
"America/Punta_Arenas": "UNK3",
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
"America/Recife": "UNK3",
"America/Regina": "CST6",
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
"America/Rio_Branco": "UNK5",
"America/Santarem": "UNK3",
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo": "AST4",
"America/Sao_Paulo": "UNK3",
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy": "AST4",
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
"America/St_Kitts": "AST4",
"America/St_Lucia": "AST4",
"America/St_Thomas": "AST4",
"America/St_Vincent": "AST4",
"America/Swift_Current": "CST6",
"America/Tegucigalpa": "CST6",
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
"America/Tortola": "AST4",
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
"America/Whitehorse": "MST7",
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey": "UNK-8",
"Antarctica/Davis": "UNK-7",
"Antarctica/DumontDUrville": "UNK-10",
"Antarctica/Macquarie": "UNK-11",
"Antarctica/Mawson": "UNK-5",
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Antarctica/Palmer": "UNK3",
"Antarctica/Rothera": "UNK3",
"Antarctica/Syowa": "UNK-3",
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok": "UNK-6",
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden": "UNK-3",
"Asia/Almaty": "UNK-6",
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
"Asia/Anadyr": "UNK-12",
"Asia/Aqtau": "UNK-5",
"Asia/Aqtobe": "UNK-5",
"Asia/Ashgabat": "UNK-5",
"Asia/Atyrau": "UNK-5",
"Asia/Baghdad": "UNK-3",
"Asia/Bahrain": "UNK-3",
"Asia/Baku": "UNK-4",
"Asia/Bangkok": "UNK-7",
"Asia/Barnaul": "UNK-7",
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
"Asia/Bishkek": "UNK-6",
"Asia/Brunei": "UNK-8",
"Asia/Chita": "UNK-9",
"Asia/Choibalsan": "UNK-8",
"Asia/Colombo": "UNK-5:30",
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
"Asia/Dhaka": "UNK-6",
"Asia/Dili": "UNK-9",
"Asia/Dubai": "UNK-4",
"Asia/Dushanbe": "UNK-5",
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Ho_Chi_Minh": "UNK-7",
"Asia/Hong_Kong": "HKT-8",
"Asia/Hovd": "UNK-7",
"Asia/Irkutsk": "UNK-8",
"Asia/Jakarta": "WIB-7",
"Asia/Jayapura": "WIT-9",
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
"Asia/Kabul": "UNK-4:30",
"Asia/Kamchatka": "UNK-12",
"Asia/Karachi": "PKT-5",
"Asia/Kathmandu": "UNK-5:45",
"Asia/Khandyga": "UNK-9",
"Asia/Kolkata": "IST-5:30",
"Asia/Krasnoyarsk": "UNK-7",
"Asia/Kuala_Lumpur": "UNK-8",
"Asia/Kuching": "UNK-8",
"Asia/Kuwait": "UNK-3",
"Asia/Macau": "CST-8",
"Asia/Magadan": "UNK-11",
"Asia/Makassar": "WITA-8",
"Asia/Manila": "PST-8",
"Asia/Muscat": "UNK-4",
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Novokuznetsk": "UNK-7",
"Asia/Novosibirsk": "UNK-7",
"Asia/Omsk": "UNK-6",
"Asia/Oral": "UNK-5",
"Asia/Phnom_Penh": "UNK-7",
"Asia/Pontianak": "WIB-7",
"Asia/Pyongyang": "KST-9",
"Asia/Qatar": "UNK-3",
"Asia/Qyzylorda": "UNK-5",
"Asia/Riyadh": "UNK-3",
"Asia/Sakhalin": "UNK-11",
"Asia/Samarkand": "UNK-5",
"Asia/Seoul": "KST-9",
"Asia/Shanghai": "CST-8",
"Asia/Singapore": "UNK-8",
"Asia/Srednekolymsk": "UNK-11",
"Asia/Taipei": "CST-8",
"Asia/Tashkent": "UNK-5",
"Asia/Tbilisi": "UNK-4",
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
"Asia/Thimphu": "UNK-6",
"Asia/Tokyo": "JST-9",
"Asia/Tomsk": "UNK-7",
"Asia/Ulaanbaatar": "UNK-8",
"Asia/Urumqi": "UNK-6",
"Asia/Ust-Nera": "UNK-10",
"Asia/Vientiane": "UNK-7",
"Asia/Vladivostok": "UNK-10",
"Asia/Yakutsk": "UNK-9",
"Asia/Yangon": "UNK-6:30",
"Asia/Yekaterinburg": "UNK-5",
"Asia/Yerevan": "UNK-4",
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Cape_Verde": "UNK1",
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Reykjavik": "GMT0",
"Atlantic/South_Georgia": "UNK2",
"Atlantic/St_Helena": "GMT0",
"Atlantic/Stanley": "UNK3",
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Brisbane": "AEST-10",
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Darwin": "ACST-9:30",
"Australia/Eucla": "UNK-8:45",
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Lindeman": "AEST-10",
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Perth": "AWST-8",
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Etc/GMT": "GMT0",
"Etc/GMT+0": "GMT0",
"Etc/GMT+1": "UNK1",
"Etc/GMT+10": "UNK10",
"Etc/GMT+11": "UNK11",
"Etc/GMT+12": "UNK12",
"Etc/GMT+2": "UNK2",
"Etc/GMT+3": "UNK3",
"Etc/GMT+4": "UNK4",
"Etc/GMT+5": "UNK5",
"Etc/GMT+6": "UNK6",
"Etc/GMT+7": "UNK7",
"Etc/GMT+8": "UNK8",
"Etc/GMT+9": "UNK9",
"Etc/GMT-0": "GMT0",
"Etc/GMT-1": "UNK-1",
"Etc/GMT-10": "UNK-10",
"Etc/GMT-11": "UNK-11",
"Etc/GMT-12": "UNK-12",
"Etc/GMT-13": "UNK-13",
"Etc/GMT-14": "UNK-14",
"Etc/GMT-2": "UNK-2",
"Etc/GMT-3": "UNK-3",
"Etc/GMT-4": "UNK-4",
"Etc/GMT-5": "UNK-5",
"Etc/GMT-6": "UNK-6",
"Etc/GMT-7": "UNK-7",
"Etc/GMT-8": "UNK-8",
"Etc/GMT-9": "UNK-9",
"Etc/GMT0": "GMT0",
"Etc/Greenwich": "GMT0",
"Etc/UCT": "UTC0",
"Etc/UTC": "UTC0",
"Etc/Universal": "UTC0",
"Etc/Zulu": "UTC0",
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Astrakhan": "UNK-4",
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Istanbul": "UNK-3",
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Kaliningrad": "EET-2",
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Kirov": "UNK-3",
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Minsk": "UNK-3",
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Moscow": "MSK-3",
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Samara": "UNK-4",
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Saratov": "UNK-4",
"Europe/Simferopol": "MSK-3",
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Ulyanovsk": "UNK-4",
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Volgograd": "UNK-4",
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
"Indian/Antananarivo": "EAT-3",
"Indian/Chagos": "UNK-6",
"Indian/Christmas": "UNK-7",
"Indian/Cocos": "UNK-6:30",
"Indian/Comoro": "EAT-3",
"Indian/Kerguelen": "UNK-5",
"Indian/Mahe": "UNK-4",
"Indian/Maldives": "UNK-5",
"Indian/Mauritius": "UNK-4",
"Indian/Mayotte": "EAT-3",
"Indian/Reunion": "UNK-4",
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Pacific/Bougainville": "UNK-11",
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
"Pacific/Chuuk": "UNK-10",
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
"Pacific/Efate": "UNK-11",
"Pacific/Enderbury": "UNK-13",
"Pacific/Fakaofo": "UNK-13",
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
"Pacific/Funafuti": "UNK-12",
"Pacific/Galapagos": "UNK6",
"Pacific/Gambier": "UNK9",
"Pacific/Guadalcanal": "UNK-11",
"Pacific/Guam": "ChST-10",
"Pacific/Honolulu": "HST10",
"Pacific/Kiritimati": "UNK-14",
"Pacific/Kosrae": "UNK-11",
"Pacific/Kwajalein": "UNK-12",
"Pacific/Majuro": "UNK-12",
"Pacific/Marquesas": "UNK9:30",
"Pacific/Midway": "SST11",
"Pacific/Nauru": "UNK-12",
"Pacific/Niue": "UNK11",
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
"Pacific/Noumea": "UNK-11",
"Pacific/Pago_Pago": "SST11",
"Pacific/Palau": "UNK-9",
"Pacific/Pitcairn": "UNK8",
"Pacific/Pohnpei": "UNK-11",
"Pacific/Port_Moresby": "UNK-10",
"Pacific/Rarotonga": "UNK10",
"Pacific/Saipan": "ChST-10",
"Pacific/Tahiti": "UNK10",
"Pacific/Tarawa": "UNK-12",
"Pacific/Tongatapu": "UNK-13",
"Pacific/Wake": "UNK-12",
"Pacific/Wallis": "UNK-12"
}
'Africa/Abidjan': 'GMT0',
'Africa/Accra': 'GMT0',
'Africa/Addis_Ababa': 'EAT-3',
'Africa/Algiers': 'CET-1',
'Africa/Asmara': 'EAT-3',
'Africa/Bamako': 'GMT0',
'Africa/Bangui': 'WAT-1',
'Africa/Banjul': 'GMT0',
'Africa/Bissau': 'GMT0',
'Africa/Blantyre': 'CAT-2',
'Africa/Brazzaville': 'WAT-1',
'Africa/Bujumbura': 'CAT-2',
'Africa/Cairo': 'EET-2',
'Africa/Casablanca': 'UNK-1',
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Africa/Conakry': 'GMT0',
'Africa/Dakar': 'GMT0',
'Africa/Dar_es_Salaam': 'EAT-3',
'Africa/Djibouti': 'EAT-3',
'Africa/Douala': 'WAT-1',
'Africa/El_Aaiun': 'UNK-1',
'Africa/Freetown': 'GMT0',
'Africa/Gaborone': 'CAT-2',
'Africa/Harare': 'CAT-2',
'Africa/Johannesburg': 'SAST-2',
'Africa/Juba': 'EAT-3',
'Africa/Kampala': 'EAT-3',
'Africa/Khartoum': 'CAT-2',
'Africa/Kigali': 'CAT-2',
'Africa/Kinshasa': 'WAT-1',
'Africa/Lagos': 'WAT-1',
'Africa/Libreville': 'WAT-1',
'Africa/Lome': 'GMT0',
'Africa/Luanda': 'WAT-1',
'Africa/Lubumbashi': 'CAT-2',
'Africa/Lusaka': 'CAT-2',
'Africa/Malabo': 'WAT-1',
'Africa/Maputo': 'CAT-2',
'Africa/Maseru': 'SAST-2',
'Africa/Mbabane': 'SAST-2',
'Africa/Mogadishu': 'EAT-3',
'Africa/Monrovia': 'GMT0',
'Africa/Nairobi': 'EAT-3',
'Africa/Ndjamena': 'WAT-1',
'Africa/Niamey': 'WAT-1',
'Africa/Nouakchott': 'GMT0',
'Africa/Ouagadougou': 'GMT0',
'Africa/Porto-Novo': 'WAT-1',
'Africa/Sao_Tome': 'GMT0',
'Africa/Tripoli': 'EET-2',
'Africa/Tunis': 'CET-1',
'Africa/Windhoek': 'CAT-2',
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Anguilla': 'AST4',
'America/Antigua': 'AST4',
'America/Araguaina': 'UNK3',
'America/Argentina/Buenos_Aires': 'UNK3',
'America/Argentina/Catamarca': 'UNK3',
'America/Argentina/Cordoba': 'UNK3',
'America/Argentina/Jujuy': 'UNK3',
'America/Argentina/La_Rioja': 'UNK3',
'America/Argentina/Mendoza': 'UNK3',
'America/Argentina/Rio_Gallegos': 'UNK3',
'America/Argentina/Salta': 'UNK3',
'America/Argentina/San_Juan': 'UNK3',
'America/Argentina/San_Luis': 'UNK3',
'America/Argentina/Tucuman': 'UNK3',
'America/Argentina/Ushuaia': 'UNK3',
'America/Aruba': 'AST4',
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
'America/Atikokan': 'EST5',
'America/Bahia': 'UNK3',
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
'America/Barbados': 'AST4',
'America/Belem': 'UNK3',
'America/Belize': 'CST6',
'America/Blanc-Sablon': 'AST4',
'America/Boa_Vista': 'UNK4',
'America/Bogota': 'UNK5',
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
'America/Campo_Grande': 'UNK4',
'America/Cancun': 'EST5',
'America/Caracas': 'UNK4',
'America/Cayenne': 'UNK3',
'America/Cayman': 'EST5',
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
'America/Costa_Rica': 'CST6',
'America/Creston': 'MST7',
'America/Cuiaba': 'UNK4',
'America/Curacao': 'AST4',
'America/Danmarkshavn': 'GMT0',
'America/Dawson': 'MST7',
'America/Dawson_Creek': 'MST7',
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
'America/Dominica': 'AST4',
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
'America/Eirunepe': 'UNK5',
'America/El_Salvador': 'CST6',
'America/Fort_Nelson': 'MST7',
'America/Fortaleza': 'UNK3',
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
'America/Grenada': 'AST4',
'America/Guadeloupe': 'AST4',
'America/Guatemala': 'CST6',
'America/Guayaquil': 'UNK5',
'America/Guyana': 'UNK4',
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
'America/Hermosillo': 'MST7',
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Knox': 'CST6CDT,M3.2.0,M11.1.0',
'America/Indiana/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Petersburg': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
'America/Indiana/Vevay': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
'America/Indiana/Winamac': 'EST5EDT,M3.2.0,M11.1.0',
'America/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
'America/Jamaica': 'EST5',
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Kentucky/Louisville': 'EST5EDT,M3.2.0,M11.1.0',
'America/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
'America/Kralendijk': 'AST4',
'America/La_Paz': 'UNK4',
'America/Lima': 'UNK5',
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
'America/Lower_Princes': 'AST4',
'America/Maceio': 'UNK3',
'America/Managua': 'CST6',
'America/Manaus': 'UNK4',
'America/Marigot': 'AST4',
'America/Martinique': 'AST4',
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
'America/Moncton': 'AST4ADT,M3.2.0,M11.1.0',
'America/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
'America/Montevideo': 'UNK3',
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
'America/Montserrat': 'AST4',
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
'America/New_York': 'EST5EDT,M3.2.0,M11.1.0',
'America/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Noronha': 'UNK2',
'America/North_Dakota/Beulah': 'CST6CDT,M3.2.0,M11.1.0',
'America/North_Dakota/Center': 'CST6CDT,M3.2.0,M11.1.0',
'America/North_Dakota/New_Salem': 'CST6CDT,M3.2.0,M11.1.0',
'America/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
'America/Panama': 'EST5',
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
'America/Paramaribo': 'UNK3',
'America/Phoenix': 'MST7',
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
'America/Port_of_Spain': 'AST4',
'America/Porto_Velho': 'UNK4',
'America/Puerto_Rico': 'AST4',
'America/Punta_Arenas': 'UNK3',
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
'America/Recife': 'UNK3',
'America/Regina': 'CST6',
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
'America/Rio_Branco': 'UNK5',
'America/Santarem': 'UNK3',
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
'America/Santo_Domingo': 'AST4',
'America/Sao_Paulo': 'UNK3',
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/St_Barthelemy': 'AST4',
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
'America/St_Kitts': 'AST4',
'America/St_Lucia': 'AST4',
'America/St_Thomas': 'AST4',
'America/St_Vincent': 'AST4',
'America/Swift_Current': 'CST6',
'America/Tegucigalpa': 'CST6',
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
'America/Thunder_Bay': 'EST5EDT,M3.2.0,M11.1.0',
'America/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
'America/Tortola': 'AST4',
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
'America/Whitehorse': 'MST7',
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
'Antarctica/Casey': 'UNK-8',
'Antarctica/Davis': 'UNK-7',
'Antarctica/DumontDUrville': 'UNK-10',
'Antarctica/Macquarie': 'UNK-11',
'Antarctica/Mawson': 'UNK-5',
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
'Antarctica/Palmer': 'UNK3',
'Antarctica/Rothera': 'UNK3',
'Antarctica/Syowa': 'UNK-3',
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
'Antarctica/Vostok': 'UNK-6',
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Asia/Aden': 'UNK-3',
'Asia/Almaty': 'UNK-6',
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
'Asia/Anadyr': 'UNK-12',
'Asia/Aqtau': 'UNK-5',
'Asia/Aqtobe': 'UNK-5',
'Asia/Ashgabat': 'UNK-5',
'Asia/Atyrau': 'UNK-5',
'Asia/Baghdad': 'UNK-3',
'Asia/Bahrain': 'UNK-3',
'Asia/Baku': 'UNK-4',
'Asia/Bangkok': 'UNK-7',
'Asia/Barnaul': 'UNK-7',
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
'Asia/Bishkek': 'UNK-6',
'Asia/Brunei': 'UNK-8',
'Asia/Chita': 'UNK-9',
'Asia/Choibalsan': 'UNK-8',
'Asia/Colombo': 'UNK-5:30',
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
'Asia/Dhaka': 'UNK-6',
'Asia/Dili': 'UNK-9',
'Asia/Dubai': 'UNK-4',
'Asia/Dushanbe': 'UNK-5',
'Asia/Famagusta': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Asia/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
'Asia/Hebron': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
'Asia/Ho_Chi_Minh': 'UNK-7',
'Asia/Hong_Kong': 'HKT-8',
'Asia/Hovd': 'UNK-7',
'Asia/Irkutsk': 'UNK-8',
'Asia/Jakarta': 'WIB-7',
'Asia/Jayapura': 'WIT-9',
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
'Asia/Kabul': 'UNK-4:30',
'Asia/Kamchatka': 'UNK-12',
'Asia/Karachi': 'PKT-5',
'Asia/Kathmandu': 'UNK-5:45',
'Asia/Khandyga': 'UNK-9',
'Asia/Kolkata': 'IST-5:30',
'Asia/Krasnoyarsk': 'UNK-7',
'Asia/Kuala_Lumpur': 'UNK-8',
'Asia/Kuching': 'UNK-8',
'Asia/Kuwait': 'UNK-3',
'Asia/Macau': 'CST-8',
'Asia/Magadan': 'UNK-11',
'Asia/Makassar': 'WITA-8',
'Asia/Manila': 'PST-8',
'Asia/Muscat': 'UNK-4',
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Asia/Novokuznetsk': 'UNK-7',
'Asia/Novosibirsk': 'UNK-7',
'Asia/Omsk': 'UNK-6',
'Asia/Oral': 'UNK-5',
'Asia/Phnom_Penh': 'UNK-7',
'Asia/Pontianak': 'WIB-7',
'Asia/Pyongyang': 'KST-9',
'Asia/Qatar': 'UNK-3',
'Asia/Qyzylorda': 'UNK-5',
'Asia/Riyadh': 'UNK-3',
'Asia/Sakhalin': 'UNK-11',
'Asia/Samarkand': 'UNK-5',
'Asia/Seoul': 'KST-9',
'Asia/Shanghai': 'CST-8',
'Asia/Singapore': 'UNK-8',
'Asia/Srednekolymsk': 'UNK-11',
'Asia/Taipei': 'CST-8',
'Asia/Tashkent': 'UNK-5',
'Asia/Tbilisi': 'UNK-4',
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
'Asia/Thimphu': 'UNK-6',
'Asia/Tokyo': 'JST-9',
'Asia/Tomsk': 'UNK-7',
'Asia/Ulaanbaatar': 'UNK-8',
'Asia/Urumqi': 'UNK-6',
'Asia/Ust-Nera': 'UNK-10',
'Asia/Vientiane': 'UNK-7',
'Asia/Vladivostok': 'UNK-10',
'Asia/Yakutsk': 'UNK-9',
'Asia/Yangon': 'UNK-6:30',
'Asia/Yekaterinburg': 'UNK-5',
'Asia/Yerevan': 'UNK-4',
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Cape_Verde': 'UNK1',
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
'Atlantic/Reykjavik': 'GMT0',
'Atlantic/South_Georgia': 'UNK2',
'Atlantic/St_Helena': 'GMT0',
'Atlantic/Stanley': 'UNK3',
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
'Australia/Brisbane': 'AEST-10',
'Australia/Broken_Hill': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
'Australia/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Darwin': 'ACST-9:30',
'Australia/Eucla': 'UNK-8:45',
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Lindeman': 'AEST-10',
'Australia/Lord_Howe': 'UNK-10:30UNK-11,M10.1.0,M4.1.0',
'Australia/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Australia/Perth': 'AWST-8',
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
'Etc/GMT': 'GMT0',
'Etc/GMT+0': 'GMT0',
'Etc/GMT+1': 'UNK1',
'Etc/GMT+10': 'UNK10',
'Etc/GMT+11': 'UNK11',
'Etc/GMT+12': 'UNK12',
'Etc/GMT+2': 'UNK2',
'Etc/GMT+3': 'UNK3',
'Etc/GMT+4': 'UNK4',
'Etc/GMT+5': 'UNK5',
'Etc/GMT+6': 'UNK6',
'Etc/GMT+7': 'UNK7',
'Etc/GMT+8': 'UNK8',
'Etc/GMT+9': 'UNK9',
'Etc/GMT-0': 'GMT0',
'Etc/GMT-1': 'UNK-1',
'Etc/GMT-10': 'UNK-10',
'Etc/GMT-11': 'UNK-11',
'Etc/GMT-12': 'UNK-12',
'Etc/GMT-13': 'UNK-13',
'Etc/GMT-14': 'UNK-14',
'Etc/GMT-2': 'UNK-2',
'Etc/GMT-3': 'UNK-3',
'Etc/GMT-4': 'UNK-4',
'Etc/GMT-5': 'UNK-5',
'Etc/GMT-6': 'UNK-6',
'Etc/GMT-7': 'UNK-7',
'Etc/GMT-8': 'UNK-8',
'Etc/GMT-9': 'UNK-9',
'Etc/GMT0': 'GMT0',
'Etc/Greenwich': 'GMT0',
'Etc/UCT': 'UTC0',
'Etc/UTC': 'UTC0',
'Etc/Universal': 'UTC0',
'Etc/Zulu': 'UTC0',
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Andorra': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Astrakhan': 'UNK-4',
'Europe/Athens': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Berlin': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Brussels': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Busingen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
'Europe/Copenhagen': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Helsinki': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Istanbul': 'UNK-3',
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Kaliningrad': 'EET-2',
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Kirov': 'UNK-3',
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Madrid': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Minsk': 'UNK-3',
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Moscow': 'MSK-3',
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Paris': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Prague': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Samara': 'UNK-4',
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Sarajevo': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Saratov': 'UNK-4',
'Europe/Simferopol': 'MSK-3',
'Europe/Skopje': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Ulyanovsk': 'UNK-4',
'Europe/Uzhgorod': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vatican': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Volgograd': 'UNK-4',
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Zagreb': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Europe/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
'Indian/Antananarivo': 'EAT-3',
'Indian/Chagos': 'UNK-6',
'Indian/Christmas': 'UNK-7',
'Indian/Cocos': 'UNK-6:30',
'Indian/Comoro': 'EAT-3',
'Indian/Kerguelen': 'UNK-5',
'Indian/Mahe': 'UNK-4',
'Indian/Maldives': 'UNK-5',
'Indian/Mauritius': 'UNK-4',
'Indian/Mayotte': 'EAT-3',
'Indian/Reunion': 'UNK-4',
'Pacific/Apia': 'UNK-13UNK,M9.5.0/3,M4.1.0/4',
'Pacific/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
'Pacific/Bougainville': 'UNK-11',
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
'Pacific/Chuuk': 'UNK-10',
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
'Pacific/Efate': 'UNK-11',
'Pacific/Enderbury': 'UNK-13',
'Pacific/Fakaofo': 'UNK-13',
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
'Pacific/Funafuti': 'UNK-12',
'Pacific/Galapagos': 'UNK6',
'Pacific/Gambier': 'UNK9',
'Pacific/Guadalcanal': 'UNK-11',
'Pacific/Guam': 'ChST-10',
'Pacific/Honolulu': 'HST10',
'Pacific/Kiritimati': 'UNK-14',
'Pacific/Kosrae': 'UNK-11',
'Pacific/Kwajalein': 'UNK-12',
'Pacific/Majuro': 'UNK-12',
'Pacific/Marquesas': 'UNK9:30',
'Pacific/Midway': 'SST11',
'Pacific/Nauru': 'UNK-12',
'Pacific/Niue': 'UNK11',
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
'Pacific/Noumea': 'UNK-11',
'Pacific/Pago_Pago': 'SST11',
'Pacific/Palau': 'UNK-9',
'Pacific/Pitcairn': 'UNK8',
'Pacific/Pohnpei': 'UNK-11',
'Pacific/Port_Moresby': 'UNK-10',
'Pacific/Rarotonga': 'UNK10',
'Pacific/Saipan': 'ChST-10',
'Pacific/Tahiti': 'UNK10',
'Pacific/Tarawa': 'UNK-12',
'Pacific/Tongatapu': 'UNK-13',
'Pacific/Wake': 'UNK-12',
'Pacific/Wallis': 'UNK-12'
};
export function selectedTimeZone(label: string, format: string) {
return TIME_ZONES[label] === format ? label : undefined;
}
export function timeZoneSelectItems() {
return Object.keys(TIME_ZONES).map(label => (
<MenuItem key={label} value={label}>{label}</MenuItem>
return Object.keys(TIME_ZONES).map((label) => (
<MenuItem key={label} value={label}>
{label}
</MenuItem>
));
}

View File

@@ -1,4 +1,4 @@
import parseMilliseconds from 'parse-ms'
import parseMilliseconds from 'parse-ms';
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
day: 'numeric',
@@ -7,37 +7,37 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false,
})
hour12: false
});
export const formatDateTime = (dateTime: string) => {
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)))
}
return LOCALE_FORMAT.format(new Date(dateTime.substr(0, 19)));
};
export const formatLocalDateTime = (date: Date) => {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, -1)
.substr(0, 19)
}
.substr(0, 19);
};
export const formatDuration = (duration: number) => {
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000)
var formatted = ''
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
let formatted = '';
if (days) {
formatted += pluralize(days, 'day')
formatted += pluralize(days, 'day');
}
if (formatted || hours) {
formatted += pluralize(hours, 'hour')
formatted += pluralize(hours, 'hour');
}
if (formatted || minutes) {
formatted += pluralize(minutes, 'minute')
formatted += pluralize(minutes, 'minute');
}
if (formatted || seconds) {
formatted += pluralize(seconds, 'second')
formatted += pluralize(seconds, 'second');
}
return formatted
}
return formatted;
};
const pluralize = (count: number, noun: string, suffix: string = 's') =>
` ${count} ${noun}${count !== 1 ? suffix : ''} `
const pluralize = (count: number, noun: string, suffix = 's') =>
` ${count} ${noun}${count !== 1 ? suffix : ''} `;

View File

@@ -1,23 +1,23 @@
export enum NTPSyncStatus {
NTP_INACTIVE = 0,
NTP_ACTIVE = 1,
NTP_ACTIVE = 1
}
export interface NTPStatus {
status: NTPSyncStatus
utc_time: string
local_time: string
server: string
uptime: number
status: NTPSyncStatus;
utc_time: string;
local_time: string;
server: string;
uptime: number;
}
export interface NTPSettings {
enabled: boolean
server: string
tz_label: string
tz_format: string
enabled: boolean;
server: string;
tz_label: string;
tz_format: string;
}
export interface Time {
local_time: string
local_time: string;
}

View File

@@ -1,23 +1,24 @@
import React from 'react';
import MenuItem from '@material-ui/core/MenuItem';
type BoardProfiles = {
[name: string]: string
[name: string]: string;
};
export const BOARD_PROFILES: BoardProfiles = {
"S32": "BBQKees Gateway S32",
"E32": "BBQKees Gateway E32",
"NODEMCU": "NodeMCU 32S",
"MH-ET": "MH-ET Live D1 Mini",
"LOLIN": "Lolin D32",
"OLIMEX": "Olimex ESP32-EVB",
"TLK110": "Generic Ethernet (TLK110)",
"LAN8720": "Generic Ethernet (LAN8720)"
}
S32: 'BBQKees Gateway S32',
E32: 'BBQKees Gateway E32',
NODEMCU: 'NodeMCU 32S',
'MH-ET': 'MH-ET Live D1 Mini',
LOLIN: 'Lolin D32',
OLIMEX: 'Olimex ESP32-EVB',
TLK110: 'Generic Ethernet (TLK110)',
LAN8720: 'Generic Ethernet (LAN8720)'
};
export function boardProfileSelectItems() {
return Object.keys(BOARD_PROFILES).map(code => (
<MenuItem key={code} value={code}>{BOARD_PROFILES[code]}</MenuItem>
return Object.keys(BOARD_PROFILES).map((code) => (
<MenuItem key={code} value={code}>
{BOARD_PROFILES[code]}
</MenuItem>
));
}

View File

@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
@@ -12,30 +12,43 @@ import EMSESPDevicesController from './EMSESPDevicesController';
import EMSESPHelp from './EMSESPHelp';
class EMSESP extends Component<RouteComponentProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Dashboard">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value={`/${PROJECT_PATH}/devices`} label="Devices & Sensors" />
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/devices`} component={EMSESPDevicesController} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/help`} component={EMSESPHelp} />
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/devices`}
component={EMSESPDevicesController}
/>
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/status`}
component={EMSESPStatusController}
/>
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/help`}
component={EMSESPHelp}
/>
<Redirect to={`/${PROJECT_PATH}/devices`} />
</Switch>
</MenuAppBar>
)
);
}
}
export default EMSESP;

View File

@@ -1,16 +1,20 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { ENDPOINT_ROOT } from '../api';
import EMSESPDevicesForm from './EMSESPDevicesForm';
import { EMSESPDevices } from './EMSESPtypes';
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices";
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + 'allDevices';
type EMSESPDevicesControllerProps = RestControllerProps<EMSESPDevices>;
class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -20,10 +24,10 @@ class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
<SectionContent title="Devices & Sensors">
<RestFormLoader
{...this.props}
render={formProps => <EMSESPDevicesForm {...formProps} />}
render={(formProps) => <EMSESPDevicesForm {...formProps} />}
/>
</SectionContent>
)
);
}
}

View File

@@ -1,38 +1,62 @@
import React, { Component, Fragment } from "react";
import { withStyles, Theme, createStyles } from "@material-ui/core/styles";
import React, { Component, Fragment } from 'react';
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
import {
Table, TableBody, TableCell, TableHead, TableRow, TableContainer, withWidth, WithWidthProps, isWidthDown,
Button, Tooltip, DialogTitle, DialogContent, DialogActions, Box, Dialog, Typography
} from "@material-ui/core";
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TableContainer,
withWidth,
WithWidthProps,
isWidthDown,
Button,
Tooltip,
DialogTitle,
DialogContent,
DialogActions,
Box,
Dialog,
Typography
} from '@material-ui/core';
import RefreshIcon from "@material-ui/icons/Refresh";
import ListIcon from "@material-ui/icons/List";
import RefreshIcon from '@material-ui/icons/Refresh';
import ListIcon from '@material-ui/icons/List';
import IconButton from '@material-ui/core/IconButton';
import EditIcon from '@material-ui/icons/Edit';
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication";
import { RestFormProps, FormButton, extractEventValue } from "../components";
import {
redirectingAuthorizedFetch,
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import { RestFormProps, FormButton, extractEventValue } from '../components';
import { EMSESPDevices, EMSESPDeviceData, Device, DeviceValue } from "./EMSESPtypes";
import {
EMSESPDevices,
EMSESPDeviceData,
Device,
DeviceValue
} from './EMSESPtypes';
import ValueForm from './ValueForm';
import { ENDPOINT_ROOT } from "../api";
import { ENDPOINT_ROOT } from '../api';
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + "writeValue";
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices';
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData';
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue';
const StyledTableCell = withStyles((theme: Theme) =>
createStyles({
head: {
backgroundColor: theme.palette.common.black,
color: theme.palette.common.white,
color: theme.palette.common.white
},
body: {
fontSize: 14,
},
fontSize: 14
}
})
)(TableCell);
@@ -42,8 +66,8 @@ const CustomTooltip = withStyles((theme: Theme) => ({
color: 'white',
boxShadow: theme.shadows[1],
fontSize: 11,
border: '1px solid #dadde9',
},
border: '1px solid #dadde9'
}
}))(Tooltip);
function compareDevices(a: Device, b: Device) {
@@ -64,63 +88,81 @@ interface EMSESPDevicesFormState {
devicevalue?: DeviceValue;
}
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> & AuthenticatedContextProps & WithWidthProps;
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> &
AuthenticatedContextProps &
WithWidthProps;
function formatTemp(t: string) {
if (t == null) {
return "n/a";
return 'n/a';
}
return t + " °C";
return t + ' °C';
}
function formatUnit(u: string) {
if (u == null) {
return u;
}
return " " + u;
return ' ' + u;
}
class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesFormState> {
class EMSESPDevicesForm extends Component<
EMSESPDevicesFormProps,
EMSESPDevicesFormState
> {
state: EMSESPDevicesFormState = {
confirmScanDevices: false,
processing: false
};
handleValueChange = (name: keyof DeviceValue) => (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ devicevalue: { ...this.state.devicevalue!, [name]: extractEventValue(event) } });
handleValueChange = (name: keyof DeviceValue) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({
devicevalue: {
...this.state.devicevalue!,
[name]: extractEventValue(event)
}
});
};
cancelEditingValue = () => {
this.setState({
devicevalue: undefined
});
}
};
doneEditingValue = () => {
const { devicevalue } = this.state;
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
method: "POST",
method: 'POST',
body: JSON.stringify({ devicevalue: devicevalue }),
headers: {
"Content-Type": "application/json",
},
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Write command sent to device", { variant: "success" });
this.props.enqueueSnackbar('Write command sent to device', {
variant: 'success'
});
} else if (response.status === 204) {
this.props.enqueueSnackbar("Write command failed", { variant: "error" });
this.props.enqueueSnackbar('Write command failed', {
variant: 'error'
});
} else if (response.status === 403) {
this.props.enqueueSnackbar("Write access denied", { variant: "error" });
this.props.enqueueSnackbar('Write access denied', {
variant: 'error'
});
} else {
throw Error("Unexpected response code: " + response.status);
throw Error('Unexpected response code: ' + response.status);
}
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || "Problem writing value", { variant: "error" }
);
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
variant: 'error'
});
});
if (devicevalue) {
@@ -128,20 +170,19 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
devicevalue: undefined
});
}
};
sendCommand = (i: any) => {
sendCommand = (i: number) => {
this.setState({
devicevalue: {
id: this.state.selectedDevice!,
data: this.state.deviceData?.data[i]!,
uom: this.state.deviceData?.data[i + 1]!,
name: this.state.deviceData?.data[i + 2]!,
cmd: this.state.deviceData?.data[i + 3]!,
cmd: this.state.deviceData?.data[i + 3]!
}
});
}
};
noDevices = () => {
return this.props.data.devices.length === 0;
@@ -166,22 +207,41 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
{!this.noDevices() && (
<Table
size="small"
padding={isWidthDown("xs", width!) ? "none" : "default"}
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
>
<TableBody>
{data.devices.sort(compareDevices).map((device) => (
<TableRow hover key={device.id} onClick={() => this.handleRowClick(device)}>
<TableRow
hover
key={device.id}
onClick={() => this.handleRowClick(device)}
>
<TableCell>
<CustomTooltip
title={"DeviceID:0x" + ("00" + device.deviceid.toString(16).toUpperCase()).slice(-2) + " ProductID:" + device.productid + " Version:" + device.version}
title={
'DeviceID:0x' +
(
'00' + device.deviceid.toString(16).toUpperCase()
).slice(-2) +
' ProductID:' +
device.productid +
' Version:' +
device.version
}
placement="right-end"
>
<Button startIcon={<ListIcon />} size="small" variant="outlined">
<Button
startIcon={<ListIcon />}
size="small"
variant="outlined"
>
{device.type}
</Button>
</CustomTooltip>
</TableCell>
<TableCell align="right">{device.brand + " " + device.name} </TableCell>
<TableCell align="right">
{device.brand + ' ' + device.name}{' '}
</TableCell>
</TableRow>
))}
</TableBody>
@@ -191,10 +251,13 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
<Box
bgcolor="error.main"
color="error.contrastText"
p={2} mt={2} mb={2}
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
No EMS devices found. Check the connections and for possible Tx errors.
No EMS devices found. Check the connections and for possible Tx
errors.
</Typography>
</Box>
)}
@@ -255,14 +318,25 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
>
<DialogTitle>Confirm Scan Devices</DialogTitle>
<DialogContent dividers>
Are you sure you want to initiate a scan on the EMS bus for all new devices?
Are you sure you want to initiate a scan on the EMS bus for all new
devices?
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={this.onScanDevicesRejected} color="secondary">
<Button
variant="contained"
onClick={this.onScanDevicesRejected}
color="secondary"
>
Cancel
</Button>
<Button
startIcon={<RefreshIcon />} variant="contained" onClick={this.onScanDevicesConfirmed} disabled={this.state.processing} color="primary" autoFocus>
startIcon={<RefreshIcon />}
variant="contained"
onClick={this.onScanDevicesConfirmed}
disabled={this.state.processing}
color="primary"
autoFocus
>
Start Scan
</Button>
</DialogActions>
@@ -283,17 +357,17 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
.then((response) => {
if (response.status === 200) {
this.props.enqueueSnackbar("Device scan is starting...", {
variant: "info",
this.props.enqueueSnackbar('Device scan is starting...', {
variant: 'info'
});
this.setState({ processing: false, confirmScanDevices: false });
} else {
throw Error("Invalid status code: " + response.status);
throw Error('Invalid status code: ' + response.status);
}
})
.catch((error) => {
this.props.enqueueSnackbar(error.message || "Problem with scan", {
variant: "error",
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
variant: 'error'
});
this.setState({ processing: false, confirmScanDevices: false });
});
@@ -302,25 +376,25 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
handleRowClick = (device: any) => {
this.setState({ selectedDevice: device.id, deviceData: undefined });
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
method: "POST",
method: 'POST',
body: JSON.stringify({ id: device.id }),
headers: {
"Content-Type": "application/json",
},
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Unexpected response code: " + response.status);
throw Error('Unexpected response code: ' + response.status);
})
.then((json) => {
this.setState({ deviceData: json });
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || "Problem getting device data",
{ variant: "error" }
error.message || 'Problem getting device data',
{ variant: 'error' }
);
this.setState({ deviceData: undefined });
});
@@ -351,10 +425,9 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
<TableContainer>
<Table
size="small"
padding={isWidthDown("xs", width!) ? "none" : "default"}
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
>
<TableHead>
</TableHead>
<TableHead></TableHead>
<TableBody>
{deviceData.data.map((item, i) => {
if (i % 4) {
@@ -362,19 +435,30 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
} else {
return (
<TableRow hover key={i}>
<TableCell padding="checkbox" style={{ width: 18 }} >
<TableCell padding="checkbox" style={{ width: 18 }}>
{deviceData.data[i + 3] && me.admin && (
<CustomTooltip title="change value" placement="left-end"
<CustomTooltip
title="change value"
placement="left-end"
>
<IconButton
edge="start"
size="small"
aria-label="Edit"
onClick={() => this.sendCommand(i)}
>
<IconButton edge="start" size="small" aria-label="Edit"
onClick={() => this.sendCommand(i)}>
<EditIcon color="primary" fontSize="small" />
</IconButton>
</CustomTooltip>
)}
</TableCell>
<TableCell padding="none" component="th" scope="row">{deviceData.data[i + 2]}</TableCell>
<TableCell padding="none" align="right">{deviceData.data[i]}{formatUnit(deviceData.data[i + 1])}</TableCell>
<TableCell padding="none" component="th" scope="row">
{deviceData.data[i + 2]}
</TableCell>
<TableCell padding="none" align="right">
{deviceData.data[i]}
{formatUnit(deviceData.data[i + 1])}
</TableCell>
</TableRow>
);
}
@@ -390,7 +474,7 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
</Typography>
</Box>
)}
</Fragment >
</Fragment>
);
}
@@ -405,26 +489,34 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
<br></br>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1} padding={1}>
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData} >
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
Refresh
</FormButton>
</Box>
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
<FormButton startIcon={<RefreshIcon />} variant="contained" onClick={this.onScanDevices} >
<FormButton
startIcon={<RefreshIcon />}
variant="contained"
onClick={this.onScanDevices}
>
Scan Devices
</FormButton>
</Box>
</Box>
{this.renderScanDevicesDialog()}
{
devicevalue &&
{devicevalue && (
<ValueForm
devicevalue={devicevalue}
onDoneEditing={this.doneEditingValue}
onCancelEditing={this.cancelEditingValue}
handleValueChange={this.handleValueChange}
/>
}
)}
</Fragment>
);
}

View File

@@ -1,31 +1,40 @@
import React, { Component } from 'react';
import { Typography, Box, List, ListItem, ListItemText, Link, ListItemAvatar } from '@material-ui/core';
import {
Typography,
Box,
List,
ListItem,
ListItemText,
Link,
ListItemAvatar
} from '@material-ui/core';
import { SectionContent } from '../components';
import CommentIcon from "@material-ui/icons/CommentTwoTone";
import MenuBookIcon from "@material-ui/icons/MenuBookTwoTone";
import GitHubIcon from "@material-ui/icons/GitHub";
import StarIcon from "@material-ui/icons/Star";
import ImportExportIcon from "@material-ui/icons/ImportExport";
import BugReportIcon from "@material-ui/icons/BugReportTwoTone";
import CommentIcon from '@material-ui/icons/CommentTwoTone';
import MenuBookIcon from '@material-ui/icons/MenuBookTwoTone';
import GitHubIcon from '@material-ui/icons/GitHub';
import StarIcon from '@material-ui/icons/Star';
import ImportExportIcon from '@material-ui/icons/ImportExport';
import BugReportIcon from '@material-ui/icons/BugReportTwoTone';
export const WebAPISystemSettings = window.location.origin + "/api/system/settings";
export const WebAPISystemInfo = window.location.origin + "/api/system/info";
export const WebAPISystemSettings =
window.location.origin + '/api/system/settings';
export const WebAPISystemInfo = window.location.origin + '/api/system/info';
class EMSESPHelp extends Component {
render() {
return (
<SectionContent title='EMS-ESP Help' titleGutter>
<SectionContent title="EMS-ESP Help" titleGutter>
<List>
<ListItem>
<ListItemAvatar>
<MenuBookIcon />
</ListItemAvatar>
<ListItemText>
For the latest news and updates go to the <Link href="https://emsesp.github.io/docs" color="primary">{'official documentation'}&nbsp;website</Link>
For the latest news and updates go to the{' '}
<Link href="https://emsesp.github.io/docs" color="primary">
{'official documentation'}&nbsp;website
</Link>
</ListItemText>
</ListItem>
@@ -34,7 +43,10 @@ class EMSESPHelp extends Component {
<CommentIcon />
</ListItemAvatar>
<ListItemText>
For live community chat join our <Link href="https://discord.gg/3J3GgnzpyT" color="primary">{'Discord'}&nbsp;server</Link>
For live community chat join our{' '}
<Link href="https://discord.gg/3J3GgnzpyT" color="primary">
{'Discord'}&nbsp;server
</Link>
</ListItemText>
</ListItem>
@@ -43,7 +55,13 @@ class EMSESPHelp extends Component {
<GitHubIcon />
</ListItemAvatar>
<ListItemText>
To report an issue or feature request go to <Link href="https://github.com/emsesp/EMS-ESP32/issues/new/choose" color="primary">{'click here'}</Link>
To report an issue or feature request go to{' '}
<Link
href="https://github.com/emsesp/EMS-ESP32/issues/new/choose"
color="primary"
>
{'click here'}
</Link>
</ListItemText>
</ListItem>
@@ -52,34 +70,41 @@ class EMSESPHelp extends Component {
<ImportExportIcon />
</ListItemAvatar>
<ListItemText>
To export your system settings <Link target="_blank" href={WebAPISystemSettings} color="primary">{'click here'}</Link>
To export your system settings{' '}
<Link target="_blank" href={WebAPISystemSettings} color="primary">
{'click here'}
</Link>
</ListItemText>
</ListItem>
<ListItem>
<ListItemAvatar>
<BugReportIcon />
</ListItemAvatar>
<ListItemText>
To export the current status of EMS-ESP <Link target="_blank" href={WebAPISystemInfo} color="primary">{'click here'}</Link>
To export the current status of EMS-ESP{' '}
<Link target="_blank" href={WebAPISystemInfo} color="primary">
{'click here'}
</Link>
</ListItemText>
</ListItem>
</List>
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
<Typography variant="h6">
EMS-ESP is free and open-source.
<br></br>Please consider supporting this project by giving it a <StarIcon style={{ color: '#fdff3a' }} /> on our <Link href="https://github.com/emsesp/EMS-ESP32" color="primary">{'GitHub page'}</Link>.
<br></br>Please consider supporting this project by giving it a{' '}
<StarIcon style={{ color: '#fdff3a' }} /> on our{' '}
<Link href="https://github.com/emsesp/EMS-ESP32" color="primary">
{'GitHub page'}
</Link>
.
</Typography>
</Box>
<br></br>
</SectionContent>
)
);
}
}
export default EMSESPHelp;

View File

@@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
@@ -10,26 +10,31 @@ import { AuthenticatedRoute } from '../authentication';
import EMSESPSettingsController from './EMSESPSettingsController';
class EMSESP extends Component<RouteComponentProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Settings">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/settings`} component={EMSESPSettingsController} />
<AuthenticatedRoute
exact
path={`/${PROJECT_PATH}/settings`}
component={EMSESPSettingsController}
/>
<Redirect to={`/${PROJECT_PATH}/settings`} />
</Switch>
</MenuAppBar>
)
);
}
}
export default EMSESP;

View File

@@ -1,19 +1,22 @@
import React, { Component } from 'react';
// import { Container } from '@material-ui/core';
import { Component } from 'react';
import { ENDPOINT_ROOT } from '../api';
import EMSESPSettingsForm from './EMSESPSettingsForm';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { EMSESPSettings } from './EMSESPtypes';
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings';
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -21,18 +24,18 @@ class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps>
render() {
return (
// <Container maxWidth="md" disableGutters>
<SectionContent title='' titleGutter>
<SectionContent title="" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => (
<EMSESPSettingsForm {...formProps} />
)}
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
/>
</SectionContent>
// </Container>
)
);
}
}
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
export default restController(
EMSESP_SETTINGS_ENDPOINT,
EMSESPSettingsController
);

View File

@@ -1,9 +1,9 @@
import React from "react";
import React from 'react';
import {
ValidatorForm,
TextValidator,
SelectValidator,
} from "react-material-ui-form-validator";
SelectValidator
} from 'react-material-ui-form-validator';
import {
Checkbox,
@@ -11,34 +11,34 @@ import {
Box,
Link,
withWidth,
WithWidthProps,
} from "@material-ui/core";
import SaveIcon from "@material-ui/icons/Save";
import MenuItem from "@material-ui/core/MenuItem";
WithWidthProps
} from '@material-ui/core';
import SaveIcon from '@material-ui/icons/Save';
import MenuItem from '@material-ui/core/MenuItem';
import Grid from "@material-ui/core/Grid";
import Grid from '@material-ui/core/Grid';
import {
redirectingAuthorizedFetch,
withAuthenticatedContext,
AuthenticatedContextProps,
} from "../authentication";
AuthenticatedContextProps
} from '../authentication';
import {
RestFormProps,
FormActions,
FormButton,
BlockFormControlLabel,
} from "../components";
BlockFormControlLabel
} from '../components';
import { isIP, optional } from "../validators";
import { isIP, optional } from '../validators';
import { EMSESPSettings } from "./EMSESPtypes";
import { EMSESPSettings } from './EMSESPtypes';
import { boardProfileSelectItems } from "./EMSESPBoardProfiles";
import { boardProfileSelectItems } from './EMSESPBoardProfiles';
import { ENDPOINT_ROOT } from "../api";
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
import { ENDPOINT_ROOT } from '../api';
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile';
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
AuthenticatedContextProps &
@@ -50,38 +50,38 @@ interface EMSESPSettingsFormState {
class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
state: EMSESPSettingsFormState = {
processing: false,
processing: false
};
componentDidMount() {
ValidatorForm.addValidationRule("isOptionalIP", optional(isIP));
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
}
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { data, setData } = this.props;
setData({
...data,
board_profile: event.target.value,
board_profile: event.target.value
});
if (event.target.value === "CUSTOM") return;
if (event.target.value === 'CUSTOM') return;
this.setState({ processing: true });
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
method: "POST",
method: 'POST',
body: JSON.stringify({ code: event.target.value }),
headers: {
"Content-Type": "application/json",
},
'Content-Type': 'application/json'
}
})
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw Error("Unexpected response code: " + response.status);
throw Error('Unexpected response code: ' + response.status);
})
.then((json) => {
this.props.enqueueSnackbar("Profile loaded", { variant: "success" });
this.props.enqueueSnackbar('Profile loaded', { variant: 'success' });
setData({
...data,
led_gpio: json.led_gpio,
@@ -89,14 +89,14 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
rx_gpio: json.rx_gpio,
tx_gpio: json.tx_gpio,
pbutton_gpio: json.pbutton_gpio,
board_profile: event.target.value,
board_profile: event.target.value
});
this.setState({ processing: false });
})
.catch((error) => {
this.props.enqueueSnackbar(
error.message || "Problem fetching board profile",
{ variant: "warning" }
error.message || 'Problem fetching board profile',
{ variant: 'warning' }
);
this.setState({ processing: false });
});
@@ -108,13 +108,13 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<ValidatorForm onSubmit={saveData}>
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
<Typography variant="body1">
Adjust any of the EMS-ESP settings here. For help refer to the{" "}
Adjust any of the EMS-ESP settings here. For help refer to the{' '}
<Link
target="_blank"
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
color="primary"
>
{"online documentation"}
{'online documentation'}
</Link>
.
</Typography>
@@ -139,7 +139,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
value={data.tx_mode}
fullWidth
variant="outlined"
onChange={handleValueChange("tx_mode")}
onChange={handleValueChange('tx_mode')}
margin="normal"
>
<MenuItem value={0}>Off</MenuItem>
@@ -156,7 +156,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
value={data.ems_bus_id}
fullWidth
variant="outlined"
onChange={handleValueChange("ems_bus_id")}
onChange={handleValueChange('ems_bus_id')}
margin="normal"
>
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
@@ -169,16 +169,16 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:120",
'required',
'isNumber',
'minNumber:0',
'maxNumber:120'
]}
errorMessages={[
"Tx delay is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 120",
'Tx delay is required',
'Must be a number',
'Must be 0 or higher',
'Max value is 120'
]}
name="tx_delay"
label="Tx start delay (seconds)"
@@ -186,7 +186,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.tx_delay}
type="number"
onChange={handleValueChange("tx_delay")}
onChange={handleValueChange('tx_delay')}
margin="normal"
/>
</Grid>
@@ -216,12 +216,12 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
margin="normal"
>
{boardProfileSelectItems()}
<MenuItem key={"CUSTOM"} value={"CUSTOM"}>
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
Custom...
</MenuItem>
</SelectValidator>
{data.board_profile === "CUSTOM" && (
{data.board_profile === 'CUSTOM' && (
<Grid
container
spacing={1}
@@ -232,18 +232,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
'required',
'isNumber',
'minNumber:0',
'maxNumber:40',
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
'GPIO is required',
'Must be a number',
'Must be 0 or higher',
'Max value is 40',
'Not a valid GPIO'
]}
name="rx_gpio"
label="Rx GPIO"
@@ -251,25 +251,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.rx_gpio}
type="number"
onChange={handleValueChange("rx_gpio")}
onChange={handleValueChange('rx_gpio')}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
'required',
'isNumber',
'minNumber:0',
'maxNumber:40',
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
'GPIO is required',
'Must be a number',
'Must be 0 or higher',
'Max value is 40',
'Not a valid GPIO'
]}
name="tx_gpio"
label="Tx GPIO"
@@ -277,25 +277,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.tx_gpio}
type="number"
onChange={handleValueChange("tx_gpio")}
onChange={handleValueChange('tx_gpio')}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
'required',
'isNumber',
'minNumber:0',
'maxNumber:40',
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
'GPIO is required',
'Must be a number',
'Must be 0 or higher',
'Max value is 40',
'Not a valid GPIO'
]}
name="pbutton_gpio"
label="Button GPIO"
@@ -303,25 +303,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.pbutton_gpio}
type="number"
onChange={handleValueChange("pbutton_gpio")}
onChange={handleValueChange('pbutton_gpio')}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
'required',
'isNumber',
'minNumber:0',
'maxNumber:40',
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
'GPIO is required',
'Must be a number',
'Must be 0 or higher',
'Max value is 40',
'Not a valid GPIO'
]}
name="dallas_gpio"
label="Dallas GPIO (0=none)"
@@ -329,25 +329,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.dallas_gpio}
type="number"
onChange={handleValueChange("dallas_gpio")}
onChange={handleValueChange('dallas_gpio')}
margin="normal"
/>
</Grid>
<Grid item xs={4}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:40",
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
'required',
'isNumber',
'minNumber:0',
'maxNumber:40',
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
]}
errorMessages={[
"GPIO is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 40",
"Not a valid GPIO",
'GPIO is required',
'Must be a number',
'Must be 0 or higher',
'Max value is 40',
'Not a valid GPIO'
]}
name="led_gpio"
label="LED GPIO (0=none)"
@@ -355,7 +355,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.led_gpio}
type="number"
onChange={handleValueChange("led_gpio")}
onChange={handleValueChange('led_gpio')}
margin="normal"
/>
</Grid>
@@ -372,7 +372,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.hide_led}
onChange={handleValueChange("hide_led")}
onChange={handleValueChange('hide_led')}
value="hide_led"
/>
}
@@ -385,7 +385,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.dallas_parasite}
onChange={handleValueChange("dallas_parasite")}
onChange={handleValueChange('dallas_parasite')}
value="dallas_parasite"
/>
}
@@ -397,7 +397,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.notoken_api}
onChange={handleValueChange("notoken_api")}
onChange={handleValueChange('notoken_api')}
value="notoken_api"
/>
}
@@ -407,7 +407,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.analog_enabled}
onChange={handleValueChange("analog_enabled")}
onChange={handleValueChange('analog_enabled')}
value="analog_enabled"
/>
}
@@ -424,7 +424,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.shower_timer}
onChange={handleValueChange("shower_timer")}
onChange={handleValueChange('shower_timer')}
value="shower_timer"
/>
}
@@ -434,7 +434,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.shower_alert}
onChange={handleValueChange("shower_alert")}
onChange={handleValueChange('shower_alert')}
value="shower_alert"
/>
}
@@ -451,7 +451,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.syslog_enabled}
onChange={handleValueChange("syslog_enabled")}
onChange={handleValueChange('syslog_enabled')}
value="syslog_enabled"
/>
}
@@ -468,30 +468,30 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
>
<Grid item xs={5}>
<TextValidator
validators={["isOptionalIP"]}
errorMessages={["Not a valid IP address"]}
validators={['isOptionalIP']}
errorMessages={['Not a valid IP address']}
name="syslog_host"
label="IP"
fullWidth
variant="outlined"
value={data.syslog_host}
onChange={handleValueChange("syslog_host")}
onChange={handleValueChange('syslog_host')}
margin="normal"
/>
</Grid>
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Port is required",
"Must be a number",
"Must be greater than 0 ",
"Max value is 65535",
'Port is required',
'Must be a number',
'Must be greater than 0 ',
'Max value is 65535'
]}
name="syslog_port"
label="Port"
@@ -499,7 +499,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.syslog_port}
type="number"
onChange={handleValueChange("syslog_port")}
onChange={handleValueChange('syslog_port')}
margin="normal"
/>
</Grid>
@@ -510,7 +510,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
value={data.syslog_level}
fullWidth
variant="outlined"
onChange={handleValueChange("syslog_level")}
onChange={handleValueChange('syslog_level')}
margin="normal"
>
<MenuItem value={-1}>OFF</MenuItem>
@@ -524,16 +524,16 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<Grid item xs={6}>
<TextValidator
validators={[
"required",
"isNumber",
"minNumber:0",
"maxNumber:65535",
'required',
'isNumber',
'minNumber:0',
'maxNumber:65535'
]}
errorMessages={[
"Syslog Mark is required",
"Must be a number",
"Must be 0 or higher",
"Max value is 10",
'Syslog Mark is required',
'Must be a number',
'Must be 0 or higher',
'Max value is 10'
]}
name="syslog_mark_interval"
label="Mark Interval seconds (0=off)"
@@ -541,7 +541,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
variant="outlined"
value={data.syslog_mark_interval}
type="number"
onChange={handleValueChange("syslog_mark_interval")}
onChange={handleValueChange('syslog_mark_interval')}
margin="normal"
/>
</Grid>
@@ -549,7 +549,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
control={
<Checkbox
checked={data.trace_raw}
onChange={handleValueChange("trace_raw")}
onChange={handleValueChange('trace_raw')}
value="trace_raw"
/>
}

View File

@@ -1,39 +1,39 @@
import { Theme } from '@material-ui/core'
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes'
import { Theme } from '@material-ui/core';
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes';
export const isConnected = ({ status }: EMSESPStatus) =>
status !== busConnectionStatus.BUS_STATUS_OFFLINE
status !== busConnectionStatus.BUS_STATUS_OFFLINE;
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main
return theme.palette.warning.main;
case busConnectionStatus.BUS_STATUS_CONNECTED:
return theme.palette.success.main
return theme.palette.success.main;
case busConnectionStatus.BUS_STATUS_OFFLINE:
return theme.palette.error.main
return theme.palette.error.main;
default:
return theme.palette.warning.main
return theme.palette.warning.main;
}
}
};
export const busStatus = ({ status }: EMSESPStatus) => {
switch (status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return 'Connected'
return 'Connected';
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return 'Tx Errors'
return 'Tx Errors';
case busConnectionStatus.BUS_STATUS_OFFLINE:
return 'Disconnected'
return 'Disconnected';
default:
return 'Unknown'
return 'Unknown';
}
}
};
export const qualityHighlight = (value: number, theme: Theme) => {
if (value >= 95) {
return theme.palette.success.main
return theme.palette.success.main;
}
return theme.palette.error.main
}
return theme.palette.error.main;
};

View File

@@ -1,16 +1,20 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { ENDPOINT_ROOT } from '../api';
import EMSESPStatusForm from './EMSESPStatusForm';
import { EMSESPStatus } from './EMSESPtypes';
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + "emsespStatus";
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'emsespStatus';
type EMSESPStatusControllerProps = RestControllerProps<EMSESPStatus>;
class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -20,10 +24,10 @@ class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
<SectionContent title="EMS Status">
<RestFormLoader
{...this.props}
render={formProps => <EMSESPStatusForm {...formProps} />}
render={(formProps) => <EMSESPStatusForm {...formProps} />}
/>
</SectionContent>
)
);
}
}

View File

@@ -1,6 +1,6 @@
import React, { Component, Fragment } from "react";
import React, { Component, Fragment } from 'react';
import { WithTheme, withTheme } from "@material-ui/core/styles";
import { WithTheme, withTheme } from '@material-ui/core/styles';
import {
TableContainer,
Table,
@@ -13,35 +13,32 @@ import {
ListItemText,
withWidth,
WithWidthProps,
isWidthDown,
} from "@material-ui/core";
isWidthDown
} from '@material-ui/core';
import RefreshIcon from "@material-ui/icons/Refresh";
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
import RefreshIcon from '@material-ui/icons/Refresh';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import {
RestFormProps,
FormActions,
FormButton,
HighlightAvatar,
} from "../components";
HighlightAvatar
} from '../components';
import {
busStatus,
busStatusHighlight,
isConnected,
} from "./EMSESPStatus";
import { busStatus, busStatusHighlight, isConnected } from './EMSESPStatus';
import { EMSESPStatus } from "./EMSESPtypes";
import { EMSESPStatus } from './EMSESPtypes';
function formatNumber(num: number) {
return new Intl.NumberFormat().format(num);
}
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> & WithTheme & WithWidthProps;
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> &
WithTheme &
WithWidthProps;
class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
createListItems() {
const { data, theme, width } = this.props;
return (
@@ -52,24 +49,30 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
<DeviceHubIcon />
</HighlightAvatar>
</ListItemAvatar>
<ListItemText primary="Connection Status" secondary={busStatus(data)} />
<ListItemText
primary="Connection Status"
secondary={busStatus(data)}
/>
</ListItem>
{isConnected(data) && (
<TableContainer>
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
<Table
size="small"
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
>
<TableBody>
<TableRow>
<TableCell>
# Telegrams Received
</TableCell>
<TableCell align="right">{formatNumber(data.rx_received)}&nbsp;(quality {data.rx_quality}%)
<TableCell># Telegrams Received</TableCell>
<TableCell align="right">
{formatNumber(data.rx_received)}&nbsp;(quality{' '}
{data.rx_quality}%)
</TableCell>
</TableRow>
<TableRow>
<TableCell >
# Telegrams Sent
</TableCell >
<TableCell align="right">{formatNumber(data.tx_sent)}&nbsp;(quality {data.tx_quality}%)
<TableCell># Telegrams Sent</TableCell>
<TableCell align="right">
{formatNumber(data.tx_sent)}&nbsp;(quality {data.tx_quality}
%)
</TableCell>
</TableRow>
</TableBody>
@@ -86,7 +89,11 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
<List>{this.createListItems()}</List>
<FormActions>
<FormButton
startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
startIcon={<RefreshIcon />}
variant="contained"
color="secondary"
onClick={this.props.loadData}
>
Refresh
</FormButton>
</FormActions>

View File

@@ -1,72 +1,72 @@
export interface EMSESPSettings {
tx_mode: number
tx_delay: number
ems_bus_id: number
syslog_enabled: boolean
syslog_level: number
syslog_mark_interval: number
syslog_host: string
syslog_port: number
master_thermostat: number
shower_timer: boolean
shower_alert: boolean
rx_gpio: number
tx_gpio: number
dallas_gpio: number
dallas_parasite: boolean
led_gpio: number
hide_led: boolean
notoken_api: boolean
analog_enabled: boolean
pbutton_gpio: number
trace_raw: boolean
board_profile: string
tx_mode: number;
tx_delay: number;
ems_bus_id: number;
syslog_enabled: boolean;
syslog_level: number;
syslog_mark_interval: number;
syslog_host: string;
syslog_port: number;
master_thermostat: number;
shower_timer: boolean;
shower_alert: boolean;
rx_gpio: number;
tx_gpio: number;
dallas_gpio: number;
dallas_parasite: boolean;
led_gpio: number;
hide_led: boolean;
notoken_api: boolean;
analog_enabled: boolean;
pbutton_gpio: number;
trace_raw: boolean;
board_profile: string;
}
export enum busConnectionStatus {
BUS_STATUS_CONNECTED = 0,
BUS_STATUS_TX_ERRORS = 1,
BUS_STATUS_OFFLINE = 2,
BUS_STATUS_OFFLINE = 2
}
export interface EMSESPStatus {
status: busConnectionStatus
rx_received: number
tx_sent: number
rx_quality: number
tx_quality: number
status: busConnectionStatus;
rx_received: number;
tx_sent: number;
rx_quality: number;
tx_quality: number;
}
export interface Device {
id: number
type: string
brand: string
name: string
deviceid: number
productid: number
version: string
id: number;
type: string;
brand: string;
name: string;
deviceid: number;
productid: number;
version: string;
}
export interface Sensor {
no: number
id: string
temp: string
no: number;
id: string;
temp: string;
}
export interface EMSESPDevices {
devices: Device[]
sensors: Sensor[]
devices: Device[];
sensors: Sensor[];
}
export interface EMSESPDeviceData {
name: string
data: string[]
name: string;
data: string[];
}
export interface DeviceValue {
id: number
data: string
uom: string
name: string
cmd: string
id: number;
data: string;
uom: string;
name: string;
cmd: string;
}

View File

@@ -1,12 +1,15 @@
import React, { Component } from "react";
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
import { Component } from 'react';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
import TuneIcon from '@material-ui/icons/Tune';
import DashboardIcon from "@material-ui/icons/Dashboard";
import DashboardIcon from '@material-ui/icons/Dashboard';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
type ProjectProps = AuthenticatedContextProps & RouteComponentProps;
@@ -16,13 +19,28 @@ class ProjectMenu extends Component<ProjectProps> {
const path = this.props.match.url;
return (
<List>
<ListItem to='/ems-esp/' selected={path.startsWith('/ems-esp/status') || path.startsWith('/ems-esp/devices') || path.startsWith('/ems-esp/help')} button component={Link}>
<ListItem
to="/ems-esp/"
selected={
path.startsWith('/ems-esp/status') ||
path.startsWith('/ems-esp/devices') ||
path.startsWith('/ems-esp/help')
}
button
component={Link}
>
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText primary="Dashboard" />
</ListItem>
<ListItem to='/ems-esp/settings' selected={path.startsWith('/ems-esp/settings')} button component={Link} disabled={!authenticatedContext.me.admin}>
<ListItem
to="/ems-esp/settings"
selected={path.startsWith('/ems-esp/settings')}
button
component={Link}
disabled={!authenticatedContext.me.admin}
>
<ListItemIcon>
<TuneIcon />
</ListItemIcon>

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { Redirect, Switch } from 'react-router';
import { AuthenticatedRoute } from '../authentication';
@@ -7,24 +7,32 @@ import EMSESPDashboard from './EMSESPDashboard';
import EMSESPSettings from './EMSESPSettings';
class ProjectRouting extends Component {
render() {
return (
<Switch>
<AuthenticatedRoute exact path="/ems-esp/status/*" component={EMSESPDashboard} />
<AuthenticatedRoute exact path="/ems-esp/settings" component={EMSESPSettings} />
<AuthenticatedRoute exact path="/ems-esp/*" component={EMSESPDashboard} />
{
/*
<AuthenticatedRoute
exact
path="/ems-esp/status/*"
component={EMSESPDashboard}
/>
<AuthenticatedRoute
exact
path="/ems-esp/settings"
component={EMSESPSettings}
/>
<AuthenticatedRoute
exact
path="/ems-esp/*"
component={EMSESPDashboard}
/>
{/*
* The redirect below caters for the default project route and redirecting invalid paths.
* The "to" property must match one of the routes above for this to work correctly.
*/
}
*/}
<Redirect to={`/ems-esp/status`} />
</Switch>
)
);
}
}
export default ProjectRouting;

View File

@@ -1,6 +1,13 @@
import React, { RefObject } from 'react';
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, Typography } from '@material-ui/core';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
Typography
} from '@material-ui/core';
import { FormButton } from '../components';
import { DeviceValue } from './EMSESPtypes';
@@ -8,30 +15,43 @@ interface ValueFormProps {
devicevalue: DeviceValue;
onDoneEditing: () => void;
onCancelEditing: () => void;
handleValueChange: (data: keyof DeviceValue) => (event: React.ChangeEvent<HTMLInputElement>) => void;
handleValueChange: (
data: keyof DeviceValue
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
}
class ValueForm extends React.Component<ValueFormProps> {
formRef: RefObject<any> = React.createRef();
submit = () => {
this.formRef.current.submit();
}
};
buildLabel = (devicevalue: DeviceValue) => {
if ((devicevalue.uom === "") || (!devicevalue.uom)) {
return "New value";
}
return "New value (" + devicevalue.uom + ")";
if (devicevalue.uom === '' || !devicevalue.uom) {
return 'New value';
}
return 'New value (' + devicevalue.uom + ')';
};
render() {
const { devicevalue, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
const {
devicevalue,
handleValueChange,
onDoneEditing,
onCancelEditing
} = this.props;
return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog maxWidth="xs" onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open>
<DialogTitle id="user-form-dialog-title">Change the {devicevalue.name}</DialogTitle>
<Dialog
maxWidth="xs"
onClose={onCancelEditing}
aria-labelledby="user-form-dialog-title"
open
>
<DialogTitle id="user-form-dialog-title">
Change the {devicevalue.name}
</DialogTitle>
<DialogContent dividers>
<TextValidator
validators={['required']}
@@ -46,14 +66,30 @@ class ValueForm extends React.Component<ValueFormProps> {
/>
<Box color="warning.main" p={1} pl={0} pr={0} mt={0} mb={0}>
<Typography variant="body2">
<i>Note: it may take a few seconds before the change is visible. If nothing happens check the logs.</i>
<i>
Note: it may take a few seconds before the change is visible.
If nothing happens check the logs.
</i>
</Typography>
</Box>
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>Cancel</FormButton>
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>Done</FormButton>
<FormButton
variant="contained"
color="secondary"
onClick={onCancelEditing}
>
Cancel
</FormButton>
<FormButton
variant="contained"
color="primary"
type="submit"
onClick={this.submit}
>
Done
</FormButton>
</DialogActions>
</Dialog>
</ValidatorForm>

View File

@@ -1,5 +1,14 @@
import React, { Fragment } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextField } from '@material-ui/core';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Box,
LinearProgress,
Typography,
TextField
} from '@material-ui/core';
import { FormButton } from '../components';
import { redirectingAuthorizedFetch } from '../authentication';
@@ -15,25 +24,34 @@ interface GenerateTokenState {
token?: string;
}
class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenState> {
class GenerateToken extends React.Component<
GenerateTokenProps,
GenerateTokenState
> {
state: GenerateTokenState = {};
componentDidMount() {
const { username } = this.props;
redirectingAuthorizedFetch(GENERATE_TOKEN_ENDPOINT + "?" + new URLSearchParams({ username }), { method: 'GET' })
.then(response => {
redirectingAuthorizedFetch(
GENERATE_TOKEN_ENDPOINT + '?' + new URLSearchParams({ username }),
{ method: 'GET' }
)
.then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw Error("Error generating token: " + response.status);
throw Error('Error generating token: ' + response.status);
}
}).then(generatedToken => {
})
.then((generatedToken) => {
console.log(generatedToken);
this.setState({ token: generatedToken.token });
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem generating token", { variant: 'error' });
.catch((error) => {
this.props.enqueueSnackbar(
error.message || 'Problem generating token',
{ variant: 'error' }
);
});
}
@@ -41,31 +59,56 @@ class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenSta
const { onClose, username } = this.props;
const { token } = this.state;
return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="generate-token-dialog-title">Token for: {username}</DialogTitle>
<Dialog
onClose={onClose}
aria-labelledby="generate-token-dialog-title"
open
fullWidth
maxWidth="sm"
>
<DialogTitle id="generate-token-dialog-title">
Token for: {username}
</DialogTitle>
<DialogContent dividers>
{token ?
{token ? (
<Fragment>
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
<Box
bgcolor="primary.main"
color="primary.contrastText"
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
The token below may be used to access the secured APIs, either as a Bearer authentication in the "Authorization" header or using the "access_token" query parameter.
The token below may be used to access the secured APIs, either
as a Bearer authentication in the "Authorization" header or
using the "access_token" query parameter.
</Typography>
</Box>
<Box mt={2} mb={2}>
<TextField label="Token" multiline value={token} fullWidth contentEditable={false} />
<TextField
label="Token"
multiline
value={token}
fullWidth
contentEditable={false}
/>
</Box>
</Fragment>
:
) : (
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">
Generating token&hellip;
</Typography>
<Typography variant="h6">Generating token&hellip;</Typography>
</Box>
}
)}
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
<FormButton
variant="contained"
color="primary"
type="submit"
onClick={onClose}
>
Close
</FormButton>
</DialogActions>

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
import ManageUsersForm from './ManageUsersForm';
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
class ManageUsersController extends Component<ManageUsersControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,14 @@ class ManageUsersController extends Component<ManageUsersControllerProps> {
<SectionContent title="Manage Users" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <ManageUsersForm {...formProps} />}
render={(formProps) => <ManageUsersForm {...formProps} />}
/>
</SectionContent>
)
);
}
}
export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
export default restController(
SECURITY_SETTINGS_ENDPOINT,
ManageUsersController
);

View File

@@ -1,8 +1,18 @@
import React, { Fragment } from 'react';
import { ValidatorForm } from 'react-material-ui-form-validator';
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow, withWidth, WithWidthProps, isWidthDown } from '@material-ui/core';
import { Box, Button, Typography, } from '@material-ui/core';
import {
Table,
TableBody,
TableCell,
TableHead,
TableFooter,
TableRow,
withWidth,
WithWidthProps,
isWidthDown
} 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';
@@ -13,8 +23,16 @@ import SaveIcon from '@material-ui/icons/Save';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import VpnKeyIcon from '@material-ui/icons/VpnKey';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
import {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import {
RestFormProps,
FormActions,
FormButton,
extractEventValue
} from '../components';
import UserForm from './UserForm';
import { SecuritySettings, User } from './types';
@@ -30,16 +48,20 @@ function compareUsers(a: User, b: User) {
return 0;
}
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
type ManageUsersFormProps = RestFormProps<SecuritySettings> &
AuthenticatedContextProps &
WithWidthProps;
type ManageUsersFormState = {
creating: boolean;
user?: User;
generateTokenFor?: string;
}
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
};
class ManageUsersForm extends React.Component<
ManageUsersFormProps,
ManageUsersFormState
> {
state: ManageUsersFormState = {
creating: false
};
@@ -48,38 +70,38 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
this.setState({
creating: true,
user: {
username: "",
password: "",
username: '',
password: '',
admin: true
}
});
};
uniqueUsername = (username: string) => {
return !this.props.data.users.find(u => u.username === username);
}
return !this.props.data.users.find((u) => u.username === username);
};
noAdminConfigured = () => {
return !this.props.data.users.find(u => u.admin);
}
return !this.props.data.users.find((u) => u.admin);
};
removeUser = (user: User) => {
const { data } = this.props;
const users = data.users.filter(u => u.username !== user.username);
const users = data.users.filter((u) => u.username !== user.username);
this.props.setData({ ...data, users });
}
};
closeGenerateToken = () => {
this.setState({
generateTokenFor: undefined
});
}
};
generateToken = (user: User) => {
this.setState({
generateTokenFor: user.username
});
}
};
startEditingUser = (user: User) => {
this.setState({
@@ -92,13 +114,13 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
this.setState({
user: undefined
});
}
};
doneEditingUser = () => {
const { user } = this.state;
if (user) {
const { data } = this.props;
const users = data.users.filter(u => u.username !== user.username);
const users = data.users.filter((u) => u.username !== user.username);
users.push(user);
this.props.setData({ ...data, users });
this.setState({
@@ -107,14 +129,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
}
};
handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ user: { ...this.state.user!, [name]: extractEventValue(event) } });
handleUserValueChange = (name: keyof User) => (
event: React.ChangeEvent<HTMLInputElement>
) => {
this.setState({
user: { ...this.state.user!, [name]: extractEventValue(event) }
});
};
onSubmit = () => {
this.props.saveData();
this.props.authenticatedContext.refresh();
}
};
render() {
const { width, data } = this.props;
@@ -122,7 +148,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
return (
<Fragment>
<ValidatorForm onSubmit={this.onSubmit}>
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
<Table
size="small"
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
>
<TableHead>
<TableRow>
<TableCell>Username</TableCell>
@@ -131,7 +160,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
</TableRow>
</TableHead>
<TableBody>
{data.users.sort(compareUsers).map(user => (
{data.users.sort(compareUsers).map((user) => (
<TableRow key={user.username}>
<TableCell component="th" scope="row">
{user.username}
@@ -140,51 +169,79 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
{user.admin ? <CheckIcon /> : <CloseIcon />}
</TableCell>
<TableCell align="center">
<IconButton size="small" aria-label="Generate Token" onClick={() => this.generateToken(user)}>
<IconButton
size="small"
aria-label="Generate Token"
onClick={() => this.generateToken(user)}
>
<VpnKeyIcon />
</IconButton>
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
<IconButton
size="small"
aria-label="Delete"
onClick={() => this.removeUser(user)}
>
<DeleteIcon />
</IconButton>
<IconButton size="small" aria-label="Edit" onClick={() => this.startEditingUser(user)}>
<IconButton
size="small"
aria-label="Edit"
onClick={() => this.startEditingUser(user)}
>
<EditIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter >
<TableFooter>
<TableRow>
<TableCell colSpan={2} />
<TableCell align="center" padding="default">
<Button startIcon={<PersonAddIcon />} variant="contained" color="secondary" onClick={this.createUser}>
<Button
startIcon={<PersonAddIcon />}
variant="contained"
color="secondary"
onClick={this.createUser}
>
Add
</Button>
</TableCell>
</TableRow>
</TableFooter>
</Table>
{
this.noAdminConfigured() &&
(
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
{this.noAdminConfigured() && (
<Box
bgcolor="error.main"
color="error.contrastText"
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
You must have at least one admin user configured.
</Typography>
</Box>
)
}
)}
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit" disabled={this.noAdminConfigured()}>
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
disabled={this.noAdminConfigured()}
>
Save
</FormButton>
</FormActions>
</ValidatorForm>
{
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
}
{
user &&
{generateTokenFor && (
<GenerateToken
username={generateTokenFor}
onClose={this.closeGenerateToken}
/>
)}
{user && (
<UserForm
user={user}
creating={creating}
@@ -193,11 +250,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
handleValueChange={this.handleUserValueChange}
uniqueUsername={this.uniqueUsername}
/>
}
)}
</Fragment>
);
}
}
export default withAuthenticatedContext(withWidth()(ManageUsersForm));

View File

@@ -1,9 +1,12 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
import {
AuthenticatedContextProps,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import ManageUsersController from './ManageUsersController';
@@ -12,25 +15,36 @@ import SecuritySettingsController from './SecuritySettingsController';
type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
class Security extends Component<SecurityProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
render() {
return (
<MenuAppBar sectionTitle="Security">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/security/users" label="Manage Users" />
<Tab value="/security/settings" label="Security Settings" />
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/security/users" component={ManageUsersController} />
<AuthenticatedRoute exact path="/security/settings" component={SecuritySettingsController} />
<AuthenticatedRoute
exact
path="/security/users"
component={ManageUsersController}
/>
<AuthenticatedRoute
exact
path="/security/settings"
component={SecuritySettingsController}
/>
<Redirect to="/security/users" />
</Switch>
</MenuAppBar>
)
);
}
}

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { SECURITY_SETTINGS_ENDPOINT } from '../api';
import SecuritySettingsForm from './SecuritySettingsForm';
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
type SecuritySettingsControllerProps = RestControllerProps<SecuritySettings>;
class SecuritySettingsController extends Component<SecuritySettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,14 @@ class SecuritySettingsController extends Component<SecuritySettingsControllerPro
<SectionContent title="Security Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <SecuritySettingsForm {...formProps} />}
render={(formProps) => <SecuritySettingsForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(SECURITY_SETTINGS_ENDPOINT, SecuritySettingsController);
export default restController(
SECURITY_SETTINGS_ENDPOINT,
SecuritySettingsController
);

View File

@@ -4,19 +4,27 @@ 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 {
withAuthenticatedContext,
AuthenticatedContextProps
} from '../authentication';
import {
RestFormProps,
PasswordValidator,
FormActions,
FormButton
} from '../components';
import { SecuritySettings } from './types';
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> &
AuthenticatedContextProps;
class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
onSubmit = () => {
this.props.saveData();
this.props.authenticatedContext.refresh();
}
};
render() {
const { data, handleValueChange } = this.props;
@@ -24,7 +32,10 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
<ValidatorForm onSubmit={this.onSubmit}>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['Password Required', 'Password must be 64 characters or less']}
errorMessages={[
'Password Required',
'Password must be 64 characters or less'
]}
name="jwt_secret"
label="Super User Password"
fullWidth
@@ -33,20 +44,32 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
onChange={handleValueChange('jwt_secret')}
margin="normal"
/>
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
<Box
bgcolor="primary.main"
color="primary.contrastText"
p={2}
mt={2}
mb={2}
>
<Typography variant="body1">
The Super User password is used to sign authentication tokens and is also the Console's `su` password. If you modify this all users will be signed out.
The Super User password is used to sign authentication tokens and is
also the Console's `su` password. If you modify this all users will
be signed out.
</Typography>
</Box>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>
</ValidatorForm>
);
}
}
export default withAuthenticatedContext(SecuritySettingsForm);

View File

@@ -1,9 +1,19 @@
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 {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Checkbox
} from '@material-ui/core';
import { PasswordValidator, BlockFormControlLabel, FormButton } from '../components';
import {
PasswordValidator,
BlockFormControlLabel,
FormButton
} from '../components';
import { User } from './types';
@@ -11,33 +21,67 @@ interface UserFormProps {
creating: boolean;
user: User;
uniqueUsername: (value: any) => boolean;
handleValueChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => void;
handleValueChange: (
name: keyof User
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
onDoneEditing: () => void;
onCancelEditing: () => void;
}
class UserForm extends React.Component<UserFormProps> {
formRef: RefObject<any> = React.createRef();
componentDidMount() {
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
ValidatorForm.addValidationRule(
'uniqueUsername',
this.props.uniqueUsername
);
}
submit = () => {
this.formRef.current.submit();
}
};
render() {
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
const {
user,
creating,
handleValueChange,
onDoneEditing,
onCancelEditing
} = this.props;
return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
<Dialog
onClose={onCancelEditing}
aria-labelledby="user-form-dialog-title"
open
fullWidth
maxWidth="sm"
>
<DialogTitle id="user-form-dialog-title">
{creating ? 'Add' : 'Modify'} User
</DialogTitle>
<DialogContent dividers>
<TextValidator
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []}
errorMessages={creating ? ['Username is required', "Username already exists", "Must be 1-24 characters: alpha numeric, '_' or '.'"] : []}
validators={
creating
? [
'required',
'uniqueUsername',
'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'
]
: []
}
errorMessages={
creating
? [
'Username is required',
'Username already exists',
"Must be 1-24 characters: alpha numeric, '_' or '.'"
]
: []
}
name="username"
label="Username"
fullWidth
@@ -49,7 +93,10 @@ class UserForm extends React.Component<UserFormProps> {
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['Password is required', 'Password must be 64 characters or less']}
errorMessages={[
'Password is required',
'Password must be 64 characters or less'
]}
name="password"
label="Password"
fullWidth
@@ -70,10 +117,19 @@ class UserForm extends React.Component<UserFormProps> {
/>
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
<FormButton
variant="contained"
color="secondary"
onClick={onCancelEditing}
>
Cancel
</FormButton>
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
<FormButton
variant="contained"
color="primary"
type="submit"
onClick={this.submit}
>
Done
</FormButton>
</DialogActions>

View File

@@ -1,14 +1,14 @@
export interface User {
username: string
password: string
admin: boolean
username: string;
password: string;
admin: boolean;
}
export interface SecuritySettings {
users: User[]
jwt_secret: string
users: User[];
jwt_secret: string;
}
export interface GeneratedToken {
token: string
token: string;
}

View File

@@ -16,46 +16,46 @@ const isLocalhost = Boolean(
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}$/,
),
)
/^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
}
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)
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
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
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)
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',
)
})
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config)
registerValidSW(swUrl, config);
}
})
});
}
}
@@ -64,9 +64,9 @@ function registerValidSW(swUrl: string, config?: Config) {
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
const installingWorker = registration.installing;
if (installingWorker == null) {
return
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
@@ -76,41 +76,41 @@ function registerValidSW(swUrl: string, config?: Config) {
// 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.',
)
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration)
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.')
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration)
}
}
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error('Error during service worker registration:', 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' },
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')
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
@@ -118,25 +118,25 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload()
})
})
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config)
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.',
)
})
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister()
})
registration.unregister();
});
}
}

View File

@@ -1,4 +1,4 @@
const { createProxyMiddleware } = require('http-proxy-middleware')
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
@@ -6,7 +6,7 @@ module.exports = function (app) {
createProxyMiddleware({
target: 'http://localhost:3080',
secure: false,
changeOrigin: true,
}),
)
}
changeOrigin: true
})
);
};

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { OTA_SETTINGS_ENDPOINT } from '../api';
import OTASettingsForm from './OTASettingsForm';
@@ -9,7 +14,6 @@ import { OTASettings } from './types';
type OTASettingsControllerProps = RestControllerProps<OTASettings>;
class OTASettingsController extends Component<OTASettingsControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class OTASettingsController extends Component<OTASettingsControllerProps> {
<SectionContent title="OTA Settings" titleGutter>
<RestFormLoader
{...this.props}
render={formProps => <OTASettingsForm {...formProps} />}
render={(formProps) => <OTASettingsForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(OTA_SETTINGS_ENDPOINT, OTASettingsController);

View File

@@ -4,7 +4,13 @@ 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 {
RestFormProps,
BlockFormControlLabel,
PasswordValidator,
FormButton,
FormActions
} from '../components';
import { isIP, isHostname, or } from '../validators';
import { OTASettings } from './types';
@@ -12,7 +18,6 @@ import { OTASettings } from './types';
type OTASettingsFormProps = RestFormProps<OTASettings>;
class OTASettingsForm extends React.Component<OTASettingsFormProps> {
componentDidMount() {
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
}
@@ -25,14 +30,24 @@ class OTASettingsForm extends React.Component<OTASettingsFormProps> {
control={
<Checkbox
checked={data.enabled}
onChange={handleValueChange("enabled")}
onChange={handleValueChange('enabled')}
/>
}
label="Enable OTA Updates"
/>
<TextValidator
validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']}
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]}
validators={[
'required',
'isNumber',
'minNumber:1025',
'maxNumber:65535'
]}
errorMessages={[
'Port is required',
'Must be a number',
'Must be greater than 1024 ',
'Max value is 65535'
]}
name="port"
label="Port"
fullWidth
@@ -44,7 +59,10 @@ class OTASettingsForm extends React.Component<OTASettingsFormProps> {
/>
<PasswordValidator
validators={['required', 'matchRegexp:^.{1,64}$']}
errorMessages={['OTA Password is required', 'OTA Point Password must be 64 characters or less']}
errorMessages={[
'OTA Password is required',
'OTA Point Password must be 64 characters or less'
]}
name="password"
label="Password"
fullWidth
@@ -54,7 +72,12 @@ class OTASettingsForm extends React.Component<OTASettingsFormProps> {
margin="normal"
/>
<FormActions>
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
<FormButton
startIcon={<SaveIcon />}
variant="contained"
color="primary"
type="submit"
>
Save
</FormButton>
</FormActions>

View File

@@ -1,22 +1,27 @@
import React, { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
import { Component } from 'react';
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
import { Tabs, Tab } from '@material-ui/core';
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
import {
withAuthenticatedContext,
AuthenticatedContextProps,
AuthenticatedRoute
} from '../authentication';
import { MenuAppBar } from '../components';
import SystemStatusController from './SystemStatusController';
import OTASettingsController from './OTASettingsController';
import UploadFirmwareController from './UploadFirmwareController';
type SystemProps = AuthenticatedContextProps & RouteComponentProps & WithFeaturesProps;
type SystemProps = AuthenticatedContextProps &
RouteComponentProps &
WithFeaturesProps;
class System extends Component<SystemProps> {
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
handleTabChange = (path: string) => {
this.props.history.push(path);
};
@@ -24,27 +29,51 @@ class System extends Component<SystemProps> {
const { authenticatedContext, features } = this.props;
return (
<MenuAppBar sectionTitle="System">
<Tabs value={this.props.match.url} onChange={this.handleTabChange} variant="fullWidth">
<Tabs
value={this.props.match.url}
onChange={(e, path) => this.handleTabChange(path)}
variant="fullWidth"
>
<Tab value="/system/status" label="System Status" />
{features.ota && (
<Tab value="/system/ota" label="OTA Settings" disabled={!authenticatedContext.me.admin} />
<Tab
value="/system/ota"
label="OTA Settings"
disabled={!authenticatedContext.me.admin}
/>
)}
{features.upload_firmware && (
<Tab value="/system/upload" label="Upload Firmware" disabled={!authenticatedContext.me.admin} />
<Tab
value="/system/upload"
label="Upload Firmware"
disabled={!authenticatedContext.me.admin}
/>
)}
</Tabs>
<Switch>
<AuthenticatedRoute exact path="/system/status" component={SystemStatusController} />
<AuthenticatedRoute
exact
path="/system/status"
component={SystemStatusController}
/>
{features.ota && (
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
<AuthenticatedRoute
exact
path="/system/ota"
component={OTASettingsController}
/>
)}
{features.upload_firmware && (
<AuthenticatedRoute exact path="/system/upload" component={UploadFirmwareController} />
<AuthenticatedRoute
exact
path="/system/upload"
component={UploadFirmwareController}
/>
)}
<Redirect to="/system/status" />
</Switch>
</MenuAppBar>
)
);
}
}

View File

@@ -1,6 +1,11 @@
import React, { Component } from 'react';
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
import {
restController,
RestControllerProps,
RestFormLoader,
SectionContent
} from '../components';
import { SYSTEM_STATUS_ENDPOINT } from '../api';
import SystemStatusForm from './SystemStatusForm';
@@ -9,7 +14,6 @@ import { SystemStatus } from './types';
type SystemStatusControllerProps = RestControllerProps<SystemStatus>;
class SystemStatusController extends Component<SystemStatusControllerProps> {
componentDidMount() {
this.props.loadData();
}
@@ -19,12 +23,11 @@ class SystemStatusController extends Component<SystemStatusControllerProps> {
<SectionContent title="System Status">
<RestFormLoader
{...this.props}
render={formProps => <SystemStatusForm {...formProps} />}
render={(formProps) => <SystemStatusForm {...formProps} />}
/>
</SectionContent>
);
}
}
export default restController(SYSTEM_STATUS_ENDPOINT, SystemStatusController);

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react';
import { Component } from 'react';
import { SectionContent } from '../components';
import { UPLOAD_FIRMWARE_ENDPOINT } from '../api';
@@ -12,8 +12,10 @@ interface UploadFirmwareControllerState {
progress?: ProgressEvent;
}
class UploadFirmwareController extends Component<WithSnackbarProps, UploadFirmwareControllerState> {
class UploadFirmwareController extends Component<
WithSnackbarProps,
UploadFirmwareControllerState
> {
state: UploadFirmwareControllerState = {
xhr: undefined,
progress: undefined
@@ -25,47 +27,67 @@ class UploadFirmwareController extends Component<WithSnackbarProps, UploadFirmwa
updateProgress = (progress: ProgressEvent) => {
this.setState({ progress });
}
};
uploadFile = (file: File) => {
if (this.state.xhr) {
return;
}
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
this.setState({ xhr });
redirectingAuthorizedUpload(xhr, UPLOAD_FIRMWARE_ENDPOINT, file, this.updateProgress).then(() => {
redirectingAuthorizedUpload(
xhr,
UPLOAD_FIRMWARE_ENDPOINT,
file,
this.updateProgress
)
.then(() => {
if (xhr.status !== 200) {
throw Error("Invalid status code: " + xhr.status);
throw Error('Invalid status code: ' + xhr.status);
}
this.props.enqueueSnackbar("Activating new firmware", { variant: 'success' });
this.props.enqueueSnackbar('Activating new firmware', {
variant: 'success'
});
this.setState({ xhr: undefined, progress: undefined });
}).catch((error: Error) => {
})
.catch((error: Error) => {
if (error.name === 'AbortError') {
this.props.enqueueSnackbar("Upload cancelled by user", { variant: 'warning' });
this.props.enqueueSnackbar('Upload cancelled by user', {
variant: 'warning'
});
} else {
const errorMessage = error.name === 'UploadError' ? "Error during upload" : (error.message || "Unknown error");
this.props.enqueueSnackbar("Problem uploading: " + errorMessage, { variant: 'error' });
const errorMessage =
error.name === 'UploadError'
? 'Error during upload'
: error.message || 'Unknown error';
this.props.enqueueSnackbar('Problem uploading: ' + errorMessage, {
variant: 'error'
});
this.setState({ xhr: undefined, progress: undefined });
}
});
}
};
cancelUpload = () => {
if (this.state.xhr) {
this.state.xhr.abort();
this.setState({ xhr: undefined, progress: undefined });
}
}
};
render() {
const { xhr, progress } = this.state;
return (
<SectionContent title="Upload Firmware">
<UploadFirmwareForm onFileSelected={this.uploadFile} onCancel={this.cancelUpload} uploading={!!xhr} progress={progress} />
<UploadFirmwareForm
onFileSelected={this.uploadFile}
onCancel={this.cancelUpload}
uploading={!!xhr}
progress={progress}
/>
</SectionContent>
);
}
}
export default withSnackbar(UploadFirmwareController);

Some files were not shown because too many files have changed in this diff Show More