mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59:52 +03:00
eslint
This commit is contained in:
@@ -1,42 +1,42 @@
|
|||||||
const ManifestPlugin = require('webpack-manifest-plugin')
|
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||||
const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
|
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const CompressionPlugin = require('compression-webpack-plugin')
|
const CompressionPlugin = require('compression-webpack-plugin');
|
||||||
const ProgmemGenerator = require('./progmem-generator.js')
|
const ProgmemGenerator = require('./progmem-generator.js');
|
||||||
|
|
||||||
module.exports = function override(config, env) {
|
module.exports = function override(config, env) {
|
||||||
const hosted = process.env.REACT_APP_HOSTED
|
const hosted = process.env.REACT_APP_HOSTED;
|
||||||
|
|
||||||
if (env === 'production' && !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
|
// rename the output file, we need it's path to be short for LittleFS
|
||||||
config.output.filename = 'js/[id].[chunkhash:4].js'
|
config.output.filename = 'js/[id].[chunkhash:4].js';
|
||||||
config.output.chunkFilename = 'js/[id].[chunkhash:4].js'
|
config.output.chunkFilename = 'js/[id].[chunkhash:4].js';
|
||||||
|
|
||||||
// take out the manifest and service worker plugins
|
// take out the manifest and service worker plugins
|
||||||
config.plugins = config.plugins.filter(
|
config.plugins = config.plugins.filter(
|
||||||
(plugin) => !(plugin instanceof ManifestPlugin),
|
(plugin) => !(plugin instanceof ManifestPlugin)
|
||||||
)
|
);
|
||||||
config.plugins = config.plugins.filter(
|
config.plugins = config.plugins.filter(
|
||||||
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW),
|
(plugin) => !(plugin instanceof WorkboxWebpackPlugin.GenerateSW)
|
||||||
)
|
);
|
||||||
|
|
||||||
// shorten css filenames
|
// shorten css filenames
|
||||||
const miniCssExtractPlugin = config.plugins.find(
|
const miniCssExtractPlugin = config.plugins.find(
|
||||||
(plugin) => plugin instanceof MiniCssExtractPlugin,
|
(plugin) => plugin instanceof MiniCssExtractPlugin
|
||||||
)
|
);
|
||||||
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css'
|
miniCssExtractPlugin.options.filename = 'css/[id].[contenthash:4].css';
|
||||||
miniCssExtractPlugin.options.chunkFilename =
|
miniCssExtractPlugin.options.chunkFilename =
|
||||||
'css/[id].[contenthash:4].c.css'
|
'css/[id].[contenthash:4].c.css';
|
||||||
|
|
||||||
// build progmem data files
|
// build progmem data files
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new ProgmemGenerator({
|
new ProgmemGenerator({
|
||||||
outputPath: '../lib/framework/WWWData.h',
|
outputPath: '../lib/framework/WWWData.h',
|
||||||
bytesPerLine: 20,
|
bytesPerLine: 20
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
// add compression plugin, compress javascript
|
// add compression plugin, compress javascript
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
@@ -44,9 +44,9 @@ module.exports = function override(config, env) {
|
|||||||
filename: '[path].gz[query]',
|
filename: '[path].gz[query]',
|
||||||
algorithm: 'gzip',
|
algorithm: 'gzip',
|
||||||
test: /\.(js)$/,
|
test: /\.(js)$/,
|
||||||
deleteOriginalAssets: true,
|
deleteOriginalAssets: true
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return config
|
return config;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
const { resolve, relative, sep } = require('path')
|
const { resolve, relative, sep } = require('path');
|
||||||
const {
|
const {
|
||||||
readdirSync,
|
readdirSync,
|
||||||
existsSync,
|
existsSync,
|
||||||
unlinkSync,
|
unlinkSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
createWriteStream,
|
createWriteStream
|
||||||
} = require('fs')
|
} = require('fs');
|
||||||
var zlib = require('zlib')
|
var zlib = require('zlib');
|
||||||
var mime = require('mime-types')
|
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 = []) {
|
function getFilesSync(dir, files = []) {
|
||||||
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
readdirSync(dir, { withFileTypes: true }).forEach((entry) => {
|
||||||
const entryPath = resolve(dir, entry.name)
|
const entryPath = resolve(dir, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
getFilesSync(entryPath, files)
|
getFilesSync(entryPath, files);
|
||||||
} else {
|
} else {
|
||||||
files.push(entryPath)
|
files.push(entryPath);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return files
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function coherseToBuffer(input) {
|
function coherseToBuffer(input) {
|
||||||
return Buffer.isBuffer(input) ? input : Buffer.from(input)
|
return Buffer.isBuffer(input) ? input : Buffer.from(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanAndOpen(path) {
|
function cleanAndOpen(path) {
|
||||||
if (existsSync(path)) {
|
if (existsSync(path)) {
|
||||||
unlinkSync(path)
|
unlinkSync(path);
|
||||||
}
|
}
|
||||||
return createWriteStream(path, { flags: 'w+' })
|
return createWriteStream(path, { flags: 'w+' });
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProgmemGenerator {
|
class ProgmemGenerator {
|
||||||
@@ -40,70 +40,70 @@ class ProgmemGenerator {
|
|||||||
outputPath,
|
outputPath,
|
||||||
bytesPerLine = 20,
|
bytesPerLine = 20,
|
||||||
indent = ' ',
|
indent = ' ',
|
||||||
includes = ARDUINO_INCLUDES,
|
includes = ARDUINO_INCLUDES
|
||||||
} = options
|
} = options;
|
||||||
this.options = { outputPath, bytesPerLine, indent, includes }
|
this.options = { outputPath, bytesPerLine, indent, includes };
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(compiler) {
|
apply(compiler) {
|
||||||
compiler.hooks.emit.tapAsync(
|
compiler.hooks.emit.tapAsync(
|
||||||
{ name: 'ProgmemGenerator' },
|
{ name: 'ProgmemGenerator' },
|
||||||
(compilation, callback) => {
|
(compilation, callback) => {
|
||||||
const { outputPath, bytesPerLine, indent, includes } = this.options
|
const { outputPath, bytesPerLine, indent, includes } = this.options;
|
||||||
const fileInfo = []
|
const fileInfo = [];
|
||||||
const writeStream = cleanAndOpen(
|
const writeStream = cleanAndOpen(
|
||||||
resolve(compilation.options.context, outputPath),
|
resolve(compilation.options.context, outputPath)
|
||||||
)
|
);
|
||||||
try {
|
try {
|
||||||
const writeIncludes = () => {
|
const writeIncludes = () => {
|
||||||
writeStream.write(includes)
|
writeStream.write(includes);
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeFile = (relativeFilePath, buffer) => {
|
const writeFile = (relativeFilePath, buffer) => {
|
||||||
const variable = 'ESP_REACT_DATA_' + fileInfo.length
|
const variable = 'ESP_REACT_DATA_' + fileInfo.length;
|
||||||
const mimeType = mime.lookup(relativeFilePath)
|
const mimeType = mime.lookup(relativeFilePath);
|
||||||
var size = 0
|
var size = 0;
|
||||||
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {')
|
writeStream.write('const uint8_t ' + variable + '[] PROGMEM = {');
|
||||||
const zipBuffer = zlib.gzipSync(buffer)
|
const zipBuffer = zlib.gzipSync(buffer);
|
||||||
zipBuffer.forEach((b) => {
|
zipBuffer.forEach((b) => {
|
||||||
if (!(size % bytesPerLine)) {
|
if (!(size % bytesPerLine)) {
|
||||||
writeStream.write('\n')
|
writeStream.write('\n');
|
||||||
writeStream.write(indent)
|
writeStream.write(indent);
|
||||||
}
|
}
|
||||||
writeStream.write(
|
writeStream.write(
|
||||||
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ',',
|
'0x' + ('00' + b.toString(16).toUpperCase()).substr(-2) + ','
|
||||||
)
|
);
|
||||||
size++
|
size++;
|
||||||
})
|
});
|
||||||
if (size % bytesPerLine) {
|
if (size % bytesPerLine) {
|
||||||
writeStream.write('\n')
|
writeStream.write('\n');
|
||||||
}
|
}
|
||||||
writeStream.write('};\n\n')
|
writeStream.write('};\n\n');
|
||||||
fileInfo.push({
|
fileInfo.push({
|
||||||
uri: '/' + relativeFilePath.replace(sep, '/'),
|
uri: '/' + relativeFilePath.replace(sep, '/'),
|
||||||
mimeType,
|
mimeType,
|
||||||
variable,
|
variable,
|
||||||
size,
|
size
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeFiles = () => {
|
const writeFiles = () => {
|
||||||
// process static files
|
// process static files
|
||||||
const buildPath = compilation.options.output.path
|
const buildPath = compilation.options.output.path;
|
||||||
for (const filePath of getFilesSync(buildPath)) {
|
for (const filePath of getFilesSync(buildPath)) {
|
||||||
const readStream = readFileSync(filePath)
|
const readStream = readFileSync(filePath);
|
||||||
const relativeFilePath = relative(buildPath, filePath)
|
const relativeFilePath = relative(buildPath, filePath);
|
||||||
writeFile(relativeFilePath, readStream)
|
writeFile(relativeFilePath, readStream);
|
||||||
}
|
}
|
||||||
// process assets
|
// process assets
|
||||||
const { assets } = compilation
|
const { assets } = compilation;
|
||||||
Object.keys(assets).forEach((relativeFilePath) => {
|
Object.keys(assets).forEach((relativeFilePath) => {
|
||||||
writeFile(
|
writeFile(
|
||||||
relativeFilePath,
|
relativeFilePath,
|
||||||
coherseToBuffer(assets[relativeFilePath].source()),
|
coherseToBuffer(assets[relativeFilePath].source())
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const generateWWWClass = () => {
|
const generateWWWClass = () => {
|
||||||
return `typedef std::function<void(const String& uri, const String& contentType, const uint8_t * content, size_t len)> RouteRegistrationHandler;
|
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 {
|
class WWWData {
|
||||||
${indent}public:
|
${indent}public:
|
||||||
${indent.repeat(
|
${indent.repeat(
|
||||||
2,
|
2
|
||||||
)}static void registerRoutes(RouteRegistrationHandler handler) {
|
)}static void registerRoutes(RouteRegistrationHandler handler) {
|
||||||
${fileInfo
|
${fileInfo
|
||||||
.map(
|
.map(
|
||||||
(file) =>
|
(file) =>
|
||||||
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${
|
`${indent.repeat(3)}handler("${file.uri}", "${file.mimeType}", ${
|
||||||
file.variable
|
file.variable
|
||||||
}, ${file.size});`,
|
}, ${file.size});`
|
||||||
)
|
)
|
||||||
.join('\n')}
|
.join('\n')}
|
||||||
${indent.repeat(2)}}
|
${indent.repeat(2)}}
|
||||||
};
|
};
|
||||||
`
|
`;
|
||||||
}
|
};
|
||||||
|
|
||||||
const writeWWWClass = () => {
|
const writeWWWClass = () => {
|
||||||
writeStream.write(generateWWWClass())
|
writeStream.write(generateWWWClass());
|
||||||
}
|
};
|
||||||
|
|
||||||
writeIncludes()
|
writeIncludes();
|
||||||
writeFiles()
|
writeFiles();
|
||||||
writeWWWClass()
|
writeWWWClass();
|
||||||
|
|
||||||
writeStream.on('finish', () => {
|
writeStream.on('finish', () => {
|
||||||
callback()
|
callback();
|
||||||
})
|
});
|
||||||
} finally {
|
} finally {
|
||||||
writeStream.end()
|
writeStream.end();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ProgmemGenerator
|
module.exports = ProgmemGenerator;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import FeaturesWrapper from './features/FeaturesWrapper';
|
|||||||
const unauthorizedRedirect = () => <Redirect to="/" />;
|
const unauthorizedRedirect = () => <Redirect to="/" />;
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
|
|
||||||
notistackRef: RefObject<any> = React.createRef();
|
notistackRef: RefObject<any> = React.createRef();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -23,21 +22,29 @@ class App extends Component {
|
|||||||
|
|
||||||
onClickDismiss = (key: string | number | undefined) => () => {
|
onClickDismiss = (key: string | number | undefined) => () => {
|
||||||
this.notistackRef.current.closeSnackbar(key);
|
this.notistackRef.current.closeSnackbar(key);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<CustomMuiTheme>
|
<CustomMuiTheme>
|
||||||
<SnackbarProvider autoHideDuration={3000} maxSnack={3} anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
<SnackbarProvider
|
||||||
|
autoHideDuration={3000}
|
||||||
|
maxSnack={3}
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
ref={this.notistackRef}
|
ref={this.notistackRef}
|
||||||
action={(key) => (
|
action={(key) => (
|
||||||
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
<IconButton onClick={this.onClickDismiss(key)} size="small">
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<FeaturesWrapper>
|
<FeaturesWrapper>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/unauthorized" component={unauthorizedRedirect} />
|
<Route
|
||||||
|
exact
|
||||||
|
path="/unauthorized"
|
||||||
|
component={unauthorizedRedirect}
|
||||||
|
/>
|
||||||
<Route component={AppRouting} />
|
<Route component={AppRouting} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</FeaturesWrapper>
|
</FeaturesWrapper>
|
||||||
@@ -47,4 +54,4 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import Mqtt from './mqtt/Mqtt';
|
|||||||
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
import { withFeatures, WithFeaturesProps } from './features/FeaturesContext';
|
||||||
import { Features } from './features/types';
|
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> {
|
class AppRouting extends Component<WithFeaturesProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
Authentication.clearLoginRedirect();
|
Authentication.clearLoginRedirect();
|
||||||
}
|
}
|
||||||
@@ -35,9 +35,17 @@ class AppRouting extends Component<WithFeaturesProps> {
|
|||||||
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
<UnauthenticatedRoute exact path="/" component={SignIn} />
|
||||||
)}
|
)}
|
||||||
{features.project && (
|
{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} />
|
<AuthenticatedRoute exact path="/ap/*" component={AccessPoint} />
|
||||||
{features.ntp && (
|
{features.ntp && (
|
||||||
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
<AuthenticatedRoute exact path="/ntp/*" component={NetworkTime} />
|
||||||
@@ -52,7 +60,7 @@ class AppRouting extends Component<WithFeaturesProps> {
|
|||||||
<Redirect to={getDefaultRoute(features)} />
|
<Redirect to={getDefaultRoute(features)} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</AuthenticationWrapper>
|
</AuthenticationWrapper>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { CssBaseline } from '@material-ui/core';
|
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';
|
import { blueGrey, orange, red, green } from '@material-ui/core/colors';
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const theme = createMuiTheme({
|
||||||
palette: {
|
palette: {
|
||||||
type: "dark",
|
type: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#33bfff',
|
main: '#33bfff'
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: '#3d5afe',
|
main: '#3d5afe'
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
main: blueGrey[500]
|
main: blueGrey[500]
|
||||||
@@ -29,7 +33,6 @@ const theme = createMuiTheme({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default class CustomMuiTheme extends Component {
|
export default class CustomMuiTheme extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<StylesProvider>
|
<StylesProvider>
|
||||||
@@ -40,5 +43,4 @@ export default class CustomMuiTheme extends Component {
|
|||||||
</StylesProvider>
|
</StylesProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,53 +2,63 @@ import React, { Component } from 'react';
|
|||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
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 { Paper, Typography, Fab } from '@material-ui/core';
|
||||||
import ForwardIcon from '@material-ui/icons/Forward';
|
import ForwardIcon from '@material-ui/icons/Forward';
|
||||||
|
|
||||||
import { withAuthenticationContext, AuthenticationContextProps } from './authentication/AuthenticationContext';
|
import {
|
||||||
|
withAuthenticationContext,
|
||||||
|
AuthenticationContextProps
|
||||||
|
} from './authentication/AuthenticationContext';
|
||||||
import { PasswordValidator } from './components';
|
import { PasswordValidator } from './components';
|
||||||
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
import { PROJECT_NAME, SIGN_IN_ENDPOINT } from './api';
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
signInPage: {
|
signInPage: {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
height: "100vh",
|
height: '100vh',
|
||||||
margin: "auto",
|
margin: 'auto',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
maxWidth: theme.breakpoints.values.sm
|
maxWidth: theme.breakpoints.values.sm
|
||||||
},
|
},
|
||||||
signInPanel: {
|
signInPanel: {
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
paddingTop: "200px",
|
paddingTop: '200px',
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: "50% " + theme.spacing(2) + "px",
|
backgroundPosition: '50% ' + theme.spacing(2) + 'px',
|
||||||
backgroundSize: "auto 150px",
|
backgroundSize: 'auto 150px',
|
||||||
width: "100%"
|
width: '100%'
|
||||||
},
|
},
|
||||||
extendedIcon: {
|
extendedIcon: {
|
||||||
marginRight: theme.spacing(0.5),
|
marginRight: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
marginRight: theme.spacing(2),
|
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 {
|
interface SignInState {
|
||||||
username: string,
|
username: string;
|
||||||
password: string,
|
password: string;
|
||||||
processing: boolean
|
processing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SignIn extends Component<SignInProps, SignInState> {
|
class SignIn extends Component<SignInProps, SignInState> {
|
||||||
|
|
||||||
constructor(props: SignInProps) {
|
constructor(props: SignInProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -60,10 +70,10 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
|
|
||||||
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
updateInputElement = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
const { name, value } = event.currentTarget;
|
const { name, value } = event.currentTarget;
|
||||||
this.setState(prevState => ({
|
this.setState((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
[name]: value,
|
[name]: value
|
||||||
}))
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
@@ -77,20 +87,21 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else if (response.status === 401) {
|
} else if (response.status === 401) {
|
||||||
throw Error("Invalid credentials.");
|
throw Error('Invalid credentials.');
|
||||||
} else {
|
} else {
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error('Invalid status code: ' + response.status);
|
||||||
}
|
}
|
||||||
}).then(json => {
|
})
|
||||||
|
.then((json) => {
|
||||||
authenticationContext.signIn(json.access_token);
|
authenticationContext.signIn(json.access_token);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message, {
|
this.props.enqueueSnackbar(error.message, {
|
||||||
variant: 'warning',
|
variant: 'warning'
|
||||||
});
|
});
|
||||||
this.setState({ processing: false });
|
this.setState({ processing: false });
|
||||||
});
|
});
|
||||||
@@ -116,8 +127,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
onChange={this.updateInputElement}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
inputProps={{
|
inputProps={{
|
||||||
autoCapitalize: "none",
|
autoCapitalize: 'none',
|
||||||
autoCorrect: "off",
|
autoCorrect: 'off'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
@@ -132,7 +143,13 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
onChange={this.updateInputElement}
|
onChange={this.updateInputElement}
|
||||||
margin="normal"
|
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} />
|
<ForwardIcon className={classes.extendedIcon} />
|
||||||
Sign In
|
Sign In
|
||||||
</Fab>
|
</Fab>
|
||||||
@@ -141,7 +158,8 @@ class SignIn extends Component<SignInProps, SignInState> {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticationContext(withSnackbar(withStyles(styles)(SignIn)));
|
export default withAuthenticationContext(
|
||||||
|
withSnackbar(withStyles(styles)(SignIn))
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { APSettings, APProvisionMode } from './types'
|
import { APSettings, APProvisionMode } from './types';
|
||||||
|
|
||||||
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||||
return (
|
return (
|
||||||
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
|
||||||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED
|
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { AP_SETTINGS_ENDPOINT } from '../api';
|
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 APSettingsForm from './APSettingsForm';
|
||||||
import { APSettings } from './types';
|
import { APSettings } from './types';
|
||||||
@@ -9,7 +14,6 @@ import { APSettings } from './types';
|
|||||||
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
type APSettingsControllerProps = RestControllerProps<APSettings>;
|
||||||
|
|
||||||
class APSettingsController extends Component<APSettingsControllerProps> {
|
class APSettingsController extends Component<APSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class APSettingsController extends Component<APSettingsControllerProps> {
|
|||||||
<SectionContent title="Access Point Settings" titleGutter>
|
<SectionContent title="Access Point Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <APSettingsForm {...formProps} />}
|
render={(formProps) => <APSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
export default restController(AP_SETTINGS_ENDPOINT, APSettingsController);
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import React, { Fragment } from 'react';
|
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 MenuItem from '@material-ui/core/MenuItem';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
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 { isAPEnabled } from './APModes';
|
||||||
import { APSettings, APProvisionMode } from './types';
|
import { APSettings, APProvisionMode } from './types';
|
||||||
@@ -13,7 +22,6 @@ import { isIP } from '../validators';
|
|||||||
type APSettingsFormProps = RestFormProps<APSettings>;
|
type APSettingsFormProps = RestFormProps<APSettings>;
|
||||||
|
|
||||||
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
class APSettingsForm extends React.Component<APSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIP', isIP);
|
ValidatorForm.addValidationRule('isIP', isIP);
|
||||||
}
|
}
|
||||||
@@ -22,23 +30,29 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
<ValidatorForm onSubmit={saveData} ref="APSettingsForm">
|
||||||
<SelectValidator name="provision_mode"
|
<SelectValidator
|
||||||
|
name="provision_mode"
|
||||||
label="Provide Access Point…"
|
label="Provide Access Point…"
|
||||||
value={data.provision_mode}
|
value={data.provision_mode}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange('provision_mode')}
|
onChange={handleValueChange('provision_mode')}
|
||||||
margin="normal">
|
margin="normal"
|
||||||
|
>
|
||||||
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
|
<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>
|
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
{
|
{isAPEnabled(data) && (
|
||||||
isAPEnabled(data) &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'matchRegexp:^.{1,32}$']}
|
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"
|
name="ssid"
|
||||||
label="Access Point SSID"
|
label="Access Point SSID"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -49,7 +63,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['required', 'matchRegexp:^.{8,64}$']}
|
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"
|
name="password"
|
||||||
label="Access Point Password"
|
label="Access Point Password"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -71,7 +88,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
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"
|
name="gateway_ip"
|
||||||
label="Gateway"
|
label="Gateway"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -82,7 +102,10 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
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"
|
name="subnet_mask"
|
||||||
label="Subnet"
|
label="Subnet"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -92,9 +115,14 @@ class APSettingsForm extends React.Component<APSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { Theme } from '@material-ui/core'
|
import { Theme } from '@material-ui/core';
|
||||||
import { APStatus, APNetworkStatus } from './types'
|
import { APStatus, APNetworkStatus } from './types';
|
||||||
|
|
||||||
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case APNetworkStatus.ACTIVE:
|
case APNetworkStatus.ACTIVE:
|
||||||
return theme.palette.success.main
|
return theme.palette.success.main;
|
||||||
case APNetworkStatus.INACTIVE:
|
case APNetworkStatus.INACTIVE:
|
||||||
return theme.palette.info.main
|
return theme.palette.info.main;
|
||||||
case APNetworkStatus.LINGERING:
|
case APNetworkStatus.LINGERING:
|
||||||
return theme.palette.warning.main
|
return theme.palette.warning.main;
|
||||||
default:
|
default:
|
||||||
return theme.palette.warning.main
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const apStatus = ({ status }: APStatus) => {
|
export const apStatus = ({ status }: APStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case APNetworkStatus.ACTIVE:
|
case APNetworkStatus.ACTIVE:
|
||||||
return 'Active'
|
return 'Active';
|
||||||
case APNetworkStatus.INACTIVE:
|
case APNetworkStatus.INACTIVE:
|
||||||
return 'Inactive'
|
return 'Inactive';
|
||||||
case APNetworkStatus.LINGERING:
|
case APNetworkStatus.LINGERING:
|
||||||
return 'Lingering until idle'
|
return 'Lingering until idle';
|
||||||
default:
|
default:
|
||||||
return 'Unknown'
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 { AP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import APStatusForm from './APStatusForm';
|
import APStatusForm from './APStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { APStatus } from './types';
|
|||||||
type APStatusControllerProps = RestControllerProps<APStatus>;
|
type APStatusControllerProps = RestControllerProps<APStatus>;
|
||||||
|
|
||||||
class APStatusController extends Component<APStatusControllerProps> {
|
class APStatusController extends Component<APStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,10 +23,10 @@ class APStatusController extends Component<APStatusControllerProps> {
|
|||||||
<SectionContent title="Access Point Status">
|
<SectionContent title="Access Point Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <APStatusForm {...formProps} />}
|
render={(formProps) => <APStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
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 { 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 SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import ComputerIcon from '@material-ui/icons/Computer';
|
import ComputerIcon from '@material-ui/icons/Computer';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
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 { apStatusHighlight, apStatus } from './APStatus';
|
||||||
import { APStatus } from './types';
|
import { APStatus } from './types';
|
||||||
|
|
||||||
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
type APStatusFormProps = RestFormProps<APStatus> & WithTheme;
|
||||||
|
|
||||||
class APStatusForm extends Component<APStatusFormProps> {
|
class APStatusForm extends Component<APStatusFormProps> {
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -61,18 +72,20 @@ class APStatusForm extends Component<APStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(APStatusForm);
|
export default withTheme(APStatusForm);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import APSettingsController from './APSettingsController';
|
import APSettingsController from './APSettingsController';
|
||||||
@@ -12,8 +16,7 @@ import APStatusController from './APStatusController';
|
|||||||
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
type AccessPointProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class AccessPoint extends Component<AccessPointProps> {
|
class AccessPoint extends Component<AccessPointProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,17 +24,33 @@ class AccessPoint extends Component<AccessPointProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Access Point">
|
<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/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>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/ap/status" component={APStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/ap/settings" component={APSettingsController} />
|
exact
|
||||||
|
path="/ap/status"
|
||||||
|
component={APStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/ap/settings"
|
||||||
|
component={APSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/ap/status" />
|
<Redirect to="/ap/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
export enum APProvisionMode {
|
export enum APProvisionMode {
|
||||||
AP_MODE_ALWAYS = 0,
|
AP_MODE_ALWAYS = 0,
|
||||||
AP_MODE_DISCONNECTED = 1,
|
AP_MODE_DISCONNECTED = 1,
|
||||||
AP_NEVER = 2,
|
AP_NEVER = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum APNetworkStatus {
|
export enum APNetworkStatus {
|
||||||
ACTIVE = 0,
|
ACTIVE = 0,
|
||||||
INACTIVE = 1,
|
INACTIVE = 1,
|
||||||
LINGERING = 2,
|
LINGERING = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APStatus {
|
export interface APStatus {
|
||||||
status: APNetworkStatus
|
status: APNetworkStatus;
|
||||||
ip_address: string
|
ip_address: string;
|
||||||
mac_address: string
|
mac_address: string;
|
||||||
station_num: number
|
station_num: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APSettings {
|
export interface APSettings {
|
||||||
provision_mode: APProvisionMode
|
provision_mode: APProvisionMode;
|
||||||
ssid: string
|
ssid: string;
|
||||||
password: string
|
password: string;
|
||||||
local_ip: string
|
local_ip: string;
|
||||||
gateway_ip: string
|
gateway_ip: string;
|
||||||
subnet_mask: string
|
subnet_mask: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { ENDPOINT_ROOT } from './Env'
|
import { ENDPOINT_ROOT } from './Env';
|
||||||
|
|
||||||
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features'
|
export const FEATURES_ENDPOINT = ENDPOINT_ROOT + 'features';
|
||||||
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus'
|
export const NTP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'ntpStatus';
|
||||||
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings'
|
export const NTP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'ntpSettings';
|
||||||
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time'
|
export const TIME_ENDPOINT = ENDPOINT_ROOT + 'time';
|
||||||
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings'
|
export const AP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'apSettings';
|
||||||
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus'
|
export const AP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'apStatus';
|
||||||
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks'
|
export const SCAN_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'scanNetworks';
|
||||||
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks'
|
export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + 'listNetworks';
|
||||||
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings'
|
export const NETWORK_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'networkSettings';
|
||||||
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus'
|
export const NETWORK_STATUS_ENDPOINT = ENDPOINT_ROOT + 'networkStatus';
|
||||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings'
|
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'otaSettings';
|
||||||
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware'
|
export const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + 'uploadFirmware';
|
||||||
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings'
|
export const MQTT_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'mqttSettings';
|
||||||
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus'
|
export const MQTT_STATUS_ENDPOINT = ENDPOINT_ROOT + 'mqttStatus';
|
||||||
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus'
|
export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + 'systemStatus';
|
||||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn'
|
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + 'signIn';
|
||||||
export const VERIFY_AUTHORIZATION_ENDPOINT =
|
export const VERIFY_AUTHORIZATION_ENDPOINT =
|
||||||
ENDPOINT_ROOT + 'verifyAuthorization'
|
ENDPOINT_ROOT + 'verifyAuthorization';
|
||||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings'
|
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'securitySettings';
|
||||||
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken'
|
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + 'generateToken';
|
||||||
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart'
|
export const RESTART_ENDPOINT = ENDPOINT_ROOT + 'restart';
|
||||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset'
|
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + 'factoryReset';
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!
|
export const PROJECT_NAME = process.env.REACT_APP_PROJECT_NAME!;
|
||||||
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!
|
export const PROJECT_PATH = process.env.REACT_APP_PROJECT_PATH!;
|
||||||
|
|
||||||
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/')
|
export const ENDPOINT_ROOT = calculateEndpointRoot('/rest/');
|
||||||
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/')
|
export const WEB_SOCKET_ROOT = calculateWebSocketRoot('/ws/');
|
||||||
|
|
||||||
function calculateEndpointRoot(endpointPath: string) {
|
function calculateEndpointRoot(endpointPath: string) {
|
||||||
const httpRoot = process.env.REACT_APP_HTTP_ROOT
|
const httpRoot = process.env.REACT_APP_HTTP_ROOT;
|
||||||
if (httpRoot) {
|
if (httpRoot) {
|
||||||
return httpRoot + endpointPath
|
return httpRoot + endpointPath;
|
||||||
}
|
}
|
||||||
const location = window.location
|
const location = window.location;
|
||||||
return location.protocol + '//' + location.host + endpointPath
|
return location.protocol + '//' + location.host + endpointPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateWebSocketRoot(webSocketPath: string) {
|
function calculateWebSocketRoot(webSocketPath: string) {
|
||||||
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT
|
const webSocketRoot = process.env.REACT_APP_WEB_SOCKET_ROOT;
|
||||||
if (webSocketRoot) {
|
if (webSocketRoot) {
|
||||||
return webSocketRoot + webSocketPath
|
return webSocketRoot + webSocketPath;
|
||||||
}
|
}
|
||||||
const location = window.location
|
const location = window.location;
|
||||||
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
const webProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
return webProtocol + '//' + location.host + webSocketPath
|
return webProtocol + '//' + location.host + webSocketPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './Env'
|
export * from './Env';
|
||||||
export * from './Endpoints'
|
export * from './Endpoints';
|
||||||
|
|||||||
@@ -1,40 +1,56 @@
|
|||||||
import * as React from 'react';
|
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 { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
|
|
||||||
import * as Authentication from './Authentication';
|
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 {
|
interface AuthenticatedRouteProps
|
||||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
extends RouteProps,
|
||||||
|
WithSnackbarProps,
|
||||||
|
AuthenticationContextProps {
|
||||||
|
component:
|
||||||
|
| React.ComponentType<RouteComponentProps<any>>
|
||||||
|
| React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
export class AuthenticatedRoute extends React.Component<AuthenticatedRouteProps> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { enqueueSnackbar, authenticationContext, component: Component, ...rest } = this.props;
|
const {
|
||||||
|
enqueueSnackbar,
|
||||||
|
authenticationContext,
|
||||||
|
component: Component,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
const renderComponent: RenderComponent = (props) => {
|
const renderComponent: RenderComponent = (props) => {
|
||||||
if (authenticationContext.me) {
|
if (authenticationContext.me) {
|
||||||
return (
|
return (
|
||||||
<AuthenticatedContext.Provider value={authenticationContext as AuthenticatedContextValue}>
|
<AuthenticatedContext.Provider
|
||||||
|
value={authenticationContext as AuthenticatedContextValue}
|
||||||
|
>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</AuthenticatedContext.Provider>
|
</AuthenticatedContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Authentication.storeLoginRedirect(location);
|
Authentication.storeLoginRedirect(location);
|
||||||
enqueueSnackbar("Please sign in to continue", { variant: 'info' });
|
enqueueSnackbar('Please sign in to continue', { variant: 'info' });
|
||||||
return (
|
return <Redirect to="/" />;
|
||||||
<Redirect to='/' />
|
};
|
||||||
);
|
return <Route {...rest} render={renderComponent} />;
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<Route {...rest} render={renderComponent} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
export default withSnackbar(withAuthenticationContext(AuthenticatedRoute));
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import * as H from 'history'
|
import * as H from 'history';
|
||||||
|
|
||||||
import history from '../history'
|
import history from '../history';
|
||||||
import { Features } from '../features/types'
|
import { Features } from '../features/types';
|
||||||
import { getDefaultRoute } from '../AppRouting'
|
import { getDefaultRoute } from '../AppRouting';
|
||||||
|
|
||||||
export const ACCESS_TOKEN = 'access_token'
|
export const ACCESS_TOKEN = 'access_token';
|
||||||
export const SIGN_IN_PATHNAME = 'signInPathname'
|
export const SIGN_IN_PATHNAME = 'signInPathname';
|
||||||
export const SIGN_IN_SEARCH = 'signInSearch'
|
export const SIGN_IN_SEARCH = 'signInSearch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
|
* Fallback to sessionStorage if localStorage is absent. WebView may not have local storage enabled.
|
||||||
*/
|
*/
|
||||||
export function getStorage() {
|
export function getStorage() {
|
||||||
return localStorage || sessionStorage
|
return localStorage || sessionStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function storeLoginRedirect(location?: H.Location) {
|
export function storeLoginRedirect(location?: H.Location) {
|
||||||
if (location) {
|
if (location) {
|
||||||
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname)
|
getStorage().setItem(SIGN_IN_PATHNAME, location.pathname);
|
||||||
getStorage().setItem(SIGN_IN_SEARCH, location.search)
|
getStorage().setItem(SIGN_IN_SEARCH, location.search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearLoginRedirect() {
|
export function clearLoginRedirect() {
|
||||||
getStorage().removeItem(SIGN_IN_PATHNAME)
|
getStorage().removeItem(SIGN_IN_PATHNAME);
|
||||||
getStorage().removeItem(SIGN_IN_SEARCH)
|
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect(
|
export function fetchLoginRedirect(
|
||||||
features: Features,
|
features: Features
|
||||||
): H.LocationDescriptorObject {
|
): H.LocationDescriptorObject {
|
||||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME)
|
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH)
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
clearLoginRedirect()
|
clearLoginRedirect();
|
||||||
return {
|
return {
|
||||||
pathname: signInPathname || getDefaultRoute(features),
|
pathname: signInPathname || getDefaultRoute(features),
|
||||||
search: (signInPathname && signInSearch) || undefined,
|
search: (signInPathname && signInSearch) || undefined
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,18 +44,18 @@ export function fetchLoginRedirect(
|
|||||||
*/
|
*/
|
||||||
export function authorizedFetch(
|
export function authorizedFetch(
|
||||||
url: RequestInfo,
|
url: RequestInfo,
|
||||||
params?: RequestInit,
|
params?: RequestInit
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
params = params || {}
|
params = params || {};
|
||||||
params.credentials = 'include'
|
params.credentials = 'include';
|
||||||
params.headers = {
|
params.headers = {
|
||||||
...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,
|
xhr: XMLHttpRequest,
|
||||||
url: string,
|
url: string,
|
||||||
file: File,
|
file: File,
|
||||||
onProgress: (event: ProgressEvent<EventTarget>) => void,
|
onProgress: (event: ProgressEvent<EventTarget>) => void
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
xhr.open('POST', url, true)
|
xhr.open('POST', url, true);
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
xhr.withCredentials = true
|
xhr.withCredentials = true;
|
||||||
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken)
|
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
|
||||||
}
|
}
|
||||||
xhr.upload.onprogress = onProgress
|
xhr.upload.onprogress = onProgress;
|
||||||
xhr.onload = function () {
|
xhr.onload = function () {
|
||||||
if (xhr.status === 401 || xhr.status === 403) {
|
if (xhr.status === 401 || xhr.status === 403) {
|
||||||
history.push('/unauthorized')
|
history.push('/unauthorized');
|
||||||
} else {
|
} else {
|
||||||
resolve()
|
resolve();
|
||||||
}
|
|
||||||
}
|
|
||||||
xhr.onerror = function (event: ProgressEvent<EventTarget>) {
|
|
||||||
reject(new DOMException('Error', 'UploadError'))
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
reject(new DOMException('Error', 'UploadError'));
|
||||||
|
};
|
||||||
xhr.onabort = function () {
|
xhr.onabort = function () {
|
||||||
reject(new DOMException('Aborted', 'AbortError'))
|
reject(new DOMException('Aborted', 'AbortError'));
|
||||||
}
|
};
|
||||||
const formData = new FormData()
|
const formData = new FormData();
|
||||||
formData.append('file', file)
|
formData.append('file', file);
|
||||||
xhr.send(formData)
|
xhr.send(formData);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,29 +101,29 @@ export function redirectingAuthorizedUpload(
|
|||||||
*/
|
*/
|
||||||
export function redirectingAuthorizedFetch(
|
export function redirectingAuthorizedFetch(
|
||||||
url: RequestInfo,
|
url: RequestInfo,
|
||||||
params?: RequestInit,
|
params?: RequestInit
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
return new Promise<Response>((resolve, reject) => {
|
return new Promise<Response>((resolve, reject) => {
|
||||||
authorizedFetch(url, params)
|
authorizedFetch(url, params)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
history.push('/unauthorized')
|
history.push('/unauthorized');
|
||||||
} else {
|
} else {
|
||||||
resolve(response)
|
resolve(response);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(error)
|
reject(error);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addAccessTokenParameter(url: string) {
|
export function addAccessTokenParameter(url: string) {
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return url
|
return url;
|
||||||
}
|
}
|
||||||
const parsedUrl = new URL(url)
|
const parsedUrl = new URL(url);
|
||||||
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken)
|
parsedUrl.searchParams.set(ACCESS_TOKEN, accessToken);
|
||||||
return parsedUrl.toString()
|
return parsedUrl.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
|
|
||||||
export interface Me {
|
export interface Me {
|
||||||
username: string;
|
username: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
version: string; // proddy added
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticationContextValue {
|
export interface AuthenticationContextValue {
|
||||||
@@ -13,7 +12,7 @@ export interface AuthenticationContextValue {
|
|||||||
me?: Me;
|
me?: Me;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue
|
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;
|
||||||
export const AuthenticationContext = React.createContext(
|
export const AuthenticationContext = React.createContext(
|
||||||
AuthenticationContextDefaultValue
|
AuthenticationContextDefaultValue
|
||||||
);
|
);
|
||||||
@@ -22,12 +21,21 @@ export interface AuthenticationContextProps {
|
|||||||
authenticationContext: AuthenticationContextValue;
|
authenticationContext: AuthenticationContextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withAuthenticationContext<T extends AuthenticationContextProps>(Component: React.ComponentType<T>) {
|
export function withAuthenticationContext<T extends AuthenticationContextProps>(
|
||||||
return class extends React.Component<Omit<T, keyof AuthenticationContextProps>> {
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
|
return class extends React.Component<
|
||||||
|
Omit<T, keyof AuthenticationContextProps>
|
||||||
|
> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AuthenticationContext.Consumer>
|
<AuthenticationContext.Consumer>
|
||||||
{authenticationContext => <Component {...this.props as T} authenticationContext={authenticationContext} />}
|
{(authenticationContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
authenticationContext={authenticationContext}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AuthenticationContext.Consumer>
|
</AuthenticationContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -38,7 +46,7 @@ export interface AuthenticatedContextValue extends AuthenticationContextValue {
|
|||||||
me: Me;
|
me: Me;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue
|
const AuthenticatedContextDefaultValue = {} as AuthenticatedContextValue;
|
||||||
export const AuthenticatedContext = React.createContext(
|
export const AuthenticatedContext = React.createContext(
|
||||||
AuthenticatedContextDefaultValue
|
AuthenticatedContextDefaultValue
|
||||||
);
|
);
|
||||||
@@ -47,12 +55,21 @@ export interface AuthenticatedContextProps {
|
|||||||
authenticatedContext: AuthenticatedContextValue;
|
authenticatedContext: AuthenticatedContextValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(Component: React.ComponentType<T>) {
|
export function withAuthenticatedContext<T extends AuthenticatedContextProps>(
|
||||||
return class extends React.Component<Omit<T, keyof AuthenticatedContextProps>> {
|
Component: React.ComponentType<T>
|
||||||
|
) {
|
||||||
|
return class extends React.Component<
|
||||||
|
Omit<T, keyof AuthenticatedContextProps>
|
||||||
|
> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<AuthenticatedContext.Consumer>
|
<AuthenticatedContext.Consumer>
|
||||||
{authenticatedContext => <Component {...this.props as T} authenticatedContext={authenticatedContext} />}
|
{(authenticatedContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
authenticatedContext={authenticatedContext}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AuthenticatedContext.Consumer>
|
</AuthenticatedContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import * as React from 'react';
|
|||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
||||||
import jwtDecode from 'jwt-decode';
|
import jwtDecode from 'jwt-decode';
|
||||||
|
|
||||||
import history from '../history'
|
import history from '../history';
|
||||||
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../api';
|
||||||
import { ACCESS_TOKEN, authorizedFetch, getStorage } from './Authentication';
|
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 FullScreenLoading from '../components/FullScreenLoading';
|
||||||
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
|
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 {
|
interface AuthenticationWrapperState {
|
||||||
context: AuthenticationContextValue;
|
context: AuthenticationContextValue;
|
||||||
@@ -18,15 +23,17 @@ interface AuthenticationWrapperState {
|
|||||||
|
|
||||||
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
type AuthenticationWrapperProps = WithSnackbarProps & WithFeaturesProps;
|
||||||
|
|
||||||
class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps, AuthenticationWrapperState> {
|
class AuthenticationWrapper extends React.Component<
|
||||||
|
AuthenticationWrapperProps,
|
||||||
|
AuthenticationWrapperState
|
||||||
|
> {
|
||||||
constructor(props: AuthenticationWrapperProps) {
|
constructor(props: AuthenticationWrapperProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
context: {
|
context: {
|
||||||
refresh: this.refresh,
|
refresh: this.refresh,
|
||||||
signIn: this.signIn,
|
signIn: this.signIn,
|
||||||
signOut: this.signOut,
|
signOut: this.signOut
|
||||||
},
|
},
|
||||||
initialized: false
|
initialized: false
|
||||||
};
|
};
|
||||||
@@ -39,7 +46,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{this.state.initialized ? this.renderContent() : this.renderContentLoading()}
|
{this.state.initialized
|
||||||
|
? this.renderContent()
|
||||||
|
: this.renderContentLoading()}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,9 +62,7 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderContentLoading() {
|
renderContentLoading() {
|
||||||
return (
|
return <FullScreenLoading />;
|
||||||
<FullScreenLoading />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh = () => {
|
refresh = () => {
|
||||||
@@ -64,34 +71,53 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
// this.setState({ initialized: true, context: { ...this.state.context, me: { admin: true, username: "admin" } } });
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
const accessToken = getStorage().getItem(ACCESS_TOKEN)
|
const accessToken = getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
const me = response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
const me =
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me } });
|
response.status === 200 ? decodeMeJWT(accessToken) : undefined;
|
||||||
}).catch(error => {
|
this.setState({
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
initialized: true,
|
||||||
this.props.enqueueSnackbar("Error verifying authorization: " + error.message, {
|
context: { ...this.state.context, me }
|
||||||
variant: 'error',
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.setState({
|
||||||
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
'Error verifying authorization: ' + error.message,
|
||||||
|
{
|
||||||
|
variant: 'error'
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
this.setState({
|
||||||
}
|
initialized: true,
|
||||||
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
signIn = (accessToken: string) => {
|
signIn = (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
getStorage().setItem(ACCESS_TOKEN, accessToken);
|
||||||
const me: Me = decodeMeJWT(accessToken);
|
const me: Me = decodeMeJWT(accessToken);
|
||||||
this.setState({ context: { ...this.state.context, me } });
|
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) {
|
} catch (err) {
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, me: undefined } });
|
this.setState({
|
||||||
throw new Error("Failed to parse JWT " + err.message);
|
initialized: true,
|
||||||
}
|
context: { ...this.state.context, me: undefined }
|
||||||
|
});
|
||||||
|
throw new Error('Failed to parse JWT ' + err.message);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
signOut = () => {
|
signOut = () => {
|
||||||
getStorage().removeItem(ACCESS_TOKEN);
|
getStorage().removeItem(ACCESS_TOKEN);
|
||||||
@@ -101,10 +127,9 @@ class AuthenticationWrapper extends React.Component<AuthenticationWrapperProps,
|
|||||||
me: undefined
|
me: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.props.enqueueSnackbar("You have signed out", { variant: 'success', });
|
this.props.enqueueSnackbar('You have signed out', { variant: 'success' });
|
||||||
history.push('/');
|
history.push('/');
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withFeatures(withSnackbar(AuthenticationWrapper))
|
export default withFeatures(withSnackbar(AuthenticationWrapper));
|
||||||
|
|||||||
@@ -1,31 +1,46 @@
|
|||||||
import * as React from 'react';
|
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 * as Authentication from './Authentication';
|
||||||
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||||
|
|
||||||
interface UnauthenticatedRouteProps extends RouteProps, AuthenticationContextProps, WithFeaturesProps {
|
interface UnauthenticatedRouteProps
|
||||||
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
|
extends RouteProps,
|
||||||
|
AuthenticationContextProps,
|
||||||
|
WithFeaturesProps {
|
||||||
|
component:
|
||||||
|
| React.ComponentType<RouteComponentProps<any>>
|
||||||
|
| React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||||
|
|
||||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { authenticationContext, component: Component, features, ...rest } = this.props;
|
const {
|
||||||
|
authenticationContext,
|
||||||
|
component: Component,
|
||||||
|
features,
|
||||||
|
...rest
|
||||||
|
} = this.props;
|
||||||
const renderComponent: RenderComponent = (props) => {
|
const renderComponent: RenderComponent = (props) => {
|
||||||
if (authenticationContext.me) {
|
if (authenticationContext.me) {
|
||||||
return (<Redirect to={Authentication.fetchLoginRedirect(features)} />);
|
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
|
||||||
}
|
}
|
||||||
if (Component) {
|
if (Component) {
|
||||||
return (<Component {...props} />);
|
return <Component {...props} />;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
return (
|
return <Route {...rest} render={renderComponent} />;
|
||||||
<Route {...rest} render={renderComponent} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export { default as AuthenticatedRoute } from './AuthenticatedRoute'
|
export { default as AuthenticatedRoute } from './AuthenticatedRoute';
|
||||||
export { default as AuthenticationWrapper } from './AuthenticationWrapper'
|
export { default as AuthenticationWrapper } from './AuthenticationWrapper';
|
||||||
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute'
|
export { default as UnauthenticatedRoute } from './UnauthenticatedRoute';
|
||||||
|
|
||||||
export * from './Authentication'
|
export * from './Authentication';
|
||||||
export * from './AuthenticationContext'
|
export * from './AuthenticationContext';
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { makeStyles } from '@material-ui/styles';
|
import { makeStyles } from '@material-ui/styles';
|
||||||
import { Paper, Typography, Box, CssBaseline } from "@material-ui/core";
|
import { Paper, Typography, Box, CssBaseline } from '@material-ui/core';
|
||||||
import WarningIcon from "@material-ui/icons/Warning"
|
import WarningIcon from '@material-ui/icons/Warning';
|
||||||
|
|
||||||
const styles = makeStyles(
|
const styles = makeStyles({
|
||||||
{
|
|
||||||
siteErrorPage: {
|
siteErrorPage: {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
height: "100vh",
|
height: '100vh',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
flexDirection: "column"
|
flexDirection: 'column'
|
||||||
},
|
},
|
||||||
siteErrorPagePanel: {
|
siteErrorPagePanel: {
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
padding: "280px 0 40px 0",
|
padding: '280px 0 40px 0',
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
backgroundRepeat: "no-repeat",
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: "50% 40px",
|
backgroundPosition: '50% 40px',
|
||||||
backgroundSize: "200px auto",
|
backgroundSize: '200px auto',
|
||||||
width: "100%",
|
width: '100%'
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
interface ApplicationErrorProps {
|
interface ApplicationErrorProps {
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -33,27 +31,29 @@ const ApplicationError: FC<ApplicationErrorProps> = ({ error }) => {
|
|||||||
<div className={classes.siteErrorPage}>
|
<div className={classes.siteErrorPage}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Paper className={classes.siteErrorPagePanel} elevation={10}>
|
<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" />
|
<WarningIcon fontSize="large" color="error" />
|
||||||
<Box ml={2}>
|
<Box ml={2}>
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">Application error</Typography>
|
||||||
Application error
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
<Typography variant="subtitle1" gutterBottom>
|
||||||
Failed to configure the application, please refresh to try again.
|
Failed to configure the application, please refresh to try again.
|
||||||
</Typography>
|
</Typography>
|
||||||
{error &&
|
{error && (
|
||||||
(
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ApplicationError;
|
export default ApplicationError;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { FC } from "react";
|
import { FC } from 'react';
|
||||||
import { FormControlLabel, FormControlLabelProps } from "@material-ui/core";
|
import { FormControlLabel, FormControlLabelProps } from '@material-ui/core';
|
||||||
|
|
||||||
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
const BlockFormControlLabel: FC<FormControlLabelProps> = (props) => (
|
||||||
<div>
|
<div>
|
||||||
<FormControlLabel {...props} />
|
<FormControlLabel {...props} />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default BlockFormControlLabel;
|
export default BlockFormControlLabel;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Button, styled } from "@material-ui/core";
|
import { Button, styled } from '@material-ui/core';
|
||||||
|
|
||||||
const ErrorButton = styled(Button)(({ theme }) => ({
|
const ErrorButton = styled(Button)(({ theme }) => ({
|
||||||
color: theme.palette.getContrastText(theme.palette.error.main),
|
color: theme.palette.getContrastText(theme.palette.error.main),
|
||||||
backgroundColor: theme.palette.error.main,
|
backgroundColor: theme.palette.error.main,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.error.dark,
|
backgroundColor: theme.palette.error.dark
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { styled, Box } from "@material-ui/core";
|
import { styled, Box } from '@material-ui/core';
|
||||||
|
|
||||||
const FormActions = styled(Box)(({ theme }) => ({
|
const FormActions = styled(Box)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(1)
|
marginTop: theme.spacing(1)
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Button, styled } from "@material-ui/core";
|
import { Button, styled } from '@material-ui/core';
|
||||||
|
|
||||||
const FormButton = styled(Button)(({ theme }) => ({
|
const FormButton = styled(Button)(({ theme }) => ({
|
||||||
margin: theme.spacing(0, 1),
|
margin: theme.spacing(0, 1),
|
||||||
'&:last-child': {
|
'&:last-child': {
|
||||||
marginRight: 0,
|
marginRight: 0
|
||||||
},
|
},
|
||||||
'&:first-child': {
|
'&:first-child': {
|
||||||
marginLeft: 0,
|
marginLeft: 0
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -3,30 +3,30 @@ import CircularProgress from '@material-ui/core/CircularProgress';
|
|||||||
import { Typography, Theme } from '@material-ui/core';
|
import { Typography, Theme } from '@material-ui/core';
|
||||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
fullScreenLoading: {
|
fullScreenLoading: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
height: "100vh",
|
height: '100vh',
|
||||||
flexDirection: "column"
|
flexDirection: 'column'
|
||||||
},
|
},
|
||||||
progress: {
|
progress: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4)
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const FullScreenLoading = () => {
|
const FullScreenLoading = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
<div className={classes.fullScreenLoading}>
|
<div className={classes.fullScreenLoading}>
|
||||||
<CircularProgress className={classes.progress} size={100} />
|
<CircularProgress className={classes.progress} size={100} />
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">Loading…</Typography>
|
||||||
Loading…
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FullScreenLoading;
|
export default FullScreenLoading;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Avatar, makeStyles } from "@material-ui/core";
|
import { Avatar, makeStyles } from '@material-ui/core';
|
||||||
import React, { FC } from "react";
|
import { FC } from 'react';
|
||||||
|
|
||||||
interface HighlightAvatarProps {
|
interface HighlightAvatarProps {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -13,11 +13,7 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
const HighlightAvatar: FC<HighlightAvatarProps> = (props) => {
|
||||||
const classes = useStyles(props);
|
const classes = useStyles(props);
|
||||||
return (
|
return <Avatar className={classes.root}>{props.children}</Avatar>;
|
||||||
<Avatar className={classes.root}>
|
};
|
||||||
{props.children}
|
|
||||||
</Avatar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HighlightAvatar;
|
export default HighlightAvatar;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import React from 'react';
|
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 { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
|
||||||
import { InputAdornment, IconButton } from '@material-ui/core';
|
import { InputAdornment, IconButton } from '@material-ui/core';
|
||||||
@@ -7,20 +10,23 @@ import { Visibility, VisibilityOff } from '@material-ui/icons';
|
|||||||
|
|
||||||
const styles = createStyles({
|
const styles = createStyles({
|
||||||
input: {
|
input: {
|
||||||
"&::-ms-reveal": {
|
'&::-ms-reveal': {
|
||||||
display: "none"
|
display: 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
type PasswordValidatorProps = WithStyles<typeof styles> &
|
||||||
|
Exclude<ValidatorComponentProps, 'type' | 'InputProps'>;
|
||||||
|
|
||||||
interface PasswordValidatorState {
|
interface PasswordValidatorState {
|
||||||
showPassword: boolean;
|
showPassword: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
class PasswordValidator extends React.Component<
|
||||||
|
PasswordValidatorProps,
|
||||||
|
PasswordValidatorState
|
||||||
|
> {
|
||||||
state = {
|
state = {
|
||||||
showPassword: false
|
showPassword: false
|
||||||
};
|
};
|
||||||
@@ -29,7 +35,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
this.setState({
|
this.setState({
|
||||||
showPassword: !this.state.showPassword
|
showPassword: !this.state.showPassword
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { classes, ...rest } = this.props;
|
const { classes, ...rest } = this.props;
|
||||||
@@ -39,7 +45,7 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
type={this.state.showPassword ? 'text' : 'password'}
|
type={this.state.showPassword ? 'text' : 'password'}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
classes,
|
classes,
|
||||||
endAdornment:
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Toggle password visibility"
|
aria-label="Toggle password visibility"
|
||||||
@@ -48,11 +54,11 @@ class PasswordValidator extends React.Component<PasswordValidatorProps, Password
|
|||||||
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(PasswordValidator);
|
export default withStyles(styles)(PasswordValidator);
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { withSnackbar, WithSnackbarProps } from 'notistack';
|
|||||||
import { redirectingAuthorizedFetch } from '../authentication';
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
|
|
||||||
export interface RestControllerProps<D> extends WithSnackbarProps {
|
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;
|
setData: (data: D, callback?: () => void) => void;
|
||||||
saveData: () => void;
|
saveData: () => void;
|
||||||
@@ -15,16 +17,18 @@ export interface RestControllerProps<D> extends WithSnackbarProps {
|
|||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
|
export const extractEventValue = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
switch (event.target.type) {
|
switch (event.target.type) {
|
||||||
case "number":
|
case 'number':
|
||||||
return event.target.valueAsNumber;
|
return event.target.valueAsNumber;
|
||||||
case "checkbox":
|
case 'checkbox':
|
||||||
return event.target.checked;
|
return event.target.checked;
|
||||||
default:
|
default:
|
||||||
return event.target.value
|
return event.target.value;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
interface RestControllerState<D> {
|
interface RestControllerState<D> {
|
||||||
data?: D;
|
data?: D;
|
||||||
@@ -32,10 +36,15 @@ interface RestControllerState<D> {
|
|||||||
errorMessage?: string;
|
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(
|
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> = {
|
state: RestControllerState<D> = {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -43,12 +52,15 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
};
|
};
|
||||||
|
|
||||||
setData = (data: D, callback?: () => void) => {
|
setData = (data: D, callback?: () => void) => {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
data,
|
data,
|
||||||
loading: false,
|
loading: false,
|
||||||
errorMessage: undefined
|
errorMessage: undefined
|
||||||
}, callback);
|
},
|
||||||
}
|
callback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
loadData = () => {
|
loadData = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -56,19 +68,24 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
loading: true,
|
loading: true,
|
||||||
errorMessage: undefined
|
errorMessage: undefined
|
||||||
});
|
});
|
||||||
redirectingAuthorizedFetch(endpointUrl).then(response => {
|
redirectingAuthorizedFetch(endpointUrl)
|
||||||
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error('Invalid status code: ' + response.status);
|
||||||
}).then(json => {
|
})
|
||||||
this.setState({ data: json, loading: false })
|
.then((json) => {
|
||||||
}).catch(error => {
|
this.setState({ data: json, loading: false });
|
||||||
const errorMessage = error.message || "Unknown error";
|
})
|
||||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
|
.catch((error) => {
|
||||||
|
const errorMessage = error.message || 'Unknown error';
|
||||||
|
this.props.enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
this.setState({ data: undefined, loading: false, errorMessage });
|
this.setState({ data: undefined, loading: false, errorMessage });
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
saveData = () => {
|
saveData = () => {
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
@@ -78,36 +95,47 @@ export function restController<D, P extends RestControllerProps<D>>(endpointUrl:
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}).then(response => {
|
})
|
||||||
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error('Invalid status code: ' + response.status);
|
||||||
}).then(json => {
|
})
|
||||||
this.props.enqueueSnackbar("Update successful.", { variant: 'success' });
|
.then((json) => {
|
||||||
|
this.props.enqueueSnackbar('Update successful.', {
|
||||||
|
variant: 'success'
|
||||||
|
});
|
||||||
this.setState({ data: json, loading: false });
|
this.setState({ data: json, loading: false });
|
||||||
}).catch(error => {
|
})
|
||||||
const errorMessage = error.message || "Unknown error";
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar("Problem updating: " + errorMessage, { variant: 'error' });
|
const errorMessage = error.message || 'Unknown error';
|
||||||
|
this.props.enqueueSnackbar('Problem updating: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
this.setState({ data: undefined, loading: false, errorMessage });
|
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) };
|
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <RestController
|
return (
|
||||||
|
<RestController
|
||||||
{...this.state}
|
{...this.state}
|
||||||
{...this.props as P}
|
{...(this.props as P)}
|
||||||
handleValueChange={this.handleValueChange}
|
handleValueChange={this.handleValueChange}
|
||||||
setData={this.setData}
|
setData={this.setData}
|
||||||
saveData={this.saveData}
|
saveData={this.saveData}
|
||||||
loadData={this.loadData}
|
loadData={this.loadData}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,23 @@ import { RestControllerProps } from '.';
|
|||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
loadingSettings: {
|
loadingSettings: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
loadingSettingsDetails: {
|
loadingSettingsDetails: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
textAlign: "center"
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
marginRight: theme.spacing(2),
|
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> {
|
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
|
||||||
render: (props: RestFormProps<D>) => JSX.Element;
|
render: (props: RestFormProps<D>) => JSX.Element;
|
||||||
@@ -46,7 +49,12 @@ export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
|
|||||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className={classes.button}
|
||||||
|
onClick={loadData}
|
||||||
|
>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
createStyles({
|
createStyles({
|
||||||
content: {
|
content: {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
margin: theme.spacing(3),
|
margin: theme.spacing(3)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,13 +4,20 @@ import { useDropzone, DropzoneState } from 'react-dropzone';
|
|||||||
import { makeStyles, createStyles } from '@material-ui/styles';
|
import { makeStyles, createStyles } from '@material-ui/styles';
|
||||||
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
|
||||||
import CancelIcon from '@material-ui/icons/Cancel';
|
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 {
|
interface SingleUploadStyleProps extends DropzoneState {
|
||||||
uploading: boolean;
|
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) => {
|
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
||||||
if (props.isDragAccept) {
|
if (props.isDragAccept) {
|
||||||
@@ -23,9 +30,10 @@ const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
|
|||||||
return theme.palette.info.main;
|
return theme.palette.info.main;
|
||||||
}
|
}
|
||||||
return theme.palette.grey[700];
|
return theme.palette.grey[700];
|
||||||
}
|
};
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => createStyles({
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
dropzone: {
|
dropzone: {
|
||||||
padding: theme.spacing(8, 2),
|
padding: theme.spacing(8, 2),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -33,11 +41,14 @@ const useStyles = makeStyles((theme: Theme) => createStyles({
|
|||||||
borderStyle: 'dashed',
|
borderStyle: 'dashed',
|
||||||
color: theme.palette.grey[700],
|
color: theme.palette.grey[700],
|
||||||
transition: 'border .24s ease-in-out',
|
transition: 'border .24s ease-in-out',
|
||||||
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
|
cursor: (props: SingleUploadStyleProps) =>
|
||||||
|
props.uploading ? 'default' : 'pointer',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
|
borderColor: (props: SingleUploadStyleProps) =>
|
||||||
|
getBorderColor(theme, props)
|
||||||
}
|
}
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export interface SingleUploadProps {
|
export interface SingleUploadProps {
|
||||||
onDrop: (acceptedFiles: File[]) => void;
|
onDrop: (acceptedFiles: File[]) => void;
|
||||||
@@ -47,26 +58,44 @@ export interface SingleUploadProps {
|
|||||||
progress?: ProgressEvent;
|
progress?: ProgressEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
|
const SingleUpload: FC<SingleUploadProps> = ({
|
||||||
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
|
onDrop,
|
||||||
|
onCancel,
|
||||||
|
accept,
|
||||||
|
uploading,
|
||||||
|
progress
|
||||||
|
}) => {
|
||||||
|
const dropzoneState = useDropzone({
|
||||||
|
onDrop,
|
||||||
|
accept,
|
||||||
|
disabled: uploading,
|
||||||
|
multiple: false
|
||||||
|
});
|
||||||
const { getRootProps, getInputProps } = dropzoneState;
|
const { getRootProps, getInputProps } = dropzoneState;
|
||||||
const classes = useStyles({ ...dropzoneState, uploading });
|
const classes = useStyles({ ...dropzoneState, uploading });
|
||||||
|
|
||||||
|
|
||||||
const renderProgressText = () => {
|
const renderProgressText = () => {
|
||||||
if (uploading) {
|
if (uploading) {
|
||||||
if (progress?.lengthComputable) {
|
if (progress?.lengthComputable) {
|
||||||
return `Uploading: ${progressPercentage(progress)}%`;
|
return `Uploading: ${progressPercentage(progress)}%`;
|
||||||
}
|
}
|
||||||
return "Uploading\u2026";
|
return 'Uploading\u2026';
|
||||||
}
|
|
||||||
return "Drop file or click here";
|
|
||||||
}
|
}
|
||||||
|
return 'Drop file or click here';
|
||||||
|
};
|
||||||
|
|
||||||
const renderProgress = (progress?: ProgressEvent) => (
|
const renderProgress = (progress?: ProgressEvent) => (
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
|
variant={
|
||||||
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
|
!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 })}>
|
<div {...getRootProps({ className: classes.dropzone })}>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<Box flexDirection="column" display="flex" alignItems="center">
|
<Box flexDirection="column" display="flex" alignItems="center">
|
||||||
<CloudUploadIcon fontSize='large' />
|
<CloudUploadIcon fontSize="large" />
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">{renderProgressText()}</Typography>
|
||||||
{renderProgressText()}
|
|
||||||
</Typography>
|
|
||||||
{uploading && (
|
{uploading && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Box width="100%" p={2}>
|
<Box width="100%" p={2}>
|
||||||
{renderProgress(progress)}
|
{renderProgress(progress)}
|
||||||
</Box>
|
</Box>
|
||||||
<Button startIcon={<CancelIcon />} variant="contained" color="secondary" onClick={onCancel}>
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -91,6 +123,6 @@ const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploadi
|
|||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SingleUpload;
|
export default SingleUpload;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { addAccessTokenParameter } from '../authentication';
|
|||||||
import { extractEventValue } from '.';
|
import { extractEventValue } from '.';
|
||||||
|
|
||||||
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
|
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;
|
setData: (data: D, callback?: () => void) => void;
|
||||||
saveData: () => void;
|
saveData: () => void;
|
||||||
@@ -25,8 +27,8 @@ interface WebSocketControllerState<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum WebSocketMessageType {
|
enum WebSocketMessageType {
|
||||||
ID = "id",
|
ID = 'id',
|
||||||
PAYLOAD = "payload"
|
PAYLOAD = 'payload'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebSocketIdMessage {
|
interface WebSocketIdMessage {
|
||||||
@@ -40,21 +42,32 @@ interface WebSocketPayloadMessage<D> {
|
|||||||
payload: 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(
|
return withSnackbar(
|
||||||
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
|
class extends React.Component<
|
||||||
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
|
Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps,
|
||||||
|
WebSocketControllerState<D>
|
||||||
|
> {
|
||||||
|
constructor(
|
||||||
|
props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps
|
||||||
|
) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
ws: new Sockette(addAccessTokenParameter(wsUrl), {
|
||||||
onmessage: this.onMessage,
|
onmessage: this.onMessage,
|
||||||
onopen: this.onOpen,
|
onopen: this.onOpen,
|
||||||
onclose: this.onClose,
|
onclose: this.onClose
|
||||||
}),
|
}),
|
||||||
connected: false
|
connected: false
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -64,37 +77,42 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
|||||||
onMessage = (event: MessageEvent) => {
|
onMessage = (event: MessageEvent) => {
|
||||||
const rawData = event.data;
|
const rawData = event.data;
|
||||||
if (typeof rawData === 'string' || rawData instanceof String) {
|
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>) => {
|
handleMessage = (message: WebSocketMessage<D>) => {
|
||||||
|
const { clientId, data } = this.state;
|
||||||
|
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case WebSocketMessageType.ID:
|
case WebSocketMessageType.ID:
|
||||||
this.setState({ clientId: message.id });
|
this.setState({ clientId: message.id });
|
||||||
break;
|
break;
|
||||||
case WebSocketMessageType.PAYLOAD:
|
case WebSocketMessageType.PAYLOAD:
|
||||||
const { clientId, data } = this.state;
|
|
||||||
if (clientId && (!data || clientId !== message.origin_id)) {
|
if (clientId && (!data || clientId !== message.origin_id)) {
|
||||||
this.setState(
|
this.setState({ data: message.payload });
|
||||||
{ data: message.payload }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onOpen = () => {
|
onOpen = () => {
|
||||||
this.setState({ connected: true });
|
this.setState({ connected: true });
|
||||||
}
|
};
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.setState({ connected: false, clientId: undefined, data: undefined });
|
this.setState({
|
||||||
}
|
connected: false,
|
||||||
|
clientId: undefined,
|
||||||
|
data: undefined
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
setData = (data: D, callback?: () => void) => {
|
setData = (data: D, callback?: () => void) => {
|
||||||
this.setState({ data }, callback);
|
this.setState({ data }, callback);
|
||||||
}
|
};
|
||||||
|
|
||||||
saveData = throttle(() => {
|
saveData = throttle(() => {
|
||||||
const { ws, connected, data } = this.state;
|
const { ws, connected, data } = this.state;
|
||||||
@@ -106,28 +124,35 @@ export function webSocketController<D, P extends WebSocketControllerProps<D>>(ws
|
|||||||
saveDataAndClear = throttle(() => {
|
saveDataAndClear = throttle(() => {
|
||||||
const { ws, connected, data } = this.state;
|
const { ws, connected, data } = this.state;
|
||||||
if (connected) {
|
if (connected) {
|
||||||
this.setState({
|
this.setState(
|
||||||
|
{
|
||||||
data: undefined
|
data: undefined
|
||||||
}, () => ws.json(data));
|
},
|
||||||
|
() => ws.json(data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, wsThrottle);
|
}, 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) };
|
const data = { ...this.state.data!, [name]: extractEventValue(event) };
|
||||||
this.setState({ data });
|
this.setState({ data });
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <WebSocketController
|
return (
|
||||||
{...this.props as P}
|
<WebSocketController
|
||||||
|
{...(this.props as P)}
|
||||||
handleValueChange={this.handleValueChange}
|
handleValueChange={this.handleValueChange}
|
||||||
setData={this.setData}
|
setData={this.setData}
|
||||||
saveData={this.saveData}
|
saveData={this.saveData}
|
||||||
saveDataAndClear={this.saveDataAndClear}
|
saveDataAndClear={this.saveDataAndClear}
|
||||||
connected={this.state.connected}
|
connected={this.state.connected}
|
||||||
data={this.state.data}
|
data={this.state.data}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
import { LinearProgress, Typography } from '@material-ui/core';
|
import { LinearProgress, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -8,22 +6,27 @@ import { WebSocketControllerProps } from '.';
|
|||||||
const useStyles = makeStyles((theme: Theme) =>
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
loadingSettings: {
|
loadingSettings: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
loadingSettingsDetails: {
|
loadingSettingsDetails: {
|
||||||
margin: theme.spacing(4),
|
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> {
|
interface WebSocketFormLoaderProps<D> extends WebSocketControllerProps<D> {
|
||||||
render: (props: WebSocketFormProps<D>) => JSX.Element;
|
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 { connected, render, data, ...rest } = props;
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
if (!connected || !data) {
|
if (!connected || !data) {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
export { default as BlockFormControlLabel } from './BlockFormControlLabel'
|
export { default as BlockFormControlLabel } from './BlockFormControlLabel';
|
||||||
export { default as FormActions } from './FormActions'
|
export { default as FormActions } from './FormActions';
|
||||||
export { default as FormButton } from './FormButton'
|
export { default as FormButton } from './FormButton';
|
||||||
export { default as HighlightAvatar } from './HighlightAvatar'
|
export { default as HighlightAvatar } from './HighlightAvatar';
|
||||||
export { default as MenuAppBar } from './MenuAppBar'
|
export { default as MenuAppBar } from './MenuAppBar';
|
||||||
export { default as PasswordValidator } from './PasswordValidator'
|
export { default as PasswordValidator } from './PasswordValidator';
|
||||||
export { default as RestFormLoader } from './RestFormLoader'
|
export { default as RestFormLoader } from './RestFormLoader';
|
||||||
export { default as SectionContent } from './SectionContent'
|
export { default as SectionContent } from './SectionContent';
|
||||||
export { default as WebSocketFormLoader } from './WebSocketFormLoader'
|
export { default as WebSocketFormLoader } from './WebSocketFormLoader';
|
||||||
export { default as ErrorButton } from './ErrorButton'
|
export { default as ErrorButton } from './ErrorButton';
|
||||||
export { default as SingleUpload } from './SingleUpload'
|
export { default as SingleUpload } from './SingleUpload';
|
||||||
|
|
||||||
export * from './RestFormLoader'
|
export * from './RestFormLoader';
|
||||||
export * from './RestController'
|
export * from './RestController';
|
||||||
|
|
||||||
export * from './WebSocketFormLoader'
|
export * from './WebSocketFormLoader';
|
||||||
export * from './WebSocketController'
|
export * from './WebSocketController';
|
||||||
|
|||||||
@@ -5,21 +5,26 @@ export interface FeaturesContextValue {
|
|||||||
features: Features;
|
features: Features;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeaturesContextDefaultValue = {} as FeaturesContextValue
|
const FeaturesContextDefaultValue = {} as FeaturesContextValue;
|
||||||
export const FeaturesContext = React.createContext(
|
export const FeaturesContext = React.createContext(FeaturesContextDefaultValue);
|
||||||
FeaturesContextDefaultValue
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface WithFeaturesProps {
|
export interface WithFeaturesProps {
|
||||||
features: Features;
|
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>> {
|
return class extends React.Component<Omit<T, keyof WithFeaturesProps>> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<FeaturesContext.Consumer>
|
<FeaturesContext.Consumer>
|
||||||
{featuresContext => <Component {...this.props as T} features={featuresContext.features} />}
|
{(featuresContext) => (
|
||||||
|
<Component
|
||||||
|
{...(this.props as T)}
|
||||||
|
features={featuresContext.features}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</FeaturesContext.Consumer>
|
</FeaturesContext.Consumer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { Features } from './types';
|
import { Features } from './types';
|
||||||
import { FeaturesContext } from './FeaturesContext';
|
import { FeaturesContext } from './FeaturesContext';
|
||||||
@@ -9,10 +9,9 @@ import { FEATURES_ENDPOINT } from '../api';
|
|||||||
interface FeaturesWrapperState {
|
interface FeaturesWrapperState {
|
||||||
features?: Features;
|
features?: Features;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||||
|
|
||||||
state: FeaturesWrapperState = {};
|
state: FeaturesWrapperState = {};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -21,41 +20,39 @@ class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
|||||||
|
|
||||||
fetchFeaturesDetails = () => {
|
fetchFeaturesDetails = () => {
|
||||||
fetch(FEATURES_ENDPOINT)
|
fetch(FEATURES_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else {
|
} else {
|
||||||
throw Error("Unexpected status code: " + response.status);
|
throw Error('Unexpected status code: ' + response.status);
|
||||||
}
|
}
|
||||||
}).then(features => {
|
})
|
||||||
|
.then((features) => {
|
||||||
this.setState({ features });
|
this.setState({ features });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.setState({ error: error.message });
|
this.setState({ error: error.message });
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { features, error } = this.state;
|
const { features, error } = this.state;
|
||||||
if (features) {
|
if (features) {
|
||||||
return (
|
return (
|
||||||
<FeaturesContext.Provider value={{
|
<FeaturesContext.Provider
|
||||||
|
value={{
|
||||||
features
|
features
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</FeaturesContext.Provider>
|
</FeaturesContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return <ApplicationError error={error} />;
|
||||||
<ApplicationError error={error} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
return <FullScreenLoading />;
|
||||||
<FullScreenLoading />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FeaturesWrapper;
|
export default FeaturesWrapper;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export interface Features {
|
export interface Features {
|
||||||
project: boolean
|
project: boolean;
|
||||||
security: boolean
|
security: boolean;
|
||||||
mqtt: boolean
|
mqtt: boolean;
|
||||||
ntp: boolean
|
ntp: boolean;
|
||||||
ota: boolean
|
ota: boolean;
|
||||||
upload_firmware: boolean
|
upload_firmware: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createBrowserHistory } from 'history'
|
import { createBrowserHistory } from 'history';
|
||||||
|
|
||||||
export default createBrowserHistory({
|
export default createBrowserHistory({
|
||||||
/* pass a configuration object here if needed */
|
/* pass a configuration object here if needed */
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { Router } from 'react-router';
|
|||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
render((
|
render(
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<App />
|
<App />
|
||||||
</Router>
|
</Router>,
|
||||||
), document.getElementById("root"))
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { AuthenticatedContextProps, withAuthenticatedContext, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
import MqttStatusController from './MqttStatusController';
|
import MqttStatusController from './MqttStatusController';
|
||||||
import MqttSettingsController from './MqttSettingsController';
|
import MqttSettingsController from './MqttSettingsController';
|
||||||
@@ -11,8 +15,7 @@ import MqttSettingsController from './MqttSettingsController';
|
|||||||
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
type MqttProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class Mqtt extends Component<MqttProps> {
|
class Mqtt extends Component<MqttProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,17 +23,33 @@ class Mqtt extends Component<MqttProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="MQTT">
|
<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/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>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/mqtt/status" component={MqttStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/mqtt/settings" component={MqttSettingsController} />
|
exact
|
||||||
|
path="/mqtt/status"
|
||||||
|
component={MqttStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/mqtt/settings"
|
||||||
|
component={MqttSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/mqtt/status" />
|
<Redirect to="/mqtt/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
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 { MQTT_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import MqttSettingsForm from './MqttSettingsForm';
|
import MqttSettingsForm from './MqttSettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { MqttSettings } from './types';
|
|||||||
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
type MqttSettingsControllerProps = RestControllerProps<MqttSettings>;
|
||||||
|
|
||||||
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class MqttSettingsController extends Component<MqttSettingsControllerProps> {
|
|||||||
<SectionContent title="MQTT Settings" titleGutter>
|
<SectionContent title="MQTT Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <MqttSettingsForm {...formProps} />}
|
render={(formProps) => <MqttSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
export default restController(MQTT_SETTINGS_ENDPOINT, MqttSettingsController);
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
TextValidator,
|
TextValidator,
|
||||||
ValidatorForm,
|
ValidatorForm,
|
||||||
SelectValidator,
|
SelectValidator
|
||||||
} from "react-material-ui-form-validator";
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Checkbox, TextField, Typography } from "@material-ui/core";
|
import { Checkbox, TextField, Typography } from '@material-ui/core';
|
||||||
import SaveIcon from "@material-ui/icons/Save";
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RestFormProps,
|
RestFormProps,
|
||||||
FormActions,
|
FormActions,
|
||||||
FormButton,
|
FormButton,
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
PasswordValidator,
|
PasswordValidator
|
||||||
} from "../components";
|
} from '../components';
|
||||||
import { isIP, isHostname, or, isPath } from "../validators";
|
import { isIP, isHostname, or, isPath } from '../validators';
|
||||||
|
|
||||||
import { MqttSettings } from "./types";
|
import { MqttSettings } from './types';
|
||||||
|
|
||||||
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
type MqttSettingsFormProps = RestFormProps<MqttSettings>;
|
||||||
|
|
||||||
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule("isIPOrHostname", or(isIP, isHostname));
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
ValidatorForm.addValidationRule("isPath", isPath);
|
ValidatorForm.addValidationRule('isPath', isPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -36,38 +36,38 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.enabled}
|
checked={data.enabled}
|
||||||
onChange={handleValueChange("enabled")}
|
onChange={handleValueChange('enabled')}
|
||||||
value="enabled"
|
value="enabled"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Enable MQTT"
|
label="Enable MQTT"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={["required", "isIPOrHostname"]}
|
validators={['required', 'isIPOrHostname']}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Host is required",
|
'Host is required',
|
||||||
"Not a valid IP address or hostname",
|
'Not a valid IP address or hostname'
|
||||||
]}
|
]}
|
||||||
name="host"
|
name="host"
|
||||||
label="Host"
|
label="Host"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.host}
|
value={data.host}
|
||||||
onChange={handleValueChange("host")}
|
onChange={handleValueChange('host')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Port is required",
|
'Port is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be greater than 0 ",
|
'Must be greater than 0 ',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="port"
|
name="port"
|
||||||
label="Port"
|
label="Port"
|
||||||
@@ -75,18 +75,18 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.port}
|
value={data.port}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("port")}
|
onChange={handleValueChange('port')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={["required", "isPath"]}
|
validators={['required', 'isPath']}
|
||||||
errorMessages={["Base is required", "Not a valid Path"]}
|
errorMessages={['Base is required', 'Not a valid Path']}
|
||||||
name="base"
|
name="base"
|
||||||
label="Base"
|
label="Base"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.base}
|
value={data.base}
|
||||||
onChange={handleValueChange("base")}
|
onChange={handleValueChange('base')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -95,7 +95,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.username}
|
value={data.username}
|
||||||
onChange={handleValueChange("username")}
|
onChange={handleValueChange('username')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
@@ -104,7 +104,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.password}
|
value={data.password}
|
||||||
onChange={handleValueChange("password")}
|
onChange={handleValueChange('password')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -113,21 +113,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.client_id}
|
value={data.client_id}
|
||||||
onChange={handleValueChange("client_id")}
|
onChange={handleValueChange('client_id')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:1",
|
'minNumber:1',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Keep alive is required",
|
'Keep alive is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be greater than 0",
|
'Must be greater than 0',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="keep_alive"
|
name="keep_alive"
|
||||||
label="Keep Alive (seconds)"
|
label="Keep Alive (seconds)"
|
||||||
@@ -135,7 +135,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.keep_alive}
|
value={data.keep_alive}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("keep_alive")}
|
onChange={handleValueChange('keep_alive')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<SelectValidator
|
<SelectValidator
|
||||||
@@ -144,7 +144,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value={data.mqtt_qos}
|
value={data.mqtt_qos}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("mqtt_qos")}
|
onChange={handleValueChange('mqtt_qos')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={0}>0 (default)</MenuItem>
|
<MenuItem value={0}>0 (default)</MenuItem>
|
||||||
@@ -155,7 +155,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.clean_session}
|
checked={data.clean_session}
|
||||||
onChange={handleValueChange("clean_session")}
|
onChange={handleValueChange('clean_session')}
|
||||||
value="clean_session"
|
value="clean_session"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.mqtt_retain}
|
checked={data.mqtt_retain}
|
||||||
onChange={handleValueChange("mqtt_retain")}
|
onChange={handleValueChange('mqtt_retain')}
|
||||||
value="mqtt_retain"
|
value="mqtt_retain"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -181,7 +181,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value={data.nested_format}
|
value={data.nested_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("nested_format")}
|
onChange={handleValueChange('nested_format')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={1}>nested on a single topic</MenuItem>
|
<MenuItem value={1}>nested on a single topic</MenuItem>
|
||||||
@@ -193,7 +193,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value={data.dallas_format}
|
value={data.dallas_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("dallas_format")}
|
onChange={handleValueChange('dallas_format')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={1}>by Sensor ID</MenuItem>
|
<MenuItem value={1}>by Sensor ID</MenuItem>
|
||||||
@@ -205,7 +205,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value={data.bool_format}
|
value={data.bool_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("bool_format")}
|
onChange={handleValueChange('bool_format')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={1}>"on"/"off"</MenuItem>
|
<MenuItem value={1}>"on"/"off"</MenuItem>
|
||||||
@@ -219,7 +219,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value={data.subscribe_format}
|
value={data.subscribe_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("subscribe_format")}
|
onChange={handleValueChange('subscribe_format')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={0}>general device topic</MenuItem>
|
<MenuItem value={0}>general device topic</MenuItem>
|
||||||
@@ -230,7 +230,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.ha_enabled}
|
checked={data.ha_enabled}
|
||||||
onChange={handleValueChange("ha_enabled")}
|
onChange={handleValueChange('ha_enabled')}
|
||||||
value="ha_enabled"
|
value="ha_enabled"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
value={data.ha_climate_format}
|
value={data.ha_climate_format}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("ha_climate_format")}
|
onChange={handleValueChange('ha_climate_format')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={1}>use Current temperature (default)</MenuItem>
|
<MenuItem value={1}>use Current temperature (default)</MenuItem>
|
||||||
@@ -257,16 +257,16 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Publish time is required",
|
'Publish time is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or greater",
|
'Must be 0 or greater',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="publish_time_boiler"
|
name="publish_time_boiler"
|
||||||
label="Boiler Publish Interval (seconds, 0=on change)"
|
label="Boiler Publish Interval (seconds, 0=on change)"
|
||||||
@@ -274,21 +274,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.publish_time_boiler}
|
value={data.publish_time_boiler}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("publish_time_boiler")}
|
onChange={handleValueChange('publish_time_boiler')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Publish time is required",
|
'Publish time is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or greater",
|
'Must be 0 or greater',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="publish_time_thermostat"
|
name="publish_time_thermostat"
|
||||||
label="Thermostat Publish Interval (seconds, 0=on change)"
|
label="Thermostat Publish Interval (seconds, 0=on change)"
|
||||||
@@ -296,21 +296,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.publish_time_thermostat}
|
value={data.publish_time_thermostat}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("publish_time_thermostat")}
|
onChange={handleValueChange('publish_time_thermostat')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Publish time is required",
|
'Publish time is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or greater",
|
'Must be 0 or greater',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="publish_time_solar"
|
name="publish_time_solar"
|
||||||
label="Solar Publish Interval (seconds, 0=on change)"
|
label="Solar Publish Interval (seconds, 0=on change)"
|
||||||
@@ -318,21 +318,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.publish_time_solar}
|
value={data.publish_time_solar}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("publish_time_solar")}
|
onChange={handleValueChange('publish_time_solar')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Publish time is required",
|
'Publish time is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or greater",
|
'Must be 0 or greater',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="publish_time_mixer"
|
name="publish_time_mixer"
|
||||||
label="Mixer Publish Interval (seconds, 0=on change)"
|
label="Mixer Publish Interval (seconds, 0=on change)"
|
||||||
@@ -340,21 +340,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.publish_time_mixer}
|
value={data.publish_time_mixer}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("publish_time_mixer")}
|
onChange={handleValueChange('publish_time_mixer')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Publish time is required",
|
'Publish time is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or greater",
|
'Must be 0 or greater',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="publish_time_sensor"
|
name="publish_time_sensor"
|
||||||
label="Sensors Publish Interval (seconds, 0=on change)"
|
label="Sensors Publish Interval (seconds, 0=on change)"
|
||||||
@@ -362,21 +362,21 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.publish_time_sensor}
|
value={data.publish_time_sensor}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("publish_time_sensor")}
|
onChange={handleValueChange('publish_time_sensor')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Publish time is required",
|
'Publish time is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or greater",
|
'Must be 0 or greater',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="publish_time_other"
|
name="publish_time_other"
|
||||||
label="All other Modules Publish Interval (seconds, 0=on change)"
|
label="All other Modules Publish Interval (seconds, 0=on change)"
|
||||||
@@ -384,7 +384,7 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.publish_time_other}
|
value={data.publish_time_other}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("publish_time_other")}
|
onChange={handleValueChange('publish_time_other')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
|
|||||||
@@ -1,59 +1,59 @@
|
|||||||
import { Theme } from '@material-ui/core'
|
import { Theme } from '@material-ui/core';
|
||||||
import { MqttStatus, MqttDisconnectReason } from './types'
|
import { MqttStatus, MqttDisconnectReason } from './types';
|
||||||
|
|
||||||
export const mqttStatusHighlight = (
|
export const mqttStatusHighlight = (
|
||||||
{ enabled, connected }: MqttStatus,
|
{ enabled, connected }: MqttStatus,
|
||||||
theme: Theme,
|
theme: Theme
|
||||||
) => {
|
) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return theme.palette.info.main
|
return theme.palette.info.main;
|
||||||
}
|
}
|
||||||
if (connected) {
|
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) => {
|
export const mqttStatus = ({ enabled, connected }: MqttStatus) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return 'Not enabled'
|
return 'Not enabled';
|
||||||
}
|
}
|
||||||
if (connected) {
|
if (connected) {
|
||||||
return 'Connected'
|
return 'Connected';
|
||||||
}
|
}
|
||||||
return 'Disconnected'
|
return 'Disconnected';
|
||||||
}
|
};
|
||||||
|
|
||||||
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
export const disconnectReason = ({ disconnect_reason }: MqttStatus) => {
|
||||||
switch (disconnect_reason) {
|
switch (disconnect_reason) {
|
||||||
case MqttDisconnectReason.TCP_DISCONNECTED:
|
case MqttDisconnectReason.TCP_DISCONNECTED:
|
||||||
return 'TCP disconnected'
|
return 'TCP disconnected';
|
||||||
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
case MqttDisconnectReason.MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
|
||||||
return 'Unacceptable protocol version'
|
return 'Unacceptable protocol version';
|
||||||
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
case MqttDisconnectReason.MQTT_IDENTIFIER_REJECTED:
|
||||||
return 'Client ID rejected'
|
return 'Client ID rejected';
|
||||||
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
case MqttDisconnectReason.MQTT_SERVER_UNAVAILABLE:
|
||||||
return 'Server unavailable'
|
return 'Server unavailable';
|
||||||
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
case MqttDisconnectReason.MQTT_MALFORMED_CREDENTIALS:
|
||||||
return 'Malformed credentials'
|
return 'Malformed credentials';
|
||||||
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
case MqttDisconnectReason.MQTT_NOT_AUTHORIZED:
|
||||||
return 'Not authorized'
|
return 'Not authorized';
|
||||||
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
case MqttDisconnectReason.ESP8266_NOT_ENOUGH_SPACE:
|
||||||
return 'Device out of memory'
|
return 'Device out of memory';
|
||||||
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
case MqttDisconnectReason.TLS_BAD_FINGERPRINT:
|
||||||
return 'Server fingerprint invalid'
|
return 'Server fingerprint invalid';
|
||||||
default:
|
default:
|
||||||
return 'Unknown'
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const mqttPublishHighlight = (
|
export const mqttPublishHighlight = (
|
||||||
{ mqtt_fails }: MqttStatus,
|
{ 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;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
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 { MQTT_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import MqttStatusForm from './MqttStatusForm';
|
import MqttStatusForm from './MqttStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { MqttStatus } from './types';
|
|||||||
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
type MqttStatusControllerProps = RestControllerProps<MqttStatus>;
|
||||||
|
|
||||||
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
class MqttStatusController extends Component<MqttStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,10 +23,10 @@ class MqttStatusController extends Component<MqttStatusControllerProps> {
|
|||||||
<SectionContent title="MQTT Status">
|
<SectionContent title="MQTT Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <MqttStatusForm {...formProps} />}
|
render={(formProps) => <MqttStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,39 @@
|
|||||||
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';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemText
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
import ReportIcon from '@material-ui/icons/Report';
|
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 {
|
||||||
import { mqttStatusHighlight, mqttStatus, mqttPublishHighlight, disconnectReason } from './MqttStatus';
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
HighlightAvatar
|
||||||
|
} from '../components';
|
||||||
|
import {
|
||||||
|
mqttStatusHighlight,
|
||||||
|
mqttStatus,
|
||||||
|
mqttPublishHighlight,
|
||||||
|
disconnectReason
|
||||||
|
} from './MqttStatus';
|
||||||
import { MqttStatus } from './types';
|
import { MqttStatus } from './types';
|
||||||
|
|
||||||
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
type MqttStatusFormProps = RestFormProps<MqttStatus> & WithTheme;
|
||||||
|
|
||||||
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
class MqttStatusForm extends Component<MqttStatusFormProps> {
|
||||||
|
|
||||||
renderConnectionStatus() {
|
renderConnectionStatus() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
if (data.connected) {
|
if (data.connected) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -50,7 +66,10 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
<ReportIcon />
|
<ReportIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Disconnect Reason" secondary={disconnectReason(data)} />
|
<ListItemText
|
||||||
|
primary="Disconnect Reason"
|
||||||
|
secondary={disconnectReason(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -58,7 +77,7 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -78,18 +97,20 @@ class MqttStatusForm extends Component<MqttStatusFormProps> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<List>
|
<List>{this.createListItems()}</List>
|
||||||
{this.createListItems()}
|
|
||||||
</List>
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
<FormButton
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(MqttStatusForm);
|
export default withTheme(MqttStatusForm);
|
||||||
|
|||||||
@@ -6,40 +6,40 @@ export enum MqttDisconnectReason {
|
|||||||
MQTT_MALFORMED_CREDENTIALS = 4,
|
MQTT_MALFORMED_CREDENTIALS = 4,
|
||||||
MQTT_NOT_AUTHORIZED = 5,
|
MQTT_NOT_AUTHORIZED = 5,
|
||||||
ESP8266_NOT_ENOUGH_SPACE = 6,
|
ESP8266_NOT_ENOUGH_SPACE = 6,
|
||||||
TLS_BAD_FINGERPRINT = 7,
|
TLS_BAD_FINGERPRINT = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MqttStatus {
|
export interface MqttStatus {
|
||||||
enabled: boolean
|
enabled: boolean;
|
||||||
connected: boolean
|
connected: boolean;
|
||||||
client_id: string
|
client_id: string;
|
||||||
disconnect_reason: MqttDisconnectReason
|
disconnect_reason: MqttDisconnectReason;
|
||||||
mqtt_fails: number
|
mqtt_fails: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MqttSettings {
|
export interface MqttSettings {
|
||||||
enabled: boolean
|
enabled: boolean;
|
||||||
host: string
|
host: string;
|
||||||
port: number
|
port: number;
|
||||||
base: string
|
base: string;
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
client_id: string
|
client_id: string;
|
||||||
keep_alive: number
|
keep_alive: number;
|
||||||
clean_session: boolean
|
clean_session: boolean;
|
||||||
max_topic_length: number
|
max_topic_length: number;
|
||||||
publish_time_boiler: number
|
publish_time_boiler: number;
|
||||||
publish_time_thermostat: number
|
publish_time_thermostat: number;
|
||||||
publish_time_solar: number
|
publish_time_solar: number;
|
||||||
publish_time_mixer: number
|
publish_time_mixer: number;
|
||||||
publish_time_other: number
|
publish_time_other: number;
|
||||||
publish_time_sensor: number
|
publish_time_sensor: number;
|
||||||
dallas_format: number
|
dallas_format: number;
|
||||||
bool_format: number
|
bool_format: number;
|
||||||
mqtt_qos: number
|
mqtt_qos: number;
|
||||||
mqtt_retain: boolean
|
mqtt_retain: boolean;
|
||||||
ha_enabled: boolean
|
ha_enabled: boolean;
|
||||||
ha_climate_format: number
|
ha_climate_format: number;
|
||||||
nested_format: number
|
nested_format: number;
|
||||||
subscribe_format: number
|
subscribe_format: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import NetworkStatusController from './NetworkStatusController';
|
import NetworkStatusController from './NetworkStatusController';
|
||||||
import NetworkSettingsController from './NetworkSettingsController';
|
import NetworkSettingsController from './NetworkSettingsController';
|
||||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
import {
|
||||||
|
NetworkConnectionContext,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
} from './NetworkConnectionContext';
|
||||||
|
|
||||||
import { WiFiNetwork } from './types';
|
import { WiFiNetwork } from './types';
|
||||||
|
|
||||||
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
type NetworkConnectionProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnectionContextValue> {
|
class NetworkConnection extends Component<
|
||||||
|
NetworkConnectionProps,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
> {
|
||||||
constructor(props: NetworkConnectionProps) {
|
constructor(props: NetworkConnectionProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -28,13 +37,13 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
|||||||
selectNetwork = (network: WiFiNetwork) => {
|
selectNetwork = (network: WiFiNetwork) => {
|
||||||
this.setState({ selectedNetwork: network });
|
this.setState({ selectedNetwork: network });
|
||||||
this.props.history.push('/network/settings');
|
this.props.history.push('/network/settings');
|
||||||
}
|
};
|
||||||
|
|
||||||
deselectNetwork = () => {
|
deselectNetwork = () => {
|
||||||
this.setState({ selectedNetwork: undefined });
|
this.setState({ selectedNetwork: undefined });
|
||||||
}
|
};
|
||||||
|
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
handleTabChange = (path: string) => {
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,20 +52,44 @@ class NetworkConnection extends Component<NetworkConnectionProps, NetworkConnect
|
|||||||
return (
|
return (
|
||||||
<NetworkConnectionContext.Provider value={this.state}>
|
<NetworkConnectionContext.Provider value={this.state}>
|
||||||
<MenuAppBar sectionTitle="Network Connection">
|
<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/status" label="Network Status" />
|
||||||
<Tab value="/network/scan" label="Scan WiFi Networks" disabled={!authenticatedContext.me.admin} />
|
<Tab
|
||||||
<Tab value="/network/settings" label="Network Settings" disabled={!authenticatedContext.me.admin} />
|
value="/network/scan"
|
||||||
|
label="Scan WiFi Networks"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="/network/settings"
|
||||||
|
label="Network Settings"
|
||||||
|
disabled={!authenticatedContext.me.admin}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/network/status" component={NetworkStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/network/scan" component={WiFiNetworkScanner} />
|
exact
|
||||||
<AuthenticatedRoute exact path="/network/settings" component={NetworkSettingsController} />
|
path="/network/status"
|
||||||
|
component={NetworkStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/scan"
|
||||||
|
component={WiFiNetworkScanner}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/network/settings"
|
||||||
|
component={NetworkSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/network/status" />
|
<Redirect to="/network/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
</NetworkConnectionContext.Provider>
|
</NetworkConnectionContext.Provider>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface NetworkConnectionContextValue {
|
|||||||
deselectNetwork: () => void;
|
deselectNetwork: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue
|
const NetworkConnectionContextDefaultValue = {} as NetworkConnectionContextValue;
|
||||||
export const NetworkConnectionContext = React.createContext(
|
export const NetworkConnectionContext = React.createContext(
|
||||||
NetworkConnectionContextDefaultValue
|
NetworkConnectionContextDefaultValue
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 NetworkSettingsForm from './NetworkSettingsForm';
|
||||||
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
import { NETWORK_SETTINGS_ENDPOINT } from '../api';
|
||||||
import { NetworkSettings } from './types';
|
import { NetworkSettings } from './types';
|
||||||
@@ -8,7 +13,6 @@ import { NetworkSettings } from './types';
|
|||||||
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
type NetworkSettingsControllerProps = RestControllerProps<NetworkSettings>;
|
||||||
|
|
||||||
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
class NetworkSettingsController extends Component<NetworkSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -18,12 +22,14 @@ class NetworkSettingsController extends Component<NetworkSettingsControllerProps
|
|||||||
<SectionContent title="Network Settings">
|
<SectionContent title="Network Settings">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NetworkSettingsForm {...formProps} />}
|
render={(formProps) => <NetworkSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NETWORK_SETTINGS_ENDPOINT, NetworkSettingsController);
|
export default restController(
|
||||||
|
NETWORK_SETTINGS_ENDPOINT,
|
||||||
|
NetworkSettingsController
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
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 Avatar from '@material-ui/core/Avatar';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
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 DeleteIcon from '@material-ui/icons/Delete';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
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 { isIP, isHostname, optional } from '../validators';
|
||||||
|
|
||||||
import { NetworkConnectionContext, NetworkConnectionContextValue } from './NetworkConnectionContext';
|
import {
|
||||||
|
NetworkConnectionContext,
|
||||||
|
NetworkConnectionContextValue
|
||||||
|
} from './NetworkConnectionContext';
|
||||||
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
import { isNetworkOpen, networkSecurityMode } from './WiFiSecurityModes';
|
||||||
import { NetworkSettings } from './types';
|
import { NetworkSettings } from './types';
|
||||||
|
|
||||||
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
type NetworkStatusFormProps = RestFormProps<NetworkSettings>;
|
||||||
|
|
||||||
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
||||||
|
|
||||||
static contextType = NetworkConnectionContext;
|
static contextType = NetworkConnectionContext;
|
||||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||||
|
|
||||||
constructor(props: NetworkStatusFormProps, context: NetworkConnectionContextValue) {
|
constructor(
|
||||||
|
props: NetworkStatusFormProps,
|
||||||
|
context: NetworkConnectionContextValue
|
||||||
|
) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { selectedNetwork } = context;
|
const { selectedNetwork } = context;
|
||||||
if (selectedNetwork) {
|
if (selectedNetwork) {
|
||||||
const networkSettings: NetworkSettings = {
|
const networkSettings: NetworkSettings = {
|
||||||
ssid: selectedNetwork.ssid,
|
ssid: selectedNetwork.ssid,
|
||||||
password: "",
|
password: '',
|
||||||
hostname: props.data.hostname,
|
hostname: props.data.hostname,
|
||||||
static_ip_config: false,
|
static_ip_config: false
|
||||||
}
|
};
|
||||||
props.setData(networkSettings);
|
props.setData(networkSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +66,7 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
deselectNetworkAndLoadData = () => {
|
deselectNetworkAndLoadData = () => {
|
||||||
this.context.deselectNetwork();
|
this.context.deselectNetwork();
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.context.deselectNetwork();
|
this.context.deselectNetwork();
|
||||||
@@ -59,27 +77,38 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
<ValidatorForm onSubmit={saveData} ref="NetworkSettingsForm">
|
||||||
{
|
{selectedNetwork ? (
|
||||||
selectedNetwork ?
|
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
{isNetworkOpen(selectedNetwork) ? <LockOpenIcon /> : <LockIcon />}
|
{isNetworkOpen(selectedNetwork) ? (
|
||||||
|
<LockOpenIcon />
|
||||||
|
) : (
|
||||||
|
<LockIcon />
|
||||||
|
)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={selectedNetwork.ssid}
|
primary={selectedNetwork.ssid}
|
||||||
secondary={"Security: " + networkSecurityMode(selectedNetwork) + ", Ch: " + selectedNetwork.channel}
|
secondary={
|
||||||
|
'Security: ' +
|
||||||
|
networkSecurityMode(selectedNetwork) +
|
||||||
|
', Ch: ' +
|
||||||
|
selectedNetwork.channel
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton aria-label="Manual Config" onClick={deselectNetwork}>
|
<IconButton
|
||||||
|
aria-label="Manual Config"
|
||||||
|
onClick={deselectNetwork}
|
||||||
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
:
|
) : (
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['matchRegexp:^.{0,32}$']}
|
validators={['matchRegexp:^.{0,32}$']}
|
||||||
errorMessages={['SSID must be 32 characters or less']}
|
errorMessages={['SSID must be 32 characters or less']}
|
||||||
@@ -91,9 +120,8 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
onChange={handleValueChange('ssid')}
|
onChange={handleValueChange('ssid')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{
|
{(!selectedNetwork || !isNetworkOpen(selectedNetwork)) && (
|
||||||
(!selectedNetwork || !isNetworkOpen(selectedNetwork)) &&
|
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['matchRegexp:^.{0,64}$']}
|
validators={['matchRegexp:^.{0,64}$']}
|
||||||
errorMessages={['Password must be 64 characters or less']}
|
errorMessages={['Password must be 64 characters or less']}
|
||||||
@@ -105,10 +133,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
onChange={handleValueChange('password')}
|
onChange={handleValueChange('password')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isHostname']}
|
validators={['required', 'isHostname']}
|
||||||
errorMessages={['Hostname is required', "Not a valid hostname"]}
|
errorMessages={['Hostname is required', 'Not a valid hostname']}
|
||||||
name="hostname"
|
name="hostname"
|
||||||
label="Hostname"
|
label="Hostname"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -122,13 +150,12 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
value="static_ip_config"
|
value="static_ip_config"
|
||||||
checked={data.static_ip_config}
|
checked={data.static_ip_config}
|
||||||
onChange={handleValueChange("static_ip_config")}
|
onChange={handleValueChange('static_ip_config')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Static IP Config"
|
label="Static IP Config"
|
||||||
/>
|
/>
|
||||||
{
|
{data.static_ip_config && (
|
||||||
data.static_ip_config &&
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
validators={['required', 'isIP']}
|
||||||
@@ -154,7 +181,10 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIP']}
|
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"
|
name="subnet_mask"
|
||||||
label="Subnet"
|
label="Subnet"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -186,9 +216,14 @@ class NetworkSettingsForm extends React.Component<NetworkStatusFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
)}
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
import { Theme } from '@material-ui/core'
|
import { Theme } from '@material-ui/core';
|
||||||
import { NetworkStatus, NetworkConnectionStatus } from './types'
|
import { NetworkStatus, NetworkConnectionStatus } from './types';
|
||||||
|
|
||||||
export const isConnected = ({ status }: NetworkStatus) => {
|
export const isConnected = ({ status }: NetworkStatus) => {
|
||||||
return (
|
return (
|
||||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED ||
|
||||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
|
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isWiFi = ({ status }: NetworkStatus) =>
|
export const isWiFi = ({ status }: NetworkStatus) =>
|
||||||
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
|
status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED;
|
||||||
export const isEthernet = ({ status }: NetworkStatus) =>
|
export const isEthernet = ({ status }: NetworkStatus) =>
|
||||||
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED
|
status === NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED;
|
||||||
|
|
||||||
export const networkStatusHighlight = (
|
export const networkStatusHighlight = (
|
||||||
{ status }: NetworkStatus,
|
{ status }: NetworkStatus,
|
||||||
theme: Theme,
|
theme: Theme
|
||||||
) => {
|
) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||||
return theme.palette.info.main
|
return theme.palette.info.main;
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||||
case NetworkConnectionStatus.ETHERNET_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_CONNECT_FAILED:
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||||
return theme.palette.error.main
|
return theme.palette.error.main;
|
||||||
default:
|
default:
|
||||||
return theme.palette.warning.main
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const networkStatus = ({ status }: NetworkStatus) => {
|
export const networkStatus = ({ status }: NetworkStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
|
||||||
return 'Inactive'
|
return 'Inactive';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
|
||||||
return 'Idle'
|
return 'Idle';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
case NetworkConnectionStatus.WIFI_STATUS_NO_SSID_AVAIL:
|
||||||
return 'No SSID Available'
|
return 'No SSID Available';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTED:
|
||||||
return 'Connected (WiFi)'
|
return 'Connected (WiFi)';
|
||||||
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
case NetworkConnectionStatus.ETHERNET_STATUS_CONNECTED:
|
||||||
return 'Connected (Ethernet)'
|
return 'Connected (Ethernet)';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECT_FAILED:
|
||||||
return 'Connection Failed'
|
return 'Connection Failed';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
case NetworkConnectionStatus.WIFI_STATUS_CONNECTION_LOST:
|
||||||
return 'Connection Lost'
|
return 'Connection Lost';
|
||||||
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
|
||||||
return 'Disconnected'
|
return 'Disconnected';
|
||||||
default:
|
default:
|
||||||
return 'Unknown'
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 NetworkStatusForm from './NetworkStatusForm';
|
||||||
import { NETWORK_STATUS_ENDPOINT } from '../api';
|
import { NETWORK_STATUS_ENDPOINT } from '../api';
|
||||||
import { NetworkStatus } from './types';
|
import { NetworkStatus } from './types';
|
||||||
@@ -8,7 +13,6 @@ import { NetworkStatus } from './types';
|
|||||||
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
type NetworkStatusControllerProps = RestControllerProps<NetworkStatus>;
|
||||||
|
|
||||||
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -18,12 +22,11 @@ class NetworkStatusController extends Component<NetworkStatusControllerProps> {
|
|||||||
<SectionContent title="Network Status">
|
<SectionContent title="Network Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NetworkStatusForm {...formProps} />}
|
render={(formProps) => <NetworkStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
export default restController(NETWORK_STATUS_ENDPOINT, NetworkStatusController);
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Divider,
|
Divider,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemText,
|
ListItemText
|
||||||
} from "@material-ui/core";
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import DNSIcon from "@material-ui/icons/Dns";
|
import DNSIcon from '@material-ui/icons/Dns';
|
||||||
import WifiIcon from "@material-ui/icons/Wifi";
|
import WifiIcon from '@material-ui/icons/Wifi';
|
||||||
import RouterIcon from "@material-ui/icons/Router";
|
import RouterIcon from '@material-ui/icons/Router';
|
||||||
import SettingsInputComponentIcon from "@material-ui/icons/SettingsInputComponent";
|
import SettingsInputComponentIcon from '@material-ui/icons/SettingsInputComponent';
|
||||||
import SettingsInputAntennaIcon from "@material-ui/icons/SettingsInputAntenna";
|
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
|
||||||
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RestFormProps,
|
RestFormProps,
|
||||||
FormActions,
|
FormActions,
|
||||||
FormButton,
|
FormButton,
|
||||||
HighlightAvatar,
|
HighlightAvatar
|
||||||
} from "../components";
|
} from '../components';
|
||||||
import {
|
import {
|
||||||
networkStatus,
|
networkStatus,
|
||||||
networkStatusHighlight,
|
networkStatusHighlight,
|
||||||
isConnected,
|
isConnected,
|
||||||
isWiFi,
|
isWiFi,
|
||||||
isEthernet,
|
isEthernet
|
||||||
} from "./NetworkStatus";
|
} from './NetworkStatus';
|
||||||
import { NetworkStatus } from "./types";
|
import { NetworkStatus } from './types';
|
||||||
|
|
||||||
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
type NetworkStatusFormProps = RestFormProps<NetworkStatus> & WithTheme;
|
||||||
|
|
||||||
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
||||||
dnsServers(status: NetworkStatus) {
|
dnsServers(status: NetworkStatus) {
|
||||||
if (!status.dns_ip_1) {
|
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() {
|
createListItems() {
|
||||||
@@ -110,7 +110,7 @@ class NetworkStatusForm extends Component<NetworkStatusFormProps> {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Gateway IP"
|
primary="Gateway IP"
|
||||||
secondary={data.gateway_ip || "none"}
|
secondary={data.gateway_ip || 'none'}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { withSnackbar, WithSnackbarProps } from 'notistack';
|
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 PermScanWifiIcon from '@material-ui/icons/PermScanWifi';
|
||||||
|
|
||||||
import { FormActions, FormButton, SectionContent } from '../components';
|
import { FormActions, FormButton, SectionContent } from '../components';
|
||||||
@@ -11,9 +18,9 @@ import { SCAN_NETWORKS_ENDPOINT, LIST_NETWORKS_ENDPOINT } from '../api';
|
|||||||
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
import WiFiNetworkSelector from './WiFiNetworkSelector';
|
||||||
import { WiFiNetworkList, WiFiNetwork } from './types';
|
import { WiFiNetworkList, WiFiNetwork } from './types';
|
||||||
|
|
||||||
const NUM_POLLS = 10
|
const NUM_POLLS = 10;
|
||||||
const POLLING_FREQUENCY = 500
|
const POLLING_FREQUENCY = 500;
|
||||||
const RETRY_EXCEPTION_TYPE = "retry"
|
const RETRY_EXCEPTION_TYPE = 'retry';
|
||||||
|
|
||||||
interface WiFiNetworkScannerState {
|
interface WiFiNetworkScannerState {
|
||||||
scanningForNetworks: boolean;
|
scanningForNetworks: boolean;
|
||||||
@@ -21,28 +28,31 @@ interface WiFiNetworkScannerState {
|
|||||||
networkList?: WiFiNetworkList;
|
networkList?: WiFiNetworkList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = (theme: Theme) => createStyles({
|
const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
scanningSettings: {
|
scanningSettings: {
|
||||||
margin: theme.spacing(0.5),
|
margin: theme.spacing(0.5)
|
||||||
},
|
},
|
||||||
scanningSettingsDetails: {
|
scanningSettingsDetails: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
textAlign: "center"
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
scanningProgress: {
|
scanningProgress: {
|
||||||
margin: theme.spacing(4),
|
margin: theme.spacing(4),
|
||||||
textAlign: "center"
|
textAlign: 'center'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
type WiFiNetworkScannerProps = WithSnackbarProps & WithStyles<typeof styles>;
|
||||||
|
|
||||||
class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkScannerState> {
|
class WiFiNetworkScanner extends Component<
|
||||||
|
WiFiNetworkScannerProps,
|
||||||
pollCount: number = 0;
|
WiFiNetworkScannerState
|
||||||
|
> {
|
||||||
|
pollCount = 0;
|
||||||
|
|
||||||
state: WiFiNetworkScannerState = {
|
state: WiFiNetworkScannerState = {
|
||||||
scanningForNetworks: false,
|
scanningForNetworks: false
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -54,22 +64,35 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
if (!scanningForNetworks) {
|
if (!scanningForNetworks) {
|
||||||
this.scanNetworks();
|
this.scanNetworks();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
scanNetworks() {
|
scanNetworks() {
|
||||||
this.pollCount = 0;
|
this.pollCount = 0;
|
||||||
this.setState({ scanningForNetworks: true, networkList: undefined, errorMessage: undefined });
|
this.setState({
|
||||||
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT).then(response => {
|
scanningForNetworks: true,
|
||||||
|
networkList: undefined,
|
||||||
|
errorMessage: undefined
|
||||||
|
});
|
||||||
|
redirectingAuthorizedFetch(SCAN_NETWORKS_ENDPOINT)
|
||||||
|
.then((response) => {
|
||||||
if (response.status === 202) {
|
if (response.status === 202) {
|
||||||
this.schedulePollTimeout();
|
this.schedulePollTimeout();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw Error("Scanning for networks returned unexpected response code: " + response.status);
|
throw Error(
|
||||||
}).catch(error => {
|
'Scanning for networks returned unexpected response code: ' +
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
response.status
|
||||||
variant: 'error',
|
);
|
||||||
|
})
|
||||||
|
.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() {
|
retryError() {
|
||||||
return {
|
return {
|
||||||
name: RETRY_EXCEPTION_TYPE,
|
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) {
|
compareNetworks(network1: WiFiNetwork, network2: WiFiNetwork) {
|
||||||
if (network1.rssi < network2.rssi)
|
if (network1.rssi < network2.rssi) return 1;
|
||||||
return 1;
|
if (network1.rssi > network2.rssi) return -1;
|
||||||
if (network1.rssi > network2.rssi)
|
|
||||||
return -1;
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pollNetworkList = () => {
|
pollNetworkList = () => {
|
||||||
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
redirectingAuthorizedFetch(LIST_NETWORKS_ENDPOINT)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
@@ -103,24 +125,34 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
this.schedulePollTimeout();
|
this.schedulePollTimeout();
|
||||||
throw this.retryError();
|
throw this.retryError();
|
||||||
} else {
|
} 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 => {
|
.then((json) => {
|
||||||
json.networks.sort(this.compareNetworks)
|
json.networks.sort(this.compareNetworks);
|
||||||
this.setState({ scanningForNetworks: false, networkList: json, errorMessage: undefined })
|
this.setState({
|
||||||
|
scanningForNetworks: false,
|
||||||
|
networkList: json,
|
||||||
|
errorMessage: undefined
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
if (error.name !== RETRY_EXCEPTION_TYPE) {
|
||||||
this.props.enqueueSnackbar("Problem scanning: " + error.message, {
|
this.props.enqueueSnackbar('Problem scanning: ' + error.message, {
|
||||||
variant: 'error',
|
variant: 'error'
|
||||||
});
|
});
|
||||||
this.setState({ scanningForNetworks: false, networkList: undefined, errorMessage: error.message });
|
this.setState({
|
||||||
}
|
scanningForNetworks: false,
|
||||||
|
networkList: undefined,
|
||||||
|
errorMessage: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderNetworkScanner() {
|
renderNetworkScanner() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
@@ -144,9 +176,7 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return <WiFiNetworkSelector networkList={networkList} />;
|
||||||
<WiFiNetworkSelector networkList={networkList} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -155,14 +185,19 @@ class WiFiNetworkScanner extends Component<WiFiNetworkScannerProps, WiFiNetworkS
|
|||||||
<SectionContent title="Network Scanner">
|
<SectionContent title="Network Scanner">
|
||||||
{this.renderNetworkScanner()}
|
{this.renderNetworkScanner()}
|
||||||
<FormActions>
|
<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…
|
Scan again…
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
export default withSnackbar(withStyles(styles)(WiFiNetworkScanner));
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { Avatar, Badge } from '@material-ui/core';
|
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 WifiIcon from '@material-ui/icons/Wifi';
|
||||||
import LockIcon from '@material-ui/icons/Lock';
|
import LockIcon from '@material-ui/icons/Lock';
|
||||||
@@ -16,13 +22,16 @@ interface WiFiNetworkSelectorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
||||||
|
|
||||||
static contextType = NetworkConnectionContext;
|
static contextType = NetworkConnectionContext;
|
||||||
context!: React.ContextType<typeof NetworkConnectionContext>;
|
context!: React.ContextType<typeof NetworkConnectionContext>;
|
||||||
|
|
||||||
renderNetwork = (network: WiFiNetwork) => {
|
renderNetwork = (network: WiFiNetwork) => {
|
||||||
return (
|
return (
|
||||||
<ListItem key={network.bssid} button onClick={() => this.context.selectNetwork(network)}>
|
<ListItem
|
||||||
|
key={network.bssid}
|
||||||
|
button
|
||||||
|
onClick={() => this.context.selectNetwork(network)}
|
||||||
|
>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Avatar>
|
<Avatar>
|
||||||
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}
|
||||||
@@ -30,25 +39,27 @@ class WiFiNetworkSelector extends Component<WiFiNetworkSelectorProps> {
|
|||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={network.ssid}
|
primary={network.ssid}
|
||||||
secondary={"Security: " + networkSecurityMode(network) + ", Ch: " + network.channel}
|
secondary={
|
||||||
|
'Security: ' +
|
||||||
|
networkSecurityMode(network) +
|
||||||
|
', Ch: ' +
|
||||||
|
network.channel
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Badge badgeContent={network.rssi + "db"}>
|
<Badge badgeContent={network.rssi + 'db'}>
|
||||||
<WifiIcon />
|
<WifiIcon />
|
||||||
</Badge>
|
</Badge>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<List>
|
<List>{this.props.networkList.networks.map(this.renderNetwork)}</List>
|
||||||
{this.props.networkList.networks.map(this.renderNetwork)}
|
|
||||||
</List>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WiFiNetworkSelector;
|
export default WiFiNetworkSelector;
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { WiFiNetwork, WiFiEncryptionType } from './types'
|
import { WiFiNetwork, WiFiEncryptionType } from './types';
|
||||||
|
|
||||||
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
|
export const isNetworkOpen = ({ encryption_type }: WiFiNetwork) =>
|
||||||
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN
|
encryption_type === WiFiEncryptionType.WIFI_AUTH_OPEN;
|
||||||
|
|
||||||
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
export const networkSecurityMode = ({ encryption_type }: WiFiNetwork) => {
|
||||||
switch (encryption_type) {
|
switch (encryption_type) {
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
case WiFiEncryptionType.WIFI_AUTH_WEP:
|
||||||
return 'WEP'
|
return 'WEP';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA_PSK:
|
||||||
return 'WPA'
|
return 'WPA';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA2_PSK:
|
||||||
return 'WPA2'
|
return 'WPA2';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
case WiFiEncryptionType.WIFI_AUTH_WPA_WPA2_PSK:
|
||||||
return 'WPA/WPA2'
|
return 'WPA/WPA2';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
case WiFiEncryptionType.WIFI_AUTH_WPA2_ENTERPRISE:
|
||||||
return 'WPA2 Enterprise'
|
return 'WPA2 Enterprise';
|
||||||
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
case WiFiEncryptionType.WIFI_AUTH_OPEN:
|
||||||
return 'None'
|
return 'None';
|
||||||
default:
|
default:
|
||||||
return 'Unknown'
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export enum NetworkConnectionStatus {
|
|||||||
WIFI_STATUS_CONNECTION_LOST = 5,
|
WIFI_STATUS_CONNECTION_LOST = 5,
|
||||||
WIFI_STATUS_DISCONNECTED = 6,
|
WIFI_STATUS_DISCONNECTED = 6,
|
||||||
ETHERNET_STATUS_CONNECTED = 10,
|
ETHERNET_STATUS_CONNECTED = 10,
|
||||||
WIFI_STATUS_NO_SHIELD = 255,
|
WIFI_STATUS_NO_SHIELD = 255
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WiFiEncryptionType {
|
export enum WiFiEncryptionType {
|
||||||
@@ -15,43 +15,43 @@ export enum WiFiEncryptionType {
|
|||||||
WIFI_AUTH_WPA_PSK = 2,
|
WIFI_AUTH_WPA_PSK = 2,
|
||||||
WIFI_AUTH_WPA2_PSK = 3,
|
WIFI_AUTH_WPA2_PSK = 3,
|
||||||
WIFI_AUTH_WPA_WPA2_PSK = 4,
|
WIFI_AUTH_WPA_WPA2_PSK = 4,
|
||||||
WIFI_AUTH_WPA2_ENTERPRISE = 5,
|
WIFI_AUTH_WPA2_ENTERPRISE = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkStatus {
|
export interface NetworkStatus {
|
||||||
status: NetworkConnectionStatus
|
status: NetworkConnectionStatus;
|
||||||
local_ip: string
|
local_ip: string;
|
||||||
mac_address: string
|
mac_address: string;
|
||||||
rssi: number
|
rssi: number;
|
||||||
ssid: string
|
ssid: string;
|
||||||
bssid: string
|
bssid: string;
|
||||||
channel: number
|
channel: number;
|
||||||
subnet_mask: string
|
subnet_mask: string;
|
||||||
gateway_ip: string
|
gateway_ip: string;
|
||||||
dns_ip_1: string
|
dns_ip_1: string;
|
||||||
dns_ip_2: string
|
dns_ip_2: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
ssid: string
|
ssid: string;
|
||||||
password: string
|
password: string;
|
||||||
hostname: string
|
hostname: string;
|
||||||
static_ip_config: boolean
|
static_ip_config: boolean;
|
||||||
local_ip?: string
|
local_ip?: string;
|
||||||
gateway_ip?: string
|
gateway_ip?: string;
|
||||||
subnet_mask?: string
|
subnet_mask?: string;
|
||||||
dns_ip_1?: string
|
dns_ip_1?: string;
|
||||||
dns_ip_2?: string
|
dns_ip_2?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WiFiNetworkList {
|
export interface WiFiNetworkList {
|
||||||
networks: WiFiNetwork[]
|
networks: WiFiNetwork[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WiFiNetwork {
|
export interface WiFiNetwork {
|
||||||
rssi: number
|
rssi: number;
|
||||||
ssid: string
|
ssid: string;
|
||||||
bssid: string
|
bssid: string;
|
||||||
channel: number
|
channel: number;
|
||||||
encryption_type: WiFiEncryptionType
|
encryption_type: WiFiEncryptionType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { NTP_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import NTPSettingsForm from './NTPSettingsForm';
|
import NTPSettingsForm from './NTPSettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { NTPSettings } from './types';
|
|||||||
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
type NTPSettingsControllerProps = RestControllerProps<NTPSettings>;
|
||||||
|
|
||||||
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class NTPSettingsController extends Component<NTPSettingsControllerProps> {
|
|||||||
<SectionContent title="NTP Settings" titleGutter>
|
<SectionContent title="NTP Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NTPSettingsForm {...formProps} />}
|
render={(formProps) => <NTPSettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
export default restController(NTP_SETTINGS_ENDPOINT, NTPSettingsController);
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import React from 'react';
|
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 { Checkbox, MenuItem } from '@material-ui/core';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
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 { isIP, isHostname, or } from '../validators';
|
||||||
|
|
||||||
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
import { TIME_ZONES, timeZoneSelectItems, selectedTimeZone } from './TZ';
|
||||||
@@ -13,7 +22,6 @@ import { NTPSettings } from './types';
|
|||||||
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
type NTPSettingsFormProps = RestFormProps<NTPSettings>;
|
||||||
|
|
||||||
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
}
|
}
|
||||||
@@ -25,7 +33,7 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
tz_label: event.target.value,
|
tz_label: event.target.value,
|
||||||
tz_format: TIME_ZONES[event.target.value]
|
tz_format: TIME_ZONES[event.target.value]
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, handleValueChange, saveData } = this.props;
|
const { data, handleValueChange, saveData } = this.props;
|
||||||
@@ -43,7 +51,10 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isIPOrHostname']}
|
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"
|
name="server"
|
||||||
label="Server"
|
label="Server"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -68,7 +79,12 @@ class NTPSettingsForm extends React.Component<NTPSettingsFormProps> {
|
|||||||
{timeZoneSelectItems()}
|
{timeZoneSelectItems()}
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { Theme } from '@material-ui/core'
|
import { Theme } from '@material-ui/core';
|
||||||
import { NTPStatus, NTPSyncStatus } from './types'
|
import { NTPStatus, NTPSyncStatus } from './types';
|
||||||
|
|
||||||
export const isNtpActive = ({ status }: NTPStatus) =>
|
export const isNtpActive = ({ status }: NTPStatus) =>
|
||||||
status === NTPSyncStatus.NTP_ACTIVE
|
status === NTPSyncStatus.NTP_ACTIVE;
|
||||||
|
|
||||||
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
export const ntpStatusHighlight = ({ status }: NTPStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NTPSyncStatus.NTP_INACTIVE:
|
case NTPSyncStatus.NTP_INACTIVE:
|
||||||
return theme.palette.info.main
|
return theme.palette.info.main;
|
||||||
case NTPSyncStatus.NTP_ACTIVE:
|
case NTPSyncStatus.NTP_ACTIVE:
|
||||||
return theme.palette.success.main
|
return theme.palette.success.main;
|
||||||
default:
|
default:
|
||||||
return theme.palette.error.main
|
return theme.palette.error.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ntpStatus = ({ status }: NTPStatus) => {
|
export const ntpStatus = ({ status }: NTPStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case NTPSyncStatus.NTP_INACTIVE:
|
case NTPSyncStatus.NTP_INACTIVE:
|
||||||
return 'Inactive'
|
return 'Inactive';
|
||||||
case NTPSyncStatus.NTP_ACTIVE:
|
case NTPSyncStatus.NTP_ACTIVE:
|
||||||
return 'Active'
|
return 'Active';
|
||||||
default:
|
default:
|
||||||
return 'Unknown'
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 { NTP_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import NTPStatusForm from './NTPStatusForm';
|
import NTPStatusForm from './NTPStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { NTPStatus } from './types';
|
|||||||
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
type NTPStatusControllerProps = RestControllerProps<NTPStatus>;
|
||||||
|
|
||||||
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
class NTPStatusController extends Component<NTPStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class NTPStatusController extends Component<NTPStatusControllerProps> {
|
|||||||
<SectionContent title="NTP Status">
|
<SectionContent title="NTP Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <NTPStatusForm {...formProps} />}
|
render={(formProps) => <NTPStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
export default restController(NTP_STATUS_ENDPOINT, NTPStatusController);
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
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 { Avatar, Divider, List, ListItem, ListItemAvatar, ListItemText, Button } from '@material-ui/core';
|
import {
|
||||||
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, TextField } from '@material-ui/core';
|
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 SwapVerticalCircleIcon from '@material-ui/icons/SwapVerticalCircle';
|
||||||
import AccessTimeIcon from '@material-ui/icons/AccessTime';
|
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 { RestFormProps, FormButton, HighlightAvatar } from '../components';
|
||||||
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
import { isNtpActive, ntpStatusHighlight, ntpStatus } from './NTPStatus';
|
||||||
import { formatDuration, formatDateTime, formatLocalDateTime } from './TimeFormat';
|
import {
|
||||||
|
formatDuration,
|
||||||
|
formatDateTime,
|
||||||
|
formatLocalDateTime
|
||||||
|
} from './TimeFormat';
|
||||||
import { NTPStatus, Time } from './types';
|
import { NTPStatus, Time } from './types';
|
||||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import {
|
||||||
|
redirectingAuthorizedFetch,
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
import { TIME_ENDPOINT } from '../api';
|
import { TIME_ENDPOINT } from '../api';
|
||||||
|
|
||||||
type NTPStatusFormProps = RestFormProps<NTPStatus> & WithTheme & AuthenticatedContextProps;
|
type NTPStatusFormProps = RestFormProps<NTPStatus> &
|
||||||
|
WithTheme &
|
||||||
|
AuthenticatedContextProps;
|
||||||
|
|
||||||
interface NTPStatusFormState {
|
interface NTPStatusFormState {
|
||||||
settingTime: boolean;
|
settingTime: boolean;
|
||||||
@@ -27,7 +52,6 @@ interface NTPStatusFormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
||||||
|
|
||||||
constructor(props: NTPStatusFormProps) {
|
constructor(props: NTPStatusFormProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -41,20 +65,20 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
localTime: event.target.value
|
localTime: event.target.value
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
openSetTime = () => {
|
openSetTime = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
localTime: formatLocalDateTime(new Date()),
|
localTime: formatLocalDateTime(new Date()),
|
||||||
settingTime: true
|
settingTime: true
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
closeSetTime = () => {
|
closeSetTime = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
settingTime: false
|
settingTime: false
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
createTime = (): Time => ({
|
createTime = (): Time => ({
|
||||||
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
local_time: formatLocalDateTime(new Date(this.state.localTime))
|
||||||
@@ -62,27 +86,34 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
|
|
||||||
configureTime = () => {
|
configureTime = () => {
|
||||||
this.setState({ processing: true });
|
this.setState({ processing: true });
|
||||||
redirectingAuthorizedFetch(TIME_ENDPOINT,
|
redirectingAuthorizedFetch(TIME_ENDPOINT, {
|
||||||
{
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(this.createTime()),
|
body: JSON.stringify(this.createTime()),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.props.enqueueSnackbar("Time set successfully", { variant: 'success' });
|
this.props.enqueueSnackbar('Time set successfully', {
|
||||||
this.setState({ processing: false, settingTime: false }, this.props.loadData);
|
variant: 'success'
|
||||||
|
});
|
||||||
|
this.setState(
|
||||||
|
{ processing: false, settingTime: false },
|
||||||
|
this.props.loadData
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw Error("Error setting time, status code: " + response.status);
|
throw Error('Error setting time, status code: ' + response.status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message || "Problem setting the time", { variant: 'error' });
|
this.props.enqueueSnackbar(
|
||||||
|
error.message || 'Problem setting the time',
|
||||||
|
{ variant: 'error' }
|
||||||
|
);
|
||||||
this.setState({ processing: false, settingTime: false });
|
this.setState({ processing: false, settingTime: false });
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
renderSetTimeDialog() {
|
renderSetTimeDialog() {
|
||||||
return (
|
return (
|
||||||
@@ -94,7 +125,9 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
>
|
>
|
||||||
<DialogTitle>Set Time</DialogTitle>
|
<DialogTitle>Set Time</DialogTitle>
|
||||||
<DialogContent dividers>
|
<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
|
<TextField
|
||||||
label="Local Time"
|
label="Local Time"
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
@@ -104,24 +137,35 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
fullWidth
|
fullWidth
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: true,
|
shrink: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={this.closeSetTime} color="secondary">
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.closeSetTime}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</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
|
Set Time
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, theme } = this.props
|
const { data, theme } = this.props;
|
||||||
const me = this.props.authenticatedContext.me;
|
const me = this.props.authenticatedContext.me;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -154,7 +198,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<AccessTimeIcon />
|
<AccessTimeIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Local Time" secondary={formatDateTime(data.local_time)} />
|
<ListItemText
|
||||||
|
primary="Local Time"
|
||||||
|
secondary={formatDateTime(data.local_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -163,7 +210,10 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<SwapVerticalCircleIcon />
|
<SwapVerticalCircleIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="UTC Time" secondary={formatDateTime(data.utc_time)} />
|
<ListItemText
|
||||||
|
primary="UTC Time"
|
||||||
|
secondary={formatDateTime(data.utc_time)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
<ListItem>
|
<ListItem>
|
||||||
@@ -172,19 +222,32 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
|
|||||||
<AvTimerIcon />
|
<AvTimerIcon />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Uptime" secondary={formatDuration(data.uptime)} />
|
<ListItemText
|
||||||
|
primary="Uptime"
|
||||||
|
secondary={formatDuration(data.uptime)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</List>
|
</List>
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1} padding={1}>
|
<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
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</Box>
|
</Box>
|
||||||
{me.admin && !isNtpActive(data) && (
|
{me.admin && !isNtpActive(data) && (
|
||||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
<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
|
Set Time
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import NTPStatusController from './NTPStatusController';
|
import NTPStatusController from './NTPStatusController';
|
||||||
@@ -12,8 +16,7 @@ import NTPSettingsController from './NTPSettingsController';
|
|||||||
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
type NetworkTimeProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class NetworkTime extends Component<NetworkTimeProps> {
|
class NetworkTime extends Component<NetworkTimeProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,19 +24,34 @@ class NetworkTime extends Component<NetworkTimeProps> {
|
|||||||
const { authenticatedContext } = this.props;
|
const { authenticatedContext } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Network Time">
|
<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/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>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/ntp/status" component={NTPStatusController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/ntp/settings" component={NTPSettingsController} />
|
exact
|
||||||
|
path="/ntp/status"
|
||||||
|
component={NTPStatusController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/ntp/settings"
|
||||||
|
component={NTPSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/ntp/status" />
|
<Redirect to="/ntp/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticatedContext(NetworkTime)
|
export default withAuthenticatedContext(NetworkTime);
|
||||||
|
|||||||
@@ -1,479 +1,480 @@
|
|||||||
import React from 'react';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
type TimeZones = {
|
type TimeZones = {
|
||||||
[name: string]: string
|
[name: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TIME_ZONES: TimeZones = {
|
export const TIME_ZONES: TimeZones = {
|
||||||
"Africa/Abidjan": "GMT0",
|
'Africa/Abidjan': 'GMT0',
|
||||||
"Africa/Accra": "GMT0",
|
'Africa/Accra': 'GMT0',
|
||||||
"Africa/Addis_Ababa": "EAT-3",
|
'Africa/Addis_Ababa': 'EAT-3',
|
||||||
"Africa/Algiers": "CET-1",
|
'Africa/Algiers': 'CET-1',
|
||||||
"Africa/Asmara": "EAT-3",
|
'Africa/Asmara': 'EAT-3',
|
||||||
"Africa/Bamako": "GMT0",
|
'Africa/Bamako': 'GMT0',
|
||||||
"Africa/Bangui": "WAT-1",
|
'Africa/Bangui': 'WAT-1',
|
||||||
"Africa/Banjul": "GMT0",
|
'Africa/Banjul': 'GMT0',
|
||||||
"Africa/Bissau": "GMT0",
|
'Africa/Bissau': 'GMT0',
|
||||||
"Africa/Blantyre": "CAT-2",
|
'Africa/Blantyre': 'CAT-2',
|
||||||
"Africa/Brazzaville": "WAT-1",
|
'Africa/Brazzaville': 'WAT-1',
|
||||||
"Africa/Bujumbura": "CAT-2",
|
'Africa/Bujumbura': 'CAT-2',
|
||||||
"Africa/Cairo": "EET-2",
|
'Africa/Cairo': 'EET-2',
|
||||||
"Africa/Casablanca": "UNK-1",
|
'Africa/Casablanca': 'UNK-1',
|
||||||
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Africa/Ceuta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Africa/Conakry": "GMT0",
|
'Africa/Conakry': 'GMT0',
|
||||||
"Africa/Dakar": "GMT0",
|
'Africa/Dakar': 'GMT0',
|
||||||
"Africa/Dar_es_Salaam": "EAT-3",
|
'Africa/Dar_es_Salaam': 'EAT-3',
|
||||||
"Africa/Djibouti": "EAT-3",
|
'Africa/Djibouti': 'EAT-3',
|
||||||
"Africa/Douala": "WAT-1",
|
'Africa/Douala': 'WAT-1',
|
||||||
"Africa/El_Aaiun": "UNK-1",
|
'Africa/El_Aaiun': 'UNK-1',
|
||||||
"Africa/Freetown": "GMT0",
|
'Africa/Freetown': 'GMT0',
|
||||||
"Africa/Gaborone": "CAT-2",
|
'Africa/Gaborone': 'CAT-2',
|
||||||
"Africa/Harare": "CAT-2",
|
'Africa/Harare': 'CAT-2',
|
||||||
"Africa/Johannesburg": "SAST-2",
|
'Africa/Johannesburg': 'SAST-2',
|
||||||
"Africa/Juba": "EAT-3",
|
'Africa/Juba': 'EAT-3',
|
||||||
"Africa/Kampala": "EAT-3",
|
'Africa/Kampala': 'EAT-3',
|
||||||
"Africa/Khartoum": "CAT-2",
|
'Africa/Khartoum': 'CAT-2',
|
||||||
"Africa/Kigali": "CAT-2",
|
'Africa/Kigali': 'CAT-2',
|
||||||
"Africa/Kinshasa": "WAT-1",
|
'Africa/Kinshasa': 'WAT-1',
|
||||||
"Africa/Lagos": "WAT-1",
|
'Africa/Lagos': 'WAT-1',
|
||||||
"Africa/Libreville": "WAT-1",
|
'Africa/Libreville': 'WAT-1',
|
||||||
"Africa/Lome": "GMT0",
|
'Africa/Lome': 'GMT0',
|
||||||
"Africa/Luanda": "WAT-1",
|
'Africa/Luanda': 'WAT-1',
|
||||||
"Africa/Lubumbashi": "CAT-2",
|
'Africa/Lubumbashi': 'CAT-2',
|
||||||
"Africa/Lusaka": "CAT-2",
|
'Africa/Lusaka': 'CAT-2',
|
||||||
"Africa/Malabo": "WAT-1",
|
'Africa/Malabo': 'WAT-1',
|
||||||
"Africa/Maputo": "CAT-2",
|
'Africa/Maputo': 'CAT-2',
|
||||||
"Africa/Maseru": "SAST-2",
|
'Africa/Maseru': 'SAST-2',
|
||||||
"Africa/Mbabane": "SAST-2",
|
'Africa/Mbabane': 'SAST-2',
|
||||||
"Africa/Mogadishu": "EAT-3",
|
'Africa/Mogadishu': 'EAT-3',
|
||||||
"Africa/Monrovia": "GMT0",
|
'Africa/Monrovia': 'GMT0',
|
||||||
"Africa/Nairobi": "EAT-3",
|
'Africa/Nairobi': 'EAT-3',
|
||||||
"Africa/Ndjamena": "WAT-1",
|
'Africa/Ndjamena': 'WAT-1',
|
||||||
"Africa/Niamey": "WAT-1",
|
'Africa/Niamey': 'WAT-1',
|
||||||
"Africa/Nouakchott": "GMT0",
|
'Africa/Nouakchott': 'GMT0',
|
||||||
"Africa/Ouagadougou": "GMT0",
|
'Africa/Ouagadougou': 'GMT0',
|
||||||
"Africa/Porto-Novo": "WAT-1",
|
'Africa/Porto-Novo': 'WAT-1',
|
||||||
"Africa/Sao_Tome": "GMT0",
|
'Africa/Sao_Tome': 'GMT0',
|
||||||
"Africa/Tripoli": "EET-2",
|
'Africa/Tripoli': 'EET-2',
|
||||||
"Africa/Tunis": "CET-1",
|
'Africa/Tunis': 'CET-1',
|
||||||
"Africa/Windhoek": "CAT-2",
|
'Africa/Windhoek': 'CAT-2',
|
||||||
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
|
'America/Adak': 'HST10HDT,M3.2.0,M11.1.0',
|
||||||
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Anchorage': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Anguilla": "AST4",
|
'America/Anguilla': 'AST4',
|
||||||
"America/Antigua": "AST4",
|
'America/Antigua': 'AST4',
|
||||||
"America/Araguaina": "UNK3",
|
'America/Araguaina': 'UNK3',
|
||||||
"America/Argentina/Buenos_Aires": "UNK3",
|
'America/Argentina/Buenos_Aires': 'UNK3',
|
||||||
"America/Argentina/Catamarca": "UNK3",
|
'America/Argentina/Catamarca': 'UNK3',
|
||||||
"America/Argentina/Cordoba": "UNK3",
|
'America/Argentina/Cordoba': 'UNK3',
|
||||||
"America/Argentina/Jujuy": "UNK3",
|
'America/Argentina/Jujuy': 'UNK3',
|
||||||
"America/Argentina/La_Rioja": "UNK3",
|
'America/Argentina/La_Rioja': 'UNK3',
|
||||||
"America/Argentina/Mendoza": "UNK3",
|
'America/Argentina/Mendoza': 'UNK3',
|
||||||
"America/Argentina/Rio_Gallegos": "UNK3",
|
'America/Argentina/Rio_Gallegos': 'UNK3',
|
||||||
"America/Argentina/Salta": "UNK3",
|
'America/Argentina/Salta': 'UNK3',
|
||||||
"America/Argentina/San_Juan": "UNK3",
|
'America/Argentina/San_Juan': 'UNK3',
|
||||||
"America/Argentina/San_Luis": "UNK3",
|
'America/Argentina/San_Luis': 'UNK3',
|
||||||
"America/Argentina/Tucuman": "UNK3",
|
'America/Argentina/Tucuman': 'UNK3',
|
||||||
"America/Argentina/Ushuaia": "UNK3",
|
'America/Argentina/Ushuaia': 'UNK3',
|
||||||
"America/Aruba": "AST4",
|
'America/Aruba': 'AST4',
|
||||||
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
|
'America/Asuncion': 'UNK4UNK,M10.1.0/0,M3.4.0/0',
|
||||||
"America/Atikokan": "EST5",
|
'America/Atikokan': 'EST5',
|
||||||
"America/Bahia": "UNK3",
|
'America/Bahia': 'UNK3',
|
||||||
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Bahia_Banderas': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Barbados": "AST4",
|
'America/Barbados': 'AST4',
|
||||||
"America/Belem": "UNK3",
|
'America/Belem': 'UNK3',
|
||||||
"America/Belize": "CST6",
|
'America/Belize': 'CST6',
|
||||||
"America/Blanc-Sablon": "AST4",
|
'America/Blanc-Sablon': 'AST4',
|
||||||
"America/Boa_Vista": "UNK4",
|
'America/Boa_Vista': 'UNK4',
|
||||||
"America/Bogota": "UNK5",
|
'America/Bogota': 'UNK5',
|
||||||
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Boise': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Cambridge_Bay': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Campo_Grande": "UNK4",
|
'America/Campo_Grande': 'UNK4',
|
||||||
"America/Cancun": "EST5",
|
'America/Cancun': 'EST5',
|
||||||
"America/Caracas": "UNK4",
|
'America/Caracas': 'UNK4',
|
||||||
"America/Cayenne": "UNK3",
|
'America/Cayenne': 'UNK3',
|
||||||
"America/Cayman": "EST5",
|
'America/Cayman': 'EST5',
|
||||||
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Chicago': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
|
'America/Chihuahua': 'MST7MDT,M4.1.0,M10.5.0',
|
||||||
"America/Costa_Rica": "CST6",
|
'America/Costa_Rica': 'CST6',
|
||||||
"America/Creston": "MST7",
|
'America/Creston': 'MST7',
|
||||||
"America/Cuiaba": "UNK4",
|
'America/Cuiaba': 'UNK4',
|
||||||
"America/Curacao": "AST4",
|
'America/Curacao': 'AST4',
|
||||||
"America/Danmarkshavn": "GMT0",
|
'America/Danmarkshavn': 'GMT0',
|
||||||
"America/Dawson": "MST7",
|
'America/Dawson': 'MST7',
|
||||||
"America/Dawson_Creek": "MST7",
|
'America/Dawson_Creek': 'MST7',
|
||||||
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Denver': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Detroit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Dominica": "AST4",
|
'America/Dominica': 'AST4',
|
||||||
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Edmonton': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Eirunepe": "UNK5",
|
'America/Eirunepe': 'UNK5',
|
||||||
"America/El_Salvador": "CST6",
|
'America/El_Salvador': 'CST6',
|
||||||
"America/Fort_Nelson": "MST7",
|
'America/Fort_Nelson': 'MST7',
|
||||||
"America/Fortaleza": "UNK3",
|
'America/Fortaleza': 'UNK3',
|
||||||
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Glace_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
|
'America/Godthab': 'UNK3UNK,M3.5.0/-2,M10.5.0/-1',
|
||||||
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Goose_Bay': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Grand_Turk': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Grenada": "AST4",
|
'America/Grenada': 'AST4',
|
||||||
"America/Guadeloupe": "AST4",
|
'America/Guadeloupe': 'AST4',
|
||||||
"America/Guatemala": "CST6",
|
'America/Guatemala': 'CST6',
|
||||||
"America/Guayaquil": "UNK5",
|
'America/Guayaquil': 'UNK5',
|
||||||
"America/Guyana": "UNK4",
|
'America/Guyana': 'UNK4',
|
||||||
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Halifax': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
|
'America/Havana': 'CST5CDT,M3.2.0/0,M11.1.0/1',
|
||||||
"America/Hermosillo": "MST7",
|
'America/Hermosillo': 'MST7',
|
||||||
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Indiana/Indianapolis': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Knox": "CST6CDT,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/Marengo': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Petersburg": "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/Tell_City': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Vevay": "EST5EDT,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/Vincennes': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Indiana/Winamac": "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/Inuvik': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Iqaluit': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Jamaica": "EST5",
|
'America/Jamaica': 'EST5',
|
||||||
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Juneau': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Kentucky/Louisville": "EST5EDT,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/Kentucky/Monticello': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Kralendijk": "AST4",
|
'America/Kralendijk': 'AST4',
|
||||||
"America/La_Paz": "UNK4",
|
'America/La_Paz': 'UNK4',
|
||||||
"America/Lima": "UNK5",
|
'America/Lima': 'UNK5',
|
||||||
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Los_Angeles': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Lower_Princes": "AST4",
|
'America/Lower_Princes': 'AST4',
|
||||||
"America/Maceio": "UNK3",
|
'America/Maceio': 'UNK3',
|
||||||
"America/Managua": "CST6",
|
'America/Managua': 'CST6',
|
||||||
"America/Manaus": "UNK4",
|
'America/Manaus': 'UNK4',
|
||||||
"America/Marigot": "AST4",
|
'America/Marigot': 'AST4',
|
||||||
"America/Martinique": "AST4",
|
'America/Martinique': 'AST4',
|
||||||
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Matamoros': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
|
'America/Mazatlan': 'MST7MDT,M4.1.0,M10.5.0',
|
||||||
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Menominee': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Merida': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Metlakatla': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
|
'America/Mexico_City': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
|
'America/Miquelon': 'UNK3UNK,M3.2.0,M11.1.0',
|
||||||
"America/Moncton": "AST4ADT,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/Monterrey': 'CST6CDT,M4.1.0,M10.5.0',
|
||||||
"America/Montevideo": "UNK3",
|
'America/Montevideo': 'UNK3',
|
||||||
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Montreal': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Montserrat": "AST4",
|
'America/Montserrat': 'AST4',
|
||||||
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Nassau': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/New_York": "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/Nipigon': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Nome': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Noronha": "UNK2",
|
'America/Noronha': 'UNK2',
|
||||||
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
|
'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/Center': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/North_Dakota/New_Salem": "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/Ojinaga': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"America/Panama": "EST5",
|
'America/Panama': 'EST5',
|
||||||
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Pangnirtung': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Paramaribo": "UNK3",
|
'America/Paramaribo': 'UNK3',
|
||||||
"America/Phoenix": "MST7",
|
'America/Phoenix': 'MST7',
|
||||||
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Port-au-Prince': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Port_of_Spain": "AST4",
|
'America/Port_of_Spain': 'AST4',
|
||||||
"America/Porto_Velho": "UNK4",
|
'America/Porto_Velho': 'UNK4',
|
||||||
"America/Puerto_Rico": "AST4",
|
'America/Puerto_Rico': 'AST4',
|
||||||
"America/Punta_Arenas": "UNK3",
|
'America/Punta_Arenas': 'UNK3',
|
||||||
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Rainy_River': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Rankin_Inlet': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Recife": "UNK3",
|
'America/Recife': 'UNK3',
|
||||||
"America/Regina": "CST6",
|
'America/Regina': 'CST6',
|
||||||
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Resolute': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Rio_Branco": "UNK5",
|
'America/Rio_Branco': 'UNK5',
|
||||||
"America/Santarem": "UNK3",
|
'America/Santarem': 'UNK3',
|
||||||
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
|
'America/Santiago': 'UNK4UNK,M9.1.6/24,M4.1.6/24',
|
||||||
"America/Santo_Domingo": "AST4",
|
'America/Santo_Domingo': 'AST4',
|
||||||
"America/Sao_Paulo": "UNK3",
|
'America/Sao_Paulo': 'UNK3',
|
||||||
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
'America/Scoresbysund': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||||
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Sitka': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/St_Barthelemy": "AST4",
|
'America/St_Barthelemy': 'AST4',
|
||||||
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
|
'America/St_Johns': 'NST3:30NDT,M3.2.0,M11.1.0',
|
||||||
"America/St_Kitts": "AST4",
|
'America/St_Kitts': 'AST4',
|
||||||
"America/St_Lucia": "AST4",
|
'America/St_Lucia': 'AST4',
|
||||||
"America/St_Thomas": "AST4",
|
'America/St_Thomas': 'AST4',
|
||||||
"America/St_Vincent": "AST4",
|
'America/St_Vincent': 'AST4',
|
||||||
"America/Swift_Current": "CST6",
|
'America/Swift_Current': 'CST6',
|
||||||
"America/Tegucigalpa": "CST6",
|
'America/Tegucigalpa': 'CST6',
|
||||||
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
|
'America/Thule': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"America/Thunder_Bay": "EST5EDT,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/Tijuana': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
|
'America/Toronto': 'EST5EDT,M3.2.0,M11.1.0',
|
||||||
"America/Tortola": "AST4",
|
'America/Tortola': 'AST4',
|
||||||
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
|
'America/Vancouver': 'PST8PDT,M3.2.0,M11.1.0',
|
||||||
"America/Whitehorse": "MST7",
|
'America/Whitehorse': 'MST7',
|
||||||
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
|
'America/Winnipeg': 'CST6CDT,M3.2.0,M11.1.0',
|
||||||
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
|
'America/Yakutat': 'AKST9AKDT,M3.2.0,M11.1.0',
|
||||||
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
|
'America/Yellowknife': 'MST7MDT,M3.2.0,M11.1.0',
|
||||||
"Antarctica/Casey": "UNK-8",
|
'Antarctica/Casey': 'UNK-8',
|
||||||
"Antarctica/Davis": "UNK-7",
|
'Antarctica/Davis': 'UNK-7',
|
||||||
"Antarctica/DumontDUrville": "UNK-10",
|
'Antarctica/DumontDUrville': 'UNK-10',
|
||||||
"Antarctica/Macquarie": "UNK-11",
|
'Antarctica/Macquarie': 'UNK-11',
|
||||||
"Antarctica/Mawson": "UNK-5",
|
'Antarctica/Mawson': 'UNK-5',
|
||||||
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
'Antarctica/McMurdo': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||||
"Antarctica/Palmer": "UNK3",
|
'Antarctica/Palmer': 'UNK3',
|
||||||
"Antarctica/Rothera": "UNK3",
|
'Antarctica/Rothera': 'UNK3',
|
||||||
"Antarctica/Syowa": "UNK-3",
|
'Antarctica/Syowa': 'UNK-3',
|
||||||
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
|
'Antarctica/Troll': 'UNK0UNK-2,M3.5.0/1,M10.5.0/3',
|
||||||
"Antarctica/Vostok": "UNK-6",
|
'Antarctica/Vostok': 'UNK-6',
|
||||||
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Arctic/Longyearbyen': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Asia/Aden": "UNK-3",
|
'Asia/Aden': 'UNK-3',
|
||||||
"Asia/Almaty": "UNK-6",
|
'Asia/Almaty': 'UNK-6',
|
||||||
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
|
'Asia/Amman': 'EET-2EEST,M3.5.4/24,M10.5.5/1',
|
||||||
"Asia/Anadyr": "UNK-12",
|
'Asia/Anadyr': 'UNK-12',
|
||||||
"Asia/Aqtau": "UNK-5",
|
'Asia/Aqtau': 'UNK-5',
|
||||||
"Asia/Aqtobe": "UNK-5",
|
'Asia/Aqtobe': 'UNK-5',
|
||||||
"Asia/Ashgabat": "UNK-5",
|
'Asia/Ashgabat': 'UNK-5',
|
||||||
"Asia/Atyrau": "UNK-5",
|
'Asia/Atyrau': 'UNK-5',
|
||||||
"Asia/Baghdad": "UNK-3",
|
'Asia/Baghdad': 'UNK-3',
|
||||||
"Asia/Bahrain": "UNK-3",
|
'Asia/Bahrain': 'UNK-3',
|
||||||
"Asia/Baku": "UNK-4",
|
'Asia/Baku': 'UNK-4',
|
||||||
"Asia/Bangkok": "UNK-7",
|
'Asia/Bangkok': 'UNK-7',
|
||||||
"Asia/Barnaul": "UNK-7",
|
'Asia/Barnaul': 'UNK-7',
|
||||||
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
|
'Asia/Beirut': 'EET-2EEST,M3.5.0/0,M10.5.0/0',
|
||||||
"Asia/Bishkek": "UNK-6",
|
'Asia/Bishkek': 'UNK-6',
|
||||||
"Asia/Brunei": "UNK-8",
|
'Asia/Brunei': 'UNK-8',
|
||||||
"Asia/Chita": "UNK-9",
|
'Asia/Chita': 'UNK-9',
|
||||||
"Asia/Choibalsan": "UNK-8",
|
'Asia/Choibalsan': 'UNK-8',
|
||||||
"Asia/Colombo": "UNK-5:30",
|
'Asia/Colombo': 'UNK-5:30',
|
||||||
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
|
'Asia/Damascus': 'EET-2EEST,M3.5.5/0,M10.5.5/0',
|
||||||
"Asia/Dhaka": "UNK-6",
|
'Asia/Dhaka': 'UNK-6',
|
||||||
"Asia/Dili": "UNK-9",
|
'Asia/Dili': 'UNK-9',
|
||||||
"Asia/Dubai": "UNK-4",
|
'Asia/Dubai': 'UNK-4',
|
||||||
"Asia/Dushanbe": "UNK-5",
|
'Asia/Dushanbe': 'UNK-5',
|
||||||
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'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/Gaza': 'EET-2EEST,M3.5.5/0,M10.5.6/1',
|
||||||
"Asia/Hebron": "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/Ho_Chi_Minh': 'UNK-7',
|
||||||
"Asia/Hong_Kong": "HKT-8",
|
'Asia/Hong_Kong': 'HKT-8',
|
||||||
"Asia/Hovd": "UNK-7",
|
'Asia/Hovd': 'UNK-7',
|
||||||
"Asia/Irkutsk": "UNK-8",
|
'Asia/Irkutsk': 'UNK-8',
|
||||||
"Asia/Jakarta": "WIB-7",
|
'Asia/Jakarta': 'WIB-7',
|
||||||
"Asia/Jayapura": "WIT-9",
|
'Asia/Jayapura': 'WIT-9',
|
||||||
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
|
'Asia/Jerusalem': 'IST-2IDT,M3.4.4/26,M10.5.0',
|
||||||
"Asia/Kabul": "UNK-4:30",
|
'Asia/Kabul': 'UNK-4:30',
|
||||||
"Asia/Kamchatka": "UNK-12",
|
'Asia/Kamchatka': 'UNK-12',
|
||||||
"Asia/Karachi": "PKT-5",
|
'Asia/Karachi': 'PKT-5',
|
||||||
"Asia/Kathmandu": "UNK-5:45",
|
'Asia/Kathmandu': 'UNK-5:45',
|
||||||
"Asia/Khandyga": "UNK-9",
|
'Asia/Khandyga': 'UNK-9',
|
||||||
"Asia/Kolkata": "IST-5:30",
|
'Asia/Kolkata': 'IST-5:30',
|
||||||
"Asia/Krasnoyarsk": "UNK-7",
|
'Asia/Krasnoyarsk': 'UNK-7',
|
||||||
"Asia/Kuala_Lumpur": "UNK-8",
|
'Asia/Kuala_Lumpur': 'UNK-8',
|
||||||
"Asia/Kuching": "UNK-8",
|
'Asia/Kuching': 'UNK-8',
|
||||||
"Asia/Kuwait": "UNK-3",
|
'Asia/Kuwait': 'UNK-3',
|
||||||
"Asia/Macau": "CST-8",
|
'Asia/Macau': 'CST-8',
|
||||||
"Asia/Magadan": "UNK-11",
|
'Asia/Magadan': 'UNK-11',
|
||||||
"Asia/Makassar": "WITA-8",
|
'Asia/Makassar': 'WITA-8',
|
||||||
"Asia/Manila": "PST-8",
|
'Asia/Manila': 'PST-8',
|
||||||
"Asia/Muscat": "UNK-4",
|
'Asia/Muscat': 'UNK-4',
|
||||||
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Asia/Nicosia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Asia/Novokuznetsk": "UNK-7",
|
'Asia/Novokuznetsk': 'UNK-7',
|
||||||
"Asia/Novosibirsk": "UNK-7",
|
'Asia/Novosibirsk': 'UNK-7',
|
||||||
"Asia/Omsk": "UNK-6",
|
'Asia/Omsk': 'UNK-6',
|
||||||
"Asia/Oral": "UNK-5",
|
'Asia/Oral': 'UNK-5',
|
||||||
"Asia/Phnom_Penh": "UNK-7",
|
'Asia/Phnom_Penh': 'UNK-7',
|
||||||
"Asia/Pontianak": "WIB-7",
|
'Asia/Pontianak': 'WIB-7',
|
||||||
"Asia/Pyongyang": "KST-9",
|
'Asia/Pyongyang': 'KST-9',
|
||||||
"Asia/Qatar": "UNK-3",
|
'Asia/Qatar': 'UNK-3',
|
||||||
"Asia/Qyzylorda": "UNK-5",
|
'Asia/Qyzylorda': 'UNK-5',
|
||||||
"Asia/Riyadh": "UNK-3",
|
'Asia/Riyadh': 'UNK-3',
|
||||||
"Asia/Sakhalin": "UNK-11",
|
'Asia/Sakhalin': 'UNK-11',
|
||||||
"Asia/Samarkand": "UNK-5",
|
'Asia/Samarkand': 'UNK-5',
|
||||||
"Asia/Seoul": "KST-9",
|
'Asia/Seoul': 'KST-9',
|
||||||
"Asia/Shanghai": "CST-8",
|
'Asia/Shanghai': 'CST-8',
|
||||||
"Asia/Singapore": "UNK-8",
|
'Asia/Singapore': 'UNK-8',
|
||||||
"Asia/Srednekolymsk": "UNK-11",
|
'Asia/Srednekolymsk': 'UNK-11',
|
||||||
"Asia/Taipei": "CST-8",
|
'Asia/Taipei': 'CST-8',
|
||||||
"Asia/Tashkent": "UNK-5",
|
'Asia/Tashkent': 'UNK-5',
|
||||||
"Asia/Tbilisi": "UNK-4",
|
'Asia/Tbilisi': 'UNK-4',
|
||||||
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
|
'Asia/Tehran': 'UNK-3:30UNK,J79/24,J263/24',
|
||||||
"Asia/Thimphu": "UNK-6",
|
'Asia/Thimphu': 'UNK-6',
|
||||||
"Asia/Tokyo": "JST-9",
|
'Asia/Tokyo': 'JST-9',
|
||||||
"Asia/Tomsk": "UNK-7",
|
'Asia/Tomsk': 'UNK-7',
|
||||||
"Asia/Ulaanbaatar": "UNK-8",
|
'Asia/Ulaanbaatar': 'UNK-8',
|
||||||
"Asia/Urumqi": "UNK-6",
|
'Asia/Urumqi': 'UNK-6',
|
||||||
"Asia/Ust-Nera": "UNK-10",
|
'Asia/Ust-Nera': 'UNK-10',
|
||||||
"Asia/Vientiane": "UNK-7",
|
'Asia/Vientiane': 'UNK-7',
|
||||||
"Asia/Vladivostok": "UNK-10",
|
'Asia/Vladivostok': 'UNK-10',
|
||||||
"Asia/Yakutsk": "UNK-9",
|
'Asia/Yakutsk': 'UNK-9',
|
||||||
"Asia/Yangon": "UNK-6:30",
|
'Asia/Yangon': 'UNK-6:30',
|
||||||
"Asia/Yekaterinburg": "UNK-5",
|
'Asia/Yekaterinburg': 'UNK-5',
|
||||||
"Asia/Yerevan": "UNK-4",
|
'Asia/Yerevan': 'UNK-4',
|
||||||
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
'Atlantic/Azores': 'UNK1UNK,M3.5.0/0,M10.5.0/1',
|
||||||
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
|
'Atlantic/Bermuda': 'AST4ADT,M3.2.0,M11.1.0',
|
||||||
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Canary': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Cape_Verde": "UNK1",
|
'Atlantic/Cape_Verde': 'UNK1',
|
||||||
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Faroe': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Atlantic/Madeira': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Atlantic/Reykjavik": "GMT0",
|
'Atlantic/Reykjavik': 'GMT0',
|
||||||
"Atlantic/South_Georgia": "UNK2",
|
'Atlantic/South_Georgia': 'UNK2',
|
||||||
"Atlantic/St_Helena": "GMT0",
|
'Atlantic/St_Helena': 'GMT0',
|
||||||
"Atlantic/Stanley": "UNK3",
|
'Atlantic/Stanley': 'UNK3',
|
||||||
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
'Australia/Adelaide': 'ACST-9:30ACDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Brisbane": "AEST-10",
|
'Australia/Brisbane': 'AEST-10',
|
||||||
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
'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/Currie': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Darwin": "ACST-9:30",
|
'Australia/Darwin': 'ACST-9:30',
|
||||||
"Australia/Eucla": "UNK-8:45",
|
'Australia/Eucla': 'UNK-8:45',
|
||||||
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Hobart': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Lindeman": "AEST-10",
|
'Australia/Lindeman': 'AEST-10',
|
||||||
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
|
'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/Melbourne': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Australia/Perth": "AWST-8",
|
'Australia/Perth': 'AWST-8',
|
||||||
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
'Australia/Sydney': 'AEST-10AEDT,M10.1.0,M4.1.0/3',
|
||||||
"Etc/GMT": "GMT0",
|
'Etc/GMT': 'GMT0',
|
||||||
"Etc/GMT+0": "GMT0",
|
'Etc/GMT+0': 'GMT0',
|
||||||
"Etc/GMT+1": "UNK1",
|
'Etc/GMT+1': 'UNK1',
|
||||||
"Etc/GMT+10": "UNK10",
|
'Etc/GMT+10': 'UNK10',
|
||||||
"Etc/GMT+11": "UNK11",
|
'Etc/GMT+11': 'UNK11',
|
||||||
"Etc/GMT+12": "UNK12",
|
'Etc/GMT+12': 'UNK12',
|
||||||
"Etc/GMT+2": "UNK2",
|
'Etc/GMT+2': 'UNK2',
|
||||||
"Etc/GMT+3": "UNK3",
|
'Etc/GMT+3': 'UNK3',
|
||||||
"Etc/GMT+4": "UNK4",
|
'Etc/GMT+4': 'UNK4',
|
||||||
"Etc/GMT+5": "UNK5",
|
'Etc/GMT+5': 'UNK5',
|
||||||
"Etc/GMT+6": "UNK6",
|
'Etc/GMT+6': 'UNK6',
|
||||||
"Etc/GMT+7": "UNK7",
|
'Etc/GMT+7': 'UNK7',
|
||||||
"Etc/GMT+8": "UNK8",
|
'Etc/GMT+8': 'UNK8',
|
||||||
"Etc/GMT+9": "UNK9",
|
'Etc/GMT+9': 'UNK9',
|
||||||
"Etc/GMT-0": "GMT0",
|
'Etc/GMT-0': 'GMT0',
|
||||||
"Etc/GMT-1": "UNK-1",
|
'Etc/GMT-1': 'UNK-1',
|
||||||
"Etc/GMT-10": "UNK-10",
|
'Etc/GMT-10': 'UNK-10',
|
||||||
"Etc/GMT-11": "UNK-11",
|
'Etc/GMT-11': 'UNK-11',
|
||||||
"Etc/GMT-12": "UNK-12",
|
'Etc/GMT-12': 'UNK-12',
|
||||||
"Etc/GMT-13": "UNK-13",
|
'Etc/GMT-13': 'UNK-13',
|
||||||
"Etc/GMT-14": "UNK-14",
|
'Etc/GMT-14': 'UNK-14',
|
||||||
"Etc/GMT-2": "UNK-2",
|
'Etc/GMT-2': 'UNK-2',
|
||||||
"Etc/GMT-3": "UNK-3",
|
'Etc/GMT-3': 'UNK-3',
|
||||||
"Etc/GMT-4": "UNK-4",
|
'Etc/GMT-4': 'UNK-4',
|
||||||
"Etc/GMT-5": "UNK-5",
|
'Etc/GMT-5': 'UNK-5',
|
||||||
"Etc/GMT-6": "UNK-6",
|
'Etc/GMT-6': 'UNK-6',
|
||||||
"Etc/GMT-7": "UNK-7",
|
'Etc/GMT-7': 'UNK-7',
|
||||||
"Etc/GMT-8": "UNK-8",
|
'Etc/GMT-8': 'UNK-8',
|
||||||
"Etc/GMT-9": "UNK-9",
|
'Etc/GMT-9': 'UNK-9',
|
||||||
"Etc/GMT0": "GMT0",
|
'Etc/GMT0': 'GMT0',
|
||||||
"Etc/Greenwich": "GMT0",
|
'Etc/Greenwich': 'GMT0',
|
||||||
"Etc/UCT": "UTC0",
|
'Etc/UCT': 'UTC0',
|
||||||
"Etc/UTC": "UTC0",
|
'Etc/UTC': 'UTC0',
|
||||||
"Etc/Universal": "UTC0",
|
'Etc/Universal': 'UTC0',
|
||||||
"Etc/Zulu": "UTC0",
|
'Etc/Zulu': 'UTC0',
|
||||||
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Amsterdam': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Andorra": "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/Astrakhan': 'UNK-4',
|
||||||
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/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/Belgrade': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Berlin": "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/Bratislava': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Brussels": "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/Bucharest': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Budapest': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Busingen": "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/Chisinau': 'EET-2EEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Copenhagen": "CET-1CEST,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/Dublin': 'IST-1GMT0,M10.5.0,M3.5.0/1',
|
||||||
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Gibraltar': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Guernsey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'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/Isle_of_Man': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Istanbul": "UNK-3",
|
'Europe/Istanbul': 'UNK-3',
|
||||||
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/Jersey': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Kaliningrad": "EET-2",
|
'Europe/Kaliningrad': 'EET-2',
|
||||||
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Kiev': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Kirov": "UNK-3",
|
'Europe/Kirov': 'UNK-3',
|
||||||
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
|
'Europe/Lisbon': 'WET0WEST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Ljubljana': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
|
'Europe/London': 'GMT0BST,M3.5.0/1,M10.5.0',
|
||||||
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Luxembourg': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Madrid": "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/Malta': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Mariehamn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Minsk": "UNK-3",
|
'Europe/Minsk': 'UNK-3',
|
||||||
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Monaco': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Moscow": "MSK-3",
|
'Europe/Moscow': 'MSK-3',
|
||||||
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Oslo': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Paris": "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/Podgorica': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Prague": "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/Riga': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Rome': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Samara": "UNK-4",
|
'Europe/Samara': 'UNK-4',
|
||||||
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/San_Marino': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Sarajevo": "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/Saratov': 'UNK-4',
|
||||||
"Europe/Simferopol": "MSK-3",
|
'Europe/Simferopol': 'MSK-3',
|
||||||
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/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/Sofia': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Stockholm': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Tallinn': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Tirane': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Ulyanovsk": "UNK-4",
|
'Europe/Ulyanovsk': 'UNK-4',
|
||||||
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/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/Vaduz': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vatican": "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/Vienna': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
'Europe/Vilnius': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Volgograd": "UNK-4",
|
'Europe/Volgograd': 'UNK-4',
|
||||||
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Warsaw': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Europe/Zagreb": "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/Zaporozhye': 'EET-2EEST,M3.5.0/3,M10.5.0/4',
|
||||||
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
|
'Europe/Zurich': 'CET-1CEST,M3.5.0,M10.5.0/3',
|
||||||
"Indian/Antananarivo": "EAT-3",
|
'Indian/Antananarivo': 'EAT-3',
|
||||||
"Indian/Chagos": "UNK-6",
|
'Indian/Chagos': 'UNK-6',
|
||||||
"Indian/Christmas": "UNK-7",
|
'Indian/Christmas': 'UNK-7',
|
||||||
"Indian/Cocos": "UNK-6:30",
|
'Indian/Cocos': 'UNK-6:30',
|
||||||
"Indian/Comoro": "EAT-3",
|
'Indian/Comoro': 'EAT-3',
|
||||||
"Indian/Kerguelen": "UNK-5",
|
'Indian/Kerguelen': 'UNK-5',
|
||||||
"Indian/Mahe": "UNK-4",
|
'Indian/Mahe': 'UNK-4',
|
||||||
"Indian/Maldives": "UNK-5",
|
'Indian/Maldives': 'UNK-5',
|
||||||
"Indian/Mauritius": "UNK-4",
|
'Indian/Mauritius': 'UNK-4',
|
||||||
"Indian/Mayotte": "EAT-3",
|
'Indian/Mayotte': 'EAT-3',
|
||||||
"Indian/Reunion": "UNK-4",
|
'Indian/Reunion': 'UNK-4',
|
||||||
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/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/Auckland': 'NZST-12NZDT,M9.5.0,M4.1.0/3',
|
||||||
"Pacific/Bougainville": "UNK-11",
|
'Pacific/Bougainville': 'UNK-11',
|
||||||
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
|
'Pacific/Chatham': 'UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45',
|
||||||
"Pacific/Chuuk": "UNK-10",
|
'Pacific/Chuuk': 'UNK-10',
|
||||||
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
|
'Pacific/Easter': 'UNK6UNK,M9.1.6/22,M4.1.6/22',
|
||||||
"Pacific/Efate": "UNK-11",
|
'Pacific/Efate': 'UNK-11',
|
||||||
"Pacific/Enderbury": "UNK-13",
|
'Pacific/Enderbury': 'UNK-13',
|
||||||
"Pacific/Fakaofo": "UNK-13",
|
'Pacific/Fakaofo': 'UNK-13',
|
||||||
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
|
'Pacific/Fiji': 'UNK-12UNK,M11.2.0,M1.2.3/99',
|
||||||
"Pacific/Funafuti": "UNK-12",
|
'Pacific/Funafuti': 'UNK-12',
|
||||||
"Pacific/Galapagos": "UNK6",
|
'Pacific/Galapagos': 'UNK6',
|
||||||
"Pacific/Gambier": "UNK9",
|
'Pacific/Gambier': 'UNK9',
|
||||||
"Pacific/Guadalcanal": "UNK-11",
|
'Pacific/Guadalcanal': 'UNK-11',
|
||||||
"Pacific/Guam": "ChST-10",
|
'Pacific/Guam': 'ChST-10',
|
||||||
"Pacific/Honolulu": "HST10",
|
'Pacific/Honolulu': 'HST10',
|
||||||
"Pacific/Kiritimati": "UNK-14",
|
'Pacific/Kiritimati': 'UNK-14',
|
||||||
"Pacific/Kosrae": "UNK-11",
|
'Pacific/Kosrae': 'UNK-11',
|
||||||
"Pacific/Kwajalein": "UNK-12",
|
'Pacific/Kwajalein': 'UNK-12',
|
||||||
"Pacific/Majuro": "UNK-12",
|
'Pacific/Majuro': 'UNK-12',
|
||||||
"Pacific/Marquesas": "UNK9:30",
|
'Pacific/Marquesas': 'UNK9:30',
|
||||||
"Pacific/Midway": "SST11",
|
'Pacific/Midway': 'SST11',
|
||||||
"Pacific/Nauru": "UNK-12",
|
'Pacific/Nauru': 'UNK-12',
|
||||||
"Pacific/Niue": "UNK11",
|
'Pacific/Niue': 'UNK11',
|
||||||
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
|
'Pacific/Norfolk': 'UNK-11UNK,M10.1.0,M4.1.0/3',
|
||||||
"Pacific/Noumea": "UNK-11",
|
'Pacific/Noumea': 'UNK-11',
|
||||||
"Pacific/Pago_Pago": "SST11",
|
'Pacific/Pago_Pago': 'SST11',
|
||||||
"Pacific/Palau": "UNK-9",
|
'Pacific/Palau': 'UNK-9',
|
||||||
"Pacific/Pitcairn": "UNK8",
|
'Pacific/Pitcairn': 'UNK8',
|
||||||
"Pacific/Pohnpei": "UNK-11",
|
'Pacific/Pohnpei': 'UNK-11',
|
||||||
"Pacific/Port_Moresby": "UNK-10",
|
'Pacific/Port_Moresby': 'UNK-10',
|
||||||
"Pacific/Rarotonga": "UNK10",
|
'Pacific/Rarotonga': 'UNK10',
|
||||||
"Pacific/Saipan": "ChST-10",
|
'Pacific/Saipan': 'ChST-10',
|
||||||
"Pacific/Tahiti": "UNK10",
|
'Pacific/Tahiti': 'UNK10',
|
||||||
"Pacific/Tarawa": "UNK-12",
|
'Pacific/Tarawa': 'UNK-12',
|
||||||
"Pacific/Tongatapu": "UNK-13",
|
'Pacific/Tongatapu': 'UNK-13',
|
||||||
"Pacific/Wake": "UNK-12",
|
'Pacific/Wake': 'UNK-12',
|
||||||
"Pacific/Wallis": "UNK-12"
|
'Pacific/Wallis': 'UNK-12'
|
||||||
}
|
};
|
||||||
|
|
||||||
export function selectedTimeZone(label: string, format: string) {
|
export function selectedTimeZone(label: string, format: string) {
|
||||||
return TIME_ZONES[label] === format ? label : undefined;
|
return TIME_ZONES[label] === format ? label : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function timeZoneSelectItems() {
|
export function timeZoneSelectItems() {
|
||||||
return Object.keys(TIME_ZONES).map(label => (
|
return Object.keys(TIME_ZONES).map((label) => (
|
||||||
<MenuItem key={label} value={label}>{label}</MenuItem>
|
<MenuItem key={label} value={label}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import parseMilliseconds from 'parse-ms'
|
import parseMilliseconds from 'parse-ms';
|
||||||
|
|
||||||
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
@@ -7,37 +7,37 @@ const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
|||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
second: 'numeric',
|
second: 'numeric',
|
||||||
hour12: false,
|
hour12: false
|
||||||
})
|
});
|
||||||
|
|
||||||
export const formatDateTime = (dateTime: string) => {
|
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) => {
|
export const formatLocalDateTime = (date: Date) => {
|
||||||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.substr(0, 19)
|
.substr(0, 19);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const formatDuration = (duration: number) => {
|
export const formatDuration = (duration: number) => {
|
||||||
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000)
|
const { days, hours, minutes, seconds } = parseMilliseconds(duration * 1000);
|
||||||
var formatted = ''
|
let formatted = '';
|
||||||
if (days) {
|
if (days) {
|
||||||
formatted += pluralize(days, 'day')
|
formatted += pluralize(days, 'day');
|
||||||
}
|
}
|
||||||
if (formatted || hours) {
|
if (formatted || hours) {
|
||||||
formatted += pluralize(hours, 'hour')
|
formatted += pluralize(hours, 'hour');
|
||||||
}
|
}
|
||||||
if (formatted || minutes) {
|
if (formatted || minutes) {
|
||||||
formatted += pluralize(minutes, 'minute')
|
formatted += pluralize(minutes, 'minute');
|
||||||
}
|
}
|
||||||
if (formatted || seconds) {
|
if (formatted || seconds) {
|
||||||
formatted += pluralize(seconds, 'second')
|
formatted += pluralize(seconds, 'second');
|
||||||
}
|
}
|
||||||
return formatted
|
return formatted;
|
||||||
}
|
};
|
||||||
|
|
||||||
const pluralize = (count: number, noun: string, suffix: string = 's') =>
|
const pluralize = (count: number, noun: string, suffix = 's') =>
|
||||||
` ${count} ${noun}${count !== 1 ? suffix : ''} `
|
` ${count} ${noun}${count !== 1 ? suffix : ''} `;
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
export enum NTPSyncStatus {
|
export enum NTPSyncStatus {
|
||||||
NTP_INACTIVE = 0,
|
NTP_INACTIVE = 0,
|
||||||
NTP_ACTIVE = 1,
|
NTP_ACTIVE = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NTPStatus {
|
export interface NTPStatus {
|
||||||
status: NTPSyncStatus
|
status: NTPSyncStatus;
|
||||||
utc_time: string
|
utc_time: string;
|
||||||
local_time: string
|
local_time: string;
|
||||||
server: string
|
server: string;
|
||||||
uptime: number
|
uptime: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NTPSettings {
|
export interface NTPSettings {
|
||||||
enabled: boolean
|
enabled: boolean;
|
||||||
server: string
|
server: string;
|
||||||
tz_label: string
|
tz_label: string;
|
||||||
tz_format: string
|
tz_format: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Time {
|
export interface Time {
|
||||||
local_time: string
|
local_time: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
import React from 'react';
|
|
||||||
import MenuItem from '@material-ui/core/MenuItem';
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
type BoardProfiles = {
|
type BoardProfiles = {
|
||||||
[name: string]: string
|
[name: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BOARD_PROFILES: BoardProfiles = {
|
export const BOARD_PROFILES: BoardProfiles = {
|
||||||
"S32": "BBQKees Gateway S32",
|
S32: 'BBQKees Gateway S32',
|
||||||
"E32": "BBQKees Gateway E32",
|
E32: 'BBQKees Gateway E32',
|
||||||
"NODEMCU": "NodeMCU 32S",
|
NODEMCU: 'NodeMCU 32S',
|
||||||
"MH-ET": "MH-ET Live D1 Mini",
|
'MH-ET': 'MH-ET Live D1 Mini',
|
||||||
"LOLIN": "Lolin D32",
|
LOLIN: 'Lolin D32',
|
||||||
"OLIMEX": "Olimex ESP32-EVB",
|
OLIMEX: 'Olimex ESP32-EVB',
|
||||||
"TLK110": "Generic Ethernet (TLK110)",
|
TLK110: 'Generic Ethernet (TLK110)',
|
||||||
"LAN8720": "Generic Ethernet (LAN8720)"
|
LAN8720: 'Generic Ethernet (LAN8720)'
|
||||||
}
|
};
|
||||||
|
|
||||||
export function boardProfileSelectItems() {
|
export function boardProfileSelectItems() {
|
||||||
return Object.keys(BOARD_PROFILES).map(code => (
|
return Object.keys(BOARD_PROFILES).map((code) => (
|
||||||
<MenuItem key={code} value={code}>{BOARD_PROFILES[code]}</MenuItem>
|
<MenuItem key={code} value={code}>
|
||||||
|
{BOARD_PROFILES[code]}
|
||||||
|
</MenuItem>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -12,30 +12,43 @@ import EMSESPDevicesController from './EMSESPDevicesController';
|
|||||||
import EMSESPHelp from './EMSESPHelp';
|
import EMSESPHelp from './EMSESPHelp';
|
||||||
|
|
||||||
class EMSESP extends Component<RouteComponentProps> {
|
class EMSESP extends Component<RouteComponentProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Dashboard">
|
<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}/devices`} label="Devices & Sensors" />
|
||||||
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
<Tab value={`/${PROJECT_PATH}/status`} label="EMS Status" />
|
||||||
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
<Tab value={`/${PROJECT_PATH}/help`} label="EMS-ESP Help" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/devices`} component={EMSESPDevicesController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/status`} component={EMSESPStatusController} />
|
exact
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/help`} component={EMSESPHelp} />
|
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`} />
|
<Redirect to={`/${PROJECT_PATH}/devices`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EMSESP;
|
export default EMSESP;
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import React, { Component } from 'react';
|
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 { ENDPOINT_ROOT } from '../api';
|
||||||
import EMSESPDevicesForm from './EMSESPDevicesForm';
|
import EMSESPDevicesForm from './EMSESPDevicesForm';
|
||||||
import { EMSESPDevices } from './EMSESPtypes';
|
import { EMSESPDevices } from './EMSESPtypes';
|
||||||
|
|
||||||
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + "allDevices";
|
export const EMSESP_DEVICES_ENDPOINT = ENDPOINT_ROOT + 'allDevices';
|
||||||
|
|
||||||
type EMSESPDevicesControllerProps = RestControllerProps<EMSESPDevices>;
|
type EMSESPDevicesControllerProps = RestControllerProps<EMSESPDevices>;
|
||||||
|
|
||||||
class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
|
class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -20,10 +24,10 @@ class EMSESPDevicesController extends Component<EMSESPDevicesControllerProps> {
|
|||||||
<SectionContent title="Devices & Sensors">
|
<SectionContent title="Devices & Sensors">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <EMSESPDevicesForm {...formProps} />}
|
render={(formProps) => <EMSESPDevicesForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,62 @@
|
|||||||
import React, { Component, Fragment } from "react";
|
import React, { Component, Fragment } from 'react';
|
||||||
import { withStyles, Theme, createStyles } from "@material-ui/core/styles";
|
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Table, TableBody, TableCell, TableHead, TableRow, TableContainer, withWidth, WithWidthProps, isWidthDown,
|
Table,
|
||||||
Button, Tooltip, DialogTitle, DialogContent, DialogActions, Box, Dialog, Typography
|
TableBody,
|
||||||
} from "@material-ui/core";
|
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 RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
import ListIcon from "@material-ui/icons/List";
|
import ListIcon from '@material-ui/icons/List';
|
||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
import EditIcon from '@material-ui/icons/Edit';
|
||||||
|
|
||||||
import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication";
|
import {
|
||||||
import { RestFormProps, FormButton, extractEventValue } from "../components";
|
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 ValueForm from './ValueForm';
|
||||||
|
|
||||||
import { ENDPOINT_ROOT } from "../api";
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
|
|
||||||
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + "scanDevices";
|
export const SCANDEVICES_ENDPOINT = ENDPOINT_ROOT + 'scanDevices';
|
||||||
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + "deviceData";
|
export const DEVICE_DATA_ENDPOINT = ENDPOINT_ROOT + 'deviceData';
|
||||||
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + "writeValue";
|
export const WRITE_VALUE_ENDPOINT = ENDPOINT_ROOT + 'writeValue';
|
||||||
|
|
||||||
const StyledTableCell = withStyles((theme: Theme) =>
|
const StyledTableCell = withStyles((theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
head: {
|
head: {
|
||||||
backgroundColor: theme.palette.common.black,
|
backgroundColor: theme.palette.common.black,
|
||||||
color: theme.palette.common.white,
|
color: theme.palette.common.white
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
fontSize: 14,
|
fontSize: 14
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
)(TableCell);
|
)(TableCell);
|
||||||
|
|
||||||
@@ -42,8 +66,8 @@ const CustomTooltip = withStyles((theme: Theme) => ({
|
|||||||
color: 'white',
|
color: 'white',
|
||||||
boxShadow: theme.shadows[1],
|
boxShadow: theme.shadows[1],
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
border: '1px solid #dadde9',
|
border: '1px solid #dadde9'
|
||||||
},
|
}
|
||||||
}))(Tooltip);
|
}))(Tooltip);
|
||||||
|
|
||||||
function compareDevices(a: Device, b: Device) {
|
function compareDevices(a: Device, b: Device) {
|
||||||
@@ -64,63 +88,81 @@ interface EMSESPDevicesFormState {
|
|||||||
devicevalue?: DeviceValue;
|
devicevalue?: DeviceValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> & AuthenticatedContextProps & WithWidthProps;
|
type EMSESPDevicesFormProps = RestFormProps<EMSESPDevices> &
|
||||||
|
AuthenticatedContextProps &
|
||||||
|
WithWidthProps;
|
||||||
|
|
||||||
function formatTemp(t: string) {
|
function formatTemp(t: string) {
|
||||||
if (t == null) {
|
if (t == null) {
|
||||||
return "n/a";
|
return 'n/a';
|
||||||
}
|
}
|
||||||
return t + " °C";
|
return t + ' °C';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatUnit(u: string) {
|
function formatUnit(u: string) {
|
||||||
if (u == null) {
|
if (u == null) {
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
return " " + u;
|
return ' ' + u;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesFormState> {
|
class EMSESPDevicesForm extends Component<
|
||||||
|
EMSESPDevicesFormProps,
|
||||||
|
EMSESPDevicesFormState
|
||||||
|
> {
|
||||||
state: EMSESPDevicesFormState = {
|
state: EMSESPDevicesFormState = {
|
||||||
confirmScanDevices: false,
|
confirmScanDevices: false,
|
||||||
processing: false
|
processing: false
|
||||||
};
|
};
|
||||||
|
|
||||||
handleValueChange = (name: keyof DeviceValue) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
handleValueChange = (name: keyof DeviceValue) => (
|
||||||
this.setState({ devicevalue: { ...this.state.devicevalue!, [name]: extractEventValue(event) } });
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
this.setState({
|
||||||
|
devicevalue: {
|
||||||
|
...this.state.devicevalue!,
|
||||||
|
[name]: extractEventValue(event)
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
cancelEditingValue = () => {
|
cancelEditingValue = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
devicevalue: undefined
|
devicevalue: undefined
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
doneEditingValue = () => {
|
doneEditingValue = () => {
|
||||||
const { devicevalue } = this.state;
|
const { devicevalue } = this.state;
|
||||||
|
|
||||||
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
|
redirectingAuthorizedFetch(WRITE_VALUE_ENDPOINT, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: JSON.stringify({ devicevalue: devicevalue }),
|
body: JSON.stringify({ devicevalue: devicevalue }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
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) {
|
} 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) {
|
} else if (response.status === 403) {
|
||||||
this.props.enqueueSnackbar("Write access denied", { variant: "error" });
|
this.props.enqueueSnackbar('Write access denied', {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw Error("Unexpected response code: " + response.status);
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(
|
this.props.enqueueSnackbar(error.message || 'Problem writing value', {
|
||||||
error.message || "Problem writing value", { variant: "error" }
|
variant: 'error'
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (devicevalue) {
|
if (devicevalue) {
|
||||||
@@ -128,20 +170,19 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
devicevalue: undefined
|
devicevalue: undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sendCommand = (i: any) => {
|
sendCommand = (i: number) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
devicevalue: {
|
devicevalue: {
|
||||||
id: this.state.selectedDevice!,
|
id: this.state.selectedDevice!,
|
||||||
data: this.state.deviceData?.data[i]!,
|
data: this.state.deviceData?.data[i]!,
|
||||||
uom: this.state.deviceData?.data[i + 1]!,
|
uom: this.state.deviceData?.data[i + 1]!,
|
||||||
name: this.state.deviceData?.data[i + 2]!,
|
name: this.state.deviceData?.data[i + 2]!,
|
||||||
cmd: this.state.deviceData?.data[i + 3]!,
|
cmd: this.state.deviceData?.data[i + 3]!
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
noDevices = () => {
|
noDevices = () => {
|
||||||
return this.props.data.devices.length === 0;
|
return this.props.data.devices.length === 0;
|
||||||
@@ -166,22 +207,41 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
{!this.noDevices() && (
|
{!this.noDevices() && (
|
||||||
<Table
|
<Table
|
||||||
size="small"
|
size="small"
|
||||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.devices.sort(compareDevices).map((device) => (
|
{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>
|
<TableCell>
|
||||||
<CustomTooltip
|
<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"
|
placement="right-end"
|
||||||
>
|
>
|
||||||
<Button startIcon={<ListIcon />} size="small" variant="outlined">
|
<Button
|
||||||
|
startIcon={<ListIcon />}
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
{device.type}
|
{device.type}
|
||||||
</Button>
|
</Button>
|
||||||
</CustomTooltip>
|
</CustomTooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">{device.brand + " " + device.name} </TableCell>
|
<TableCell align="right">
|
||||||
|
{device.brand + ' ' + device.name}{' '}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -191,10 +251,13 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
<Box
|
<Box
|
||||||
bgcolor="error.main"
|
bgcolor="error.main"
|
||||||
color="error.contrastText"
|
color="error.contrastText"
|
||||||
p={2} mt={2} mb={2}
|
p={2}
|
||||||
|
mt={2}
|
||||||
|
mb={2}
|
||||||
>
|
>
|
||||||
<Typography variant="body1">
|
<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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -255,14 +318,25 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
>
|
>
|
||||||
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
<DialogTitle>Confirm Scan Devices</DialogTitle>
|
||||||
<DialogContent dividers>
|
<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>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={this.onScanDevicesRejected} color="secondary">
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.onScanDevicesRejected}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<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
|
Start Scan
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -283,17 +357,17 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
redirectingAuthorizedFetch(SCANDEVICES_ENDPOINT)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.props.enqueueSnackbar("Device scan is starting...", {
|
this.props.enqueueSnackbar('Device scan is starting...', {
|
||||||
variant: "info",
|
variant: 'info'
|
||||||
});
|
});
|
||||||
this.setState({ processing: false, confirmScanDevices: false });
|
this.setState({ processing: false, confirmScanDevices: false });
|
||||||
} else {
|
} else {
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error('Invalid status code: ' + response.status);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message || "Problem with scan", {
|
this.props.enqueueSnackbar(error.message || 'Problem with scan', {
|
||||||
variant: "error",
|
variant: 'error'
|
||||||
});
|
});
|
||||||
this.setState({ processing: false, confirmScanDevices: false });
|
this.setState({ processing: false, confirmScanDevices: false });
|
||||||
});
|
});
|
||||||
@@ -302,25 +376,25 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
handleRowClick = (device: any) => {
|
handleRowClick = (device: any) => {
|
||||||
this.setState({ selectedDevice: device.id, deviceData: undefined });
|
this.setState({ selectedDevice: device.id, deviceData: undefined });
|
||||||
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
redirectingAuthorizedFetch(DEVICE_DATA_ENDPOINT, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: JSON.stringify({ id: device.id }),
|
body: JSON.stringify({ id: device.id }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
throw Error("Unexpected response code: " + response.status);
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
})
|
})
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
this.setState({ deviceData: json });
|
this.setState({ deviceData: json });
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(
|
this.props.enqueueSnackbar(
|
||||||
error.message || "Problem getting device data",
|
error.message || 'Problem getting device data',
|
||||||
{ variant: "error" }
|
{ variant: 'error' }
|
||||||
);
|
);
|
||||||
this.setState({ deviceData: undefined });
|
this.setState({ deviceData: undefined });
|
||||||
});
|
});
|
||||||
@@ -351,10 +425,9 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table
|
<Table
|
||||||
size="small"
|
size="small"
|
||||||
padding={isWidthDown("xs", width!) ? "none" : "default"}
|
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||||
>
|
>
|
||||||
<TableHead>
|
<TableHead></TableHead>
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{deviceData.data.map((item, i) => {
|
{deviceData.data.map((item, i) => {
|
||||||
if (i % 4) {
|
if (i % 4) {
|
||||||
@@ -362,19 +435,30 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<TableRow hover key={i}>
|
<TableRow hover key={i}>
|
||||||
<TableCell padding="checkbox" style={{ width: 18 }} >
|
<TableCell padding="checkbox" style={{ width: 18 }}>
|
||||||
{deviceData.data[i + 3] && me.admin && (
|
{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" />
|
<EditIcon color="primary" fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</CustomTooltip>
|
</CustomTooltip>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell padding="none" component="th" scope="row">{deviceData.data[i + 2]}</TableCell>
|
<TableCell padding="none" component="th" scope="row">
|
||||||
<TableCell padding="none" align="right">{deviceData.data[i]}{formatUnit(deviceData.data[i + 1])}</TableCell>
|
{deviceData.data[i + 2]}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell padding="none" align="right">
|
||||||
|
{deviceData.data[i]}
|
||||||
|
{formatUnit(deviceData.data[i + 1])}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -390,7 +474,7 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Fragment >
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,26 +489,34 @@ class EMSESPDevicesForm extends Component<EMSESPDevicesFormProps, EMSESPDevicesF
|
|||||||
<br></br>
|
<br></br>
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1} padding={1}>
|
<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
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="none" padding={1} whiteSpace="nowrap">
|
<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
|
Scan Devices
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{this.renderScanDevicesDialog()}
|
{this.renderScanDevicesDialog()}
|
||||||
{
|
{devicevalue && (
|
||||||
devicevalue &&
|
|
||||||
<ValueForm
|
<ValueForm
|
||||||
devicevalue={devicevalue}
|
devicevalue={devicevalue}
|
||||||
onDoneEditing={this.doneEditingValue}
|
onDoneEditing={this.doneEditingValue}
|
||||||
onCancelEditing={this.cancelEditingValue}
|
onCancelEditing={this.cancelEditingValue}
|
||||||
handleValueChange={this.handleValueChange}
|
handleValueChange={this.handleValueChange}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,40 @@
|
|||||||
import React, { Component } from 'react';
|
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 { SectionContent } from '../components';
|
||||||
|
|
||||||
import CommentIcon from "@material-ui/icons/CommentTwoTone";
|
import CommentIcon from '@material-ui/icons/CommentTwoTone';
|
||||||
import MenuBookIcon from "@material-ui/icons/MenuBookTwoTone";
|
import MenuBookIcon from '@material-ui/icons/MenuBookTwoTone';
|
||||||
import GitHubIcon from "@material-ui/icons/GitHub";
|
import GitHubIcon from '@material-ui/icons/GitHub';
|
||||||
import StarIcon from "@material-ui/icons/Star";
|
import StarIcon from '@material-ui/icons/Star';
|
||||||
import ImportExportIcon from "@material-ui/icons/ImportExport";
|
import ImportExportIcon from '@material-ui/icons/ImportExport';
|
||||||
import BugReportIcon from "@material-ui/icons/BugReportTwoTone";
|
import BugReportIcon from '@material-ui/icons/BugReportTwoTone';
|
||||||
|
|
||||||
export const WebAPISystemSettings = window.location.origin + "/api/system/settings";
|
export const WebAPISystemSettings =
|
||||||
export const WebAPISystemInfo = window.location.origin + "/api/system/info";
|
window.location.origin + '/api/system/settings';
|
||||||
|
export const WebAPISystemInfo = window.location.origin + '/api/system/info';
|
||||||
|
|
||||||
class EMSESPHelp extends Component {
|
class EMSESPHelp extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<SectionContent title='EMS-ESP Help' titleGutter>
|
<SectionContent title="EMS-ESP Help" titleGutter>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<MenuBookIcon />
|
<MenuBookIcon />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
For the latest news and updates go to the <Link href="https://emsesp.github.io/docs" color="primary">{'official documentation'} website</Link>
|
For the latest news and updates go to the{' '}
|
||||||
|
<Link href="https://emsesp.github.io/docs" color="primary">
|
||||||
|
{'official documentation'} website
|
||||||
|
</Link>
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
@@ -34,7 +43,10 @@ class EMSESPHelp extends Component {
|
|||||||
<CommentIcon />
|
<CommentIcon />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
For live community chat join our <Link href="https://discord.gg/3J3GgnzpyT" color="primary">{'Discord'} server</Link>
|
For live community chat join our{' '}
|
||||||
|
<Link href="https://discord.gg/3J3GgnzpyT" color="primary">
|
||||||
|
{'Discord'} server
|
||||||
|
</Link>
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
@@ -43,7 +55,13 @@ class EMSESPHelp extends Component {
|
|||||||
<GitHubIcon />
|
<GitHubIcon />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText>
|
<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>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
@@ -52,34 +70,41 @@ class EMSESPHelp extends Component {
|
|||||||
<ImportExportIcon />
|
<ImportExportIcon />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText>
|
<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>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<BugReportIcon />
|
<BugReportIcon />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText>
|
<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>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
|
<Box bgcolor="info.main" border={1} p={3} mt={1} mb={0}>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
EMS-ESP is free and open-source.
|
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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<br></br>
|
<br></br>
|
||||||
|
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EMSESPHelp;
|
export default EMSESPHelp;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
@@ -10,26 +10,31 @@ import { AuthenticatedRoute } from '../authentication';
|
|||||||
import EMSESPSettingsController from './EMSESPSettingsController';
|
import EMSESPSettingsController from './EMSESPSettingsController';
|
||||||
|
|
||||||
class EMSESP extends Component<RouteComponentProps> {
|
class EMSESP extends Component<RouteComponentProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Settings">
|
<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" />
|
<Tab value={`/${PROJECT_PATH}/settings`} label="EMS-ESP Settings" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path={`/${PROJECT_PATH}/settings`} component={EMSESPSettingsController} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path={`/${PROJECT_PATH}/settings`}
|
||||||
|
component={EMSESPSettingsController}
|
||||||
|
/>
|
||||||
<Redirect to={`/${PROJECT_PATH}/settings`} />
|
<Redirect to={`/${PROJECT_PATH}/settings`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EMSESP;
|
export default EMSESP;
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
// import { Container } from '@material-ui/core';
|
|
||||||
|
|
||||||
import { ENDPOINT_ROOT } from '../api';
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
import EMSESPSettingsForm from './EMSESPSettingsForm';
|
import EMSESPSettingsForm from './EMSESPSettingsForm';
|
||||||
|
|
||||||
import { restController, RestControllerProps, RestFormLoader, SectionContent } from '../components';
|
import {
|
||||||
|
restController,
|
||||||
|
RestControllerProps,
|
||||||
|
RestFormLoader,
|
||||||
|
SectionContent
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import { EMSESPSettings } from './EMSESPtypes';
|
import { EMSESPSettings } from './EMSESPtypes';
|
||||||
|
|
||||||
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
export const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + 'emsespSettings';
|
||||||
|
|
||||||
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
type EMSESPSettingsControllerProps = RestControllerProps<EMSESPSettings>;
|
||||||
|
|
||||||
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -21,18 +24,18 @@ class EMSESPSettingsController extends Component<EMSESPSettingsControllerProps>
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
// <Container maxWidth="md" disableGutters>
|
// <Container maxWidth="md" disableGutters>
|
||||||
<SectionContent title='' titleGutter>
|
<SectionContent title="" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => (
|
render={(formProps) => <EMSESPSettingsForm {...formProps} />}
|
||||||
<EMSESPSettingsForm {...formProps} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
// </Container>
|
// </Container>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(EMSESP_SETTINGS_ENDPOINT, EMSESPSettingsController);
|
export default restController(
|
||||||
|
EMSESP_SETTINGS_ENDPOINT,
|
||||||
|
EMSESPSettingsController
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
ValidatorForm,
|
ValidatorForm,
|
||||||
TextValidator,
|
TextValidator,
|
||||||
SelectValidator,
|
SelectValidator
|
||||||
} from "react-material-ui-form-validator";
|
} from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -11,34 +11,34 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Link,
|
Link,
|
||||||
withWidth,
|
withWidth,
|
||||||
WithWidthProps,
|
WithWidthProps
|
||||||
} from "@material-ui/core";
|
} from '@material-ui/core';
|
||||||
import SaveIcon from "@material-ui/icons/Save";
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
import MenuItem from "@material-ui/core/MenuItem";
|
import MenuItem from '@material-ui/core/MenuItem';
|
||||||
|
|
||||||
import Grid from "@material-ui/core/Grid";
|
import Grid from '@material-ui/core/Grid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
redirectingAuthorizedFetch,
|
redirectingAuthorizedFetch,
|
||||||
withAuthenticatedContext,
|
withAuthenticatedContext,
|
||||||
AuthenticatedContextProps,
|
AuthenticatedContextProps
|
||||||
} from "../authentication";
|
} from '../authentication';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RestFormProps,
|
RestFormProps,
|
||||||
FormActions,
|
FormActions,
|
||||||
FormButton,
|
FormButton,
|
||||||
BlockFormControlLabel,
|
BlockFormControlLabel
|
||||||
} from "../components";
|
} 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";
|
import { ENDPOINT_ROOT } from '../api';
|
||||||
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
|
export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + 'boardProfile';
|
||||||
|
|
||||||
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
|
type EMSESPSettingsFormProps = RestFormProps<EMSESPSettings> &
|
||||||
AuthenticatedContextProps &
|
AuthenticatedContextProps &
|
||||||
@@ -50,38 +50,38 @@ interface EMSESPSettingsFormState {
|
|||||||
|
|
||||||
class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||||
state: EMSESPSettingsFormState = {
|
state: EMSESPSettingsFormState = {
|
||||||
processing: false,
|
processing: false
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule("isOptionalIP", optional(isIP));
|
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
|
||||||
}
|
}
|
||||||
|
|
||||||
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
changeBoardProfile = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const { data, setData } = this.props;
|
const { data, setData } = this.props;
|
||||||
setData({
|
setData({
|
||||||
...data,
|
...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 });
|
this.setState({ processing: true });
|
||||||
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
|
redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
body: JSON.stringify({ code: event.target.value }),
|
body: JSON.stringify({ code: event.target.value }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
throw Error("Unexpected response code: " + response.status);
|
throw Error('Unexpected response code: ' + response.status);
|
||||||
})
|
})
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
this.props.enqueueSnackbar("Profile loaded", { variant: "success" });
|
this.props.enqueueSnackbar('Profile loaded', { variant: 'success' });
|
||||||
setData({
|
setData({
|
||||||
...data,
|
...data,
|
||||||
led_gpio: json.led_gpio,
|
led_gpio: json.led_gpio,
|
||||||
@@ -89,14 +89,14 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
rx_gpio: json.rx_gpio,
|
rx_gpio: json.rx_gpio,
|
||||||
tx_gpio: json.tx_gpio,
|
tx_gpio: json.tx_gpio,
|
||||||
pbutton_gpio: json.pbutton_gpio,
|
pbutton_gpio: json.pbutton_gpio,
|
||||||
board_profile: event.target.value,
|
board_profile: event.target.value
|
||||||
});
|
});
|
||||||
this.setState({ processing: false });
|
this.setState({ processing: false });
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(
|
this.props.enqueueSnackbar(
|
||||||
error.message || "Problem fetching board profile",
|
error.message || 'Problem fetching board profile',
|
||||||
{ variant: "warning" }
|
{ variant: 'warning' }
|
||||||
);
|
);
|
||||||
this.setState({ processing: false });
|
this.setState({ processing: false });
|
||||||
});
|
});
|
||||||
@@ -108,13 +108,13 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
<ValidatorForm onSubmit={saveData}>
|
<ValidatorForm onSubmit={saveData}>
|
||||||
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
|
<Box bgcolor="info.main" p={2} mt={2} mb={2}>
|
||||||
<Typography variant="body1">
|
<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
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
|
href="https://emsesp.github.io/docs/#/Configure-firmware32?id=ems-esp-settings"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{"online documentation"}
|
{'online documentation'}
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -139,7 +139,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
value={data.tx_mode}
|
value={data.tx_mode}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("tx_mode")}
|
onChange={handleValueChange('tx_mode')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={0}>Off</MenuItem>
|
<MenuItem value={0}>Off</MenuItem>
|
||||||
@@ -156,7 +156,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
value={data.ems_bus_id}
|
value={data.ems_bus_id}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("ems_bus_id")}
|
onChange={handleValueChange('ems_bus_id')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
<MenuItem value={0x0b}>Service Key (0x0B)</MenuItem>
|
||||||
@@ -169,16 +169,16 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:120",
|
'maxNumber:120'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Tx delay is required",
|
'Tx delay is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or higher",
|
'Must be 0 or higher',
|
||||||
"Max value is 120",
|
'Max value is 120'
|
||||||
]}
|
]}
|
||||||
name="tx_delay"
|
name="tx_delay"
|
||||||
label="Tx start delay (seconds)"
|
label="Tx start delay (seconds)"
|
||||||
@@ -186,7 +186,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.tx_delay}
|
value={data.tx_delay}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("tx_delay")}
|
onChange={handleValueChange('tx_delay')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -216,12 +216,12 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
{boardProfileSelectItems()}
|
{boardProfileSelectItems()}
|
||||||
<MenuItem key={"CUSTOM"} value={"CUSTOM"}>
|
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||||
Custom...
|
Custom...
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</SelectValidator>
|
</SelectValidator>
|
||||||
|
|
||||||
{data.board_profile === "CUSTOM" && (
|
{data.board_profile === 'CUSTOM' && (
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={1}
|
spacing={1}
|
||||||
@@ -232,18 +232,18 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:40",
|
'maxNumber:40',
|
||||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"GPIO is required",
|
'GPIO is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or higher",
|
'Must be 0 or higher',
|
||||||
"Max value is 40",
|
'Max value is 40',
|
||||||
"Not a valid GPIO",
|
'Not a valid GPIO'
|
||||||
]}
|
]}
|
||||||
name="rx_gpio"
|
name="rx_gpio"
|
||||||
label="Rx GPIO"
|
label="Rx GPIO"
|
||||||
@@ -251,25 +251,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.rx_gpio}
|
value={data.rx_gpio}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("rx_gpio")}
|
onChange={handleValueChange('rx_gpio')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:40",
|
'maxNumber:40',
|
||||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"GPIO is required",
|
'GPIO is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or higher",
|
'Must be 0 or higher',
|
||||||
"Max value is 40",
|
'Max value is 40',
|
||||||
"Not a valid GPIO",
|
'Not a valid GPIO'
|
||||||
]}
|
]}
|
||||||
name="tx_gpio"
|
name="tx_gpio"
|
||||||
label="Tx GPIO"
|
label="Tx GPIO"
|
||||||
@@ -277,25 +277,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.tx_gpio}
|
value={data.tx_gpio}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("tx_gpio")}
|
onChange={handleValueChange('tx_gpio')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:40",
|
'maxNumber:40',
|
||||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"GPIO is required",
|
'GPIO is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or higher",
|
'Must be 0 or higher',
|
||||||
"Max value is 40",
|
'Max value is 40',
|
||||||
"Not a valid GPIO",
|
'Not a valid GPIO'
|
||||||
]}
|
]}
|
||||||
name="pbutton_gpio"
|
name="pbutton_gpio"
|
||||||
label="Button GPIO"
|
label="Button GPIO"
|
||||||
@@ -303,25 +303,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.pbutton_gpio}
|
value={data.pbutton_gpio}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("pbutton_gpio")}
|
onChange={handleValueChange('pbutton_gpio')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:40",
|
'maxNumber:40',
|
||||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"GPIO is required",
|
'GPIO is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or higher",
|
'Must be 0 or higher',
|
||||||
"Max value is 40",
|
'Max value is 40',
|
||||||
"Not a valid GPIO",
|
'Not a valid GPIO'
|
||||||
]}
|
]}
|
||||||
name="dallas_gpio"
|
name="dallas_gpio"
|
||||||
label="Dallas GPIO (0=none)"
|
label="Dallas GPIO (0=none)"
|
||||||
@@ -329,25 +329,25 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.dallas_gpio}
|
value={data.dallas_gpio}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("dallas_gpio")}
|
onChange={handleValueChange('dallas_gpio')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={4}>
|
<Grid item xs={4}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:40",
|
'maxNumber:40',
|
||||||
"matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$",
|
'matchRegexp:^((?!6|7|8|9|10|11|12|14|15|20|24|28|29|30|31)[0-9]*)$'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"GPIO is required",
|
'GPIO is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or higher",
|
'Must be 0 or higher',
|
||||||
"Max value is 40",
|
'Max value is 40',
|
||||||
"Not a valid GPIO",
|
'Not a valid GPIO'
|
||||||
]}
|
]}
|
||||||
name="led_gpio"
|
name="led_gpio"
|
||||||
label="LED GPIO (0=none)"
|
label="LED GPIO (0=none)"
|
||||||
@@ -355,7 +355,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.led_gpio}
|
value={data.led_gpio}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("led_gpio")}
|
onChange={handleValueChange('led_gpio')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -372,7 +372,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.hide_led}
|
checked={data.hide_led}
|
||||||
onChange={handleValueChange("hide_led")}
|
onChange={handleValueChange('hide_led')}
|
||||||
value="hide_led"
|
value="hide_led"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -385,7 +385,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.dallas_parasite}
|
checked={data.dallas_parasite}
|
||||||
onChange={handleValueChange("dallas_parasite")}
|
onChange={handleValueChange('dallas_parasite')}
|
||||||
value="dallas_parasite"
|
value="dallas_parasite"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -397,7 +397,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.notoken_api}
|
checked={data.notoken_api}
|
||||||
onChange={handleValueChange("notoken_api")}
|
onChange={handleValueChange('notoken_api')}
|
||||||
value="notoken_api"
|
value="notoken_api"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -407,7 +407,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.analog_enabled}
|
checked={data.analog_enabled}
|
||||||
onChange={handleValueChange("analog_enabled")}
|
onChange={handleValueChange('analog_enabled')}
|
||||||
value="analog_enabled"
|
value="analog_enabled"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -424,7 +424,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.shower_timer}
|
checked={data.shower_timer}
|
||||||
onChange={handleValueChange("shower_timer")}
|
onChange={handleValueChange('shower_timer')}
|
||||||
value="shower_timer"
|
value="shower_timer"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -434,7 +434,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.shower_alert}
|
checked={data.shower_alert}
|
||||||
onChange={handleValueChange("shower_alert")}
|
onChange={handleValueChange('shower_alert')}
|
||||||
value="shower_alert"
|
value="shower_alert"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -451,7 +451,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.syslog_enabled}
|
checked={data.syslog_enabled}
|
||||||
onChange={handleValueChange("syslog_enabled")}
|
onChange={handleValueChange('syslog_enabled')}
|
||||||
value="syslog_enabled"
|
value="syslog_enabled"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -468,30 +468,30 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
>
|
>
|
||||||
<Grid item xs={5}>
|
<Grid item xs={5}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={["isOptionalIP"]}
|
validators={['isOptionalIP']}
|
||||||
errorMessages={["Not a valid IP address"]}
|
errorMessages={['Not a valid IP address']}
|
||||||
name="syslog_host"
|
name="syslog_host"
|
||||||
label="IP"
|
label="IP"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.syslog_host}
|
value={data.syslog_host}
|
||||||
onChange={handleValueChange("syslog_host")}
|
onChange={handleValueChange('syslog_host')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Port is required",
|
'Port is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be greater than 0 ",
|
'Must be greater than 0 ',
|
||||||
"Max value is 65535",
|
'Max value is 65535'
|
||||||
]}
|
]}
|
||||||
name="syslog_port"
|
name="syslog_port"
|
||||||
label="Port"
|
label="Port"
|
||||||
@@ -499,7 +499,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.syslog_port}
|
value={data.syslog_port}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("syslog_port")}
|
onChange={handleValueChange('syslog_port')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -510,7 +510,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
value={data.syslog_level}
|
value={data.syslog_level}
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleValueChange("syslog_level")}
|
onChange={handleValueChange('syslog_level')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
>
|
>
|
||||||
<MenuItem value={-1}>OFF</MenuItem>
|
<MenuItem value={-1}>OFF</MenuItem>
|
||||||
@@ -524,16 +524,16 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={[
|
validators={[
|
||||||
"required",
|
'required',
|
||||||
"isNumber",
|
'isNumber',
|
||||||
"minNumber:0",
|
'minNumber:0',
|
||||||
"maxNumber:65535",
|
'maxNumber:65535'
|
||||||
]}
|
]}
|
||||||
errorMessages={[
|
errorMessages={[
|
||||||
"Syslog Mark is required",
|
'Syslog Mark is required',
|
||||||
"Must be a number",
|
'Must be a number',
|
||||||
"Must be 0 or higher",
|
'Must be 0 or higher',
|
||||||
"Max value is 10",
|
'Max value is 10'
|
||||||
]}
|
]}
|
||||||
name="syslog_mark_interval"
|
name="syslog_mark_interval"
|
||||||
label="Mark Interval seconds (0=off)"
|
label="Mark Interval seconds (0=off)"
|
||||||
@@ -541,7 +541,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.syslog_mark_interval}
|
value={data.syslog_mark_interval}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={handleValueChange("syslog_mark_interval")}
|
onChange={handleValueChange('syslog_mark_interval')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -549,7 +549,7 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.trace_raw}
|
checked={data.trace_raw}
|
||||||
onChange={handleValueChange("trace_raw")}
|
onChange={handleValueChange('trace_raw')}
|
||||||
value="trace_raw"
|
value="trace_raw"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
import { Theme } from '@material-ui/core'
|
import { Theme } from '@material-ui/core';
|
||||||
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes'
|
import { EMSESPStatus, busConnectionStatus } from './EMSESPtypes';
|
||||||
|
|
||||||
export const isConnected = ({ status }: EMSESPStatus) =>
|
export const isConnected = ({ status }: EMSESPStatus) =>
|
||||||
status !== busConnectionStatus.BUS_STATUS_OFFLINE
|
status !== busConnectionStatus.BUS_STATUS_OFFLINE;
|
||||||
|
|
||||||
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
export const busStatusHighlight = ({ status }: EMSESPStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||||
return theme.palette.warning.main
|
return theme.palette.warning.main;
|
||||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||||
return theme.palette.success.main
|
return theme.palette.success.main;
|
||||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||||
return theme.palette.error.main
|
return theme.palette.error.main;
|
||||||
default:
|
default:
|
||||||
return theme.palette.warning.main
|
return theme.palette.warning.main;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const busStatus = ({ status }: EMSESPStatus) => {
|
export const busStatus = ({ status }: EMSESPStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
case busConnectionStatus.BUS_STATUS_CONNECTED:
|
||||||
return 'Connected'
|
return 'Connected';
|
||||||
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
|
||||||
return 'Tx Errors'
|
return 'Tx Errors';
|
||||||
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
case busConnectionStatus.BUS_STATUS_OFFLINE:
|
||||||
return 'Disconnected'
|
return 'Disconnected';
|
||||||
default:
|
default:
|
||||||
return 'Unknown'
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const qualityHighlight = (value: number, theme: Theme) => {
|
export const qualityHighlight = (value: number, theme: Theme) => {
|
||||||
if (value >= 95) {
|
if (value >= 95) {
|
||||||
return theme.palette.success.main
|
return theme.palette.success.main;
|
||||||
}
|
}
|
||||||
|
|
||||||
return theme.palette.error.main
|
return theme.palette.error.main;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import React, { Component } from 'react';
|
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 { ENDPOINT_ROOT } from '../api';
|
||||||
import EMSESPStatusForm from './EMSESPStatusForm';
|
import EMSESPStatusForm from './EMSESPStatusForm';
|
||||||
import { EMSESPStatus } from './EMSESPtypes';
|
import { EMSESPStatus } from './EMSESPtypes';
|
||||||
|
|
||||||
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + "emsespStatus";
|
export const EMSESP_STATUS_ENDPOINT = ENDPOINT_ROOT + 'emsespStatus';
|
||||||
|
|
||||||
type EMSESPStatusControllerProps = RestControllerProps<EMSESPStatus>;
|
type EMSESPStatusControllerProps = RestControllerProps<EMSESPStatus>;
|
||||||
|
|
||||||
class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
|
class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -20,10 +24,10 @@ class EMSESPStatusController extends Component<EMSESPStatusControllerProps> {
|
|||||||
<SectionContent title="EMS Status">
|
<SectionContent title="EMS Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <EMSESPStatusForm {...formProps} />}
|
render={(formProps) => <EMSESPStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
TableContainer,
|
TableContainer,
|
||||||
Table,
|
Table,
|
||||||
@@ -13,35 +13,32 @@ import {
|
|||||||
ListItemText,
|
ListItemText,
|
||||||
withWidth,
|
withWidth,
|
||||||
WithWidthProps,
|
WithWidthProps,
|
||||||
isWidthDown,
|
isWidthDown
|
||||||
} from "@material-ui/core";
|
} from '@material-ui/core';
|
||||||
|
|
||||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
|
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RestFormProps,
|
RestFormProps,
|
||||||
FormActions,
|
FormActions,
|
||||||
FormButton,
|
FormButton,
|
||||||
HighlightAvatar,
|
HighlightAvatar
|
||||||
} from "../components";
|
} from '../components';
|
||||||
|
|
||||||
import {
|
import { busStatus, busStatusHighlight, isConnected } from './EMSESPStatus';
|
||||||
busStatus,
|
|
||||||
busStatusHighlight,
|
|
||||||
isConnected,
|
|
||||||
} from "./EMSESPStatus";
|
|
||||||
|
|
||||||
import { EMSESPStatus } from "./EMSESPtypes";
|
import { EMSESPStatus } from './EMSESPtypes';
|
||||||
|
|
||||||
function formatNumber(num: number) {
|
function formatNumber(num: number) {
|
||||||
return new Intl.NumberFormat().format(num);
|
return new Intl.NumberFormat().format(num);
|
||||||
}
|
}
|
||||||
|
|
||||||
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> & WithTheme & WithWidthProps;
|
type EMSESPStatusFormProps = RestFormProps<EMSESPStatus> &
|
||||||
|
WithTheme &
|
||||||
|
WithWidthProps;
|
||||||
|
|
||||||
class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
||||||
|
|
||||||
createListItems() {
|
createListItems() {
|
||||||
const { data, theme, width } = this.props;
|
const { data, theme, width } = this.props;
|
||||||
return (
|
return (
|
||||||
@@ -52,24 +49,30 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
|||||||
<DeviceHubIcon />
|
<DeviceHubIcon />
|
||||||
</HighlightAvatar>
|
</HighlightAvatar>
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Connection Status" secondary={busStatus(data)} />
|
<ListItemText
|
||||||
|
primary="Connection Status"
|
||||||
|
secondary={busStatus(data)}
|
||||||
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{isConnected(data) && (
|
{isConnected(data) && (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
|
<Table
|
||||||
|
size="small"
|
||||||
|
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||||
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell># Telegrams Received</TableCell>
|
||||||
# Telegrams Received
|
<TableCell align="right">
|
||||||
</TableCell>
|
{formatNumber(data.rx_received)} (quality{' '}
|
||||||
<TableCell align="right">{formatNumber(data.rx_received)} (quality {data.rx_quality}%)
|
{data.rx_quality}%)
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell >
|
<TableCell># Telegrams Sent</TableCell>
|
||||||
# Telegrams Sent
|
<TableCell align="right">
|
||||||
</TableCell >
|
{formatNumber(data.tx_sent)} (quality {data.tx_quality}
|
||||||
<TableCell align="right">{formatNumber(data.tx_sent)} (quality {data.tx_quality}%)
|
%)
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -86,7 +89,11 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
|
|||||||
<List>{this.createListItems()}</List>
|
<List>{this.createListItems()}</List>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton
|
<FormButton
|
||||||
startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={this.props.loadData}>
|
startIcon={<RefreshIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.props.loadData}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,72 +1,72 @@
|
|||||||
export interface EMSESPSettings {
|
export interface EMSESPSettings {
|
||||||
tx_mode: number
|
tx_mode: number;
|
||||||
tx_delay: number
|
tx_delay: number;
|
||||||
ems_bus_id: number
|
ems_bus_id: number;
|
||||||
syslog_enabled: boolean
|
syslog_enabled: boolean;
|
||||||
syslog_level: number
|
syslog_level: number;
|
||||||
syslog_mark_interval: number
|
syslog_mark_interval: number;
|
||||||
syslog_host: string
|
syslog_host: string;
|
||||||
syslog_port: number
|
syslog_port: number;
|
||||||
master_thermostat: number
|
master_thermostat: number;
|
||||||
shower_timer: boolean
|
shower_timer: boolean;
|
||||||
shower_alert: boolean
|
shower_alert: boolean;
|
||||||
rx_gpio: number
|
rx_gpio: number;
|
||||||
tx_gpio: number
|
tx_gpio: number;
|
||||||
dallas_gpio: number
|
dallas_gpio: number;
|
||||||
dallas_parasite: boolean
|
dallas_parasite: boolean;
|
||||||
led_gpio: number
|
led_gpio: number;
|
||||||
hide_led: boolean
|
hide_led: boolean;
|
||||||
notoken_api: boolean
|
notoken_api: boolean;
|
||||||
analog_enabled: boolean
|
analog_enabled: boolean;
|
||||||
pbutton_gpio: number
|
pbutton_gpio: number;
|
||||||
trace_raw: boolean
|
trace_raw: boolean;
|
||||||
board_profile: string
|
board_profile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum busConnectionStatus {
|
export enum busConnectionStatus {
|
||||||
BUS_STATUS_CONNECTED = 0,
|
BUS_STATUS_CONNECTED = 0,
|
||||||
BUS_STATUS_TX_ERRORS = 1,
|
BUS_STATUS_TX_ERRORS = 1,
|
||||||
BUS_STATUS_OFFLINE = 2,
|
BUS_STATUS_OFFLINE = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EMSESPStatus {
|
export interface EMSESPStatus {
|
||||||
status: busConnectionStatus
|
status: busConnectionStatus;
|
||||||
rx_received: number
|
rx_received: number;
|
||||||
tx_sent: number
|
tx_sent: number;
|
||||||
rx_quality: number
|
rx_quality: number;
|
||||||
tx_quality: number
|
tx_quality: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
id: number
|
id: number;
|
||||||
type: string
|
type: string;
|
||||||
brand: string
|
brand: string;
|
||||||
name: string
|
name: string;
|
||||||
deviceid: number
|
deviceid: number;
|
||||||
productid: number
|
productid: number;
|
||||||
version: string
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Sensor {
|
export interface Sensor {
|
||||||
no: number
|
no: number;
|
||||||
id: string
|
id: string;
|
||||||
temp: string
|
temp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EMSESPDevices {
|
export interface EMSESPDevices {
|
||||||
devices: Device[]
|
devices: Device[];
|
||||||
sensors: Sensor[]
|
sensors: Sensor[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EMSESPDeviceData {
|
export interface EMSESPDeviceData {
|
||||||
name: string
|
name: string;
|
||||||
data: string[]
|
data: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceValue {
|
export interface DeviceValue {
|
||||||
id: number
|
id: number;
|
||||||
data: string
|
data: string;
|
||||||
uom: string
|
uom: string;
|
||||||
name: string
|
name: string;
|
||||||
cmd: string
|
cmd: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, { Component } from "react";
|
import { Component } from 'react';
|
||||||
import { Link, withRouter, RouteComponentProps } from "react-router-dom";
|
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 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;
|
type ProjectProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
@@ -16,13 +19,28 @@ class ProjectMenu extends Component<ProjectProps> {
|
|||||||
const path = this.props.match.url;
|
const path = this.props.match.url;
|
||||||
return (
|
return (
|
||||||
<List>
|
<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>
|
<ListItemIcon>
|
||||||
<DashboardIcon />
|
<DashboardIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary="Dashboard" />
|
<ListItemText primary="Dashboard" />
|
||||||
</ListItem>
|
</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>
|
<ListItemIcon>
|
||||||
<TuneIcon />
|
<TuneIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch } from 'react-router';
|
import { Redirect, Switch } from 'react-router';
|
||||||
|
|
||||||
import { AuthenticatedRoute } from '../authentication';
|
import { AuthenticatedRoute } from '../authentication';
|
||||||
@@ -7,24 +7,32 @@ import EMSESPDashboard from './EMSESPDashboard';
|
|||||||
import EMSESPSettings from './EMSESPSettings';
|
import EMSESPSettings from './EMSESPSettings';
|
||||||
|
|
||||||
class ProjectRouting extends Component {
|
class ProjectRouting extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/ems-esp/status/*" component={EMSESPDashboard} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/ems-esp/settings" component={EMSESPSettings} />
|
exact
|
||||||
<AuthenticatedRoute exact path="/ems-esp/*" component={EMSESPDashboard} />
|
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 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.
|
* The "to" property must match one of the routes above for this to work correctly.
|
||||||
*/
|
*/}
|
||||||
}
|
|
||||||
<Redirect to={`/ems-esp/status`} />
|
<Redirect to={`/ems-esp/status`} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProjectRouting;
|
export default ProjectRouting;
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import React, { RefObject } from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
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 { FormButton } from '../components';
|
||||||
import { DeviceValue } from './EMSESPtypes';
|
import { DeviceValue } from './EMSESPtypes';
|
||||||
|
|
||||||
@@ -8,30 +15,43 @@ interface ValueFormProps {
|
|||||||
devicevalue: DeviceValue;
|
devicevalue: DeviceValue;
|
||||||
onDoneEditing: () => void;
|
onDoneEditing: () => void;
|
||||||
onCancelEditing: () => 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> {
|
class ValueForm extends React.Component<ValueFormProps> {
|
||||||
|
|
||||||
formRef: RefObject<any> = React.createRef();
|
formRef: RefObject<any> = React.createRef();
|
||||||
|
|
||||||
submit = () => {
|
submit = () => {
|
||||||
this.formRef.current.submit();
|
this.formRef.current.submit();
|
||||||
}
|
};
|
||||||
|
|
||||||
buildLabel = (devicevalue: DeviceValue) => {
|
buildLabel = (devicevalue: DeviceValue) => {
|
||||||
if ((devicevalue.uom === "") || (!devicevalue.uom)) {
|
if (devicevalue.uom === '' || !devicevalue.uom) {
|
||||||
return "New value";
|
return 'New value';
|
||||||
}
|
|
||||||
return "New value (" + devicevalue.uom + ")";
|
|
||||||
}
|
}
|
||||||
|
return 'New value (' + devicevalue.uom + ')';
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { devicevalue, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
|
const {
|
||||||
|
devicevalue,
|
||||||
|
handleValueChange,
|
||||||
|
onDoneEditing,
|
||||||
|
onCancelEditing
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||||
<Dialog maxWidth="xs" onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open>
|
<Dialog
|
||||||
<DialogTitle id="user-form-dialog-title">Change the {devicevalue.name}</DialogTitle>
|
maxWidth="xs"
|
||||||
|
onClose={onCancelEditing}
|
||||||
|
aria-labelledby="user-form-dialog-title"
|
||||||
|
open
|
||||||
|
>
|
||||||
|
<DialogTitle id="user-form-dialog-title">
|
||||||
|
Change the {devicevalue.name}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required']}
|
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}>
|
<Box color="warning.main" p={1} pl={0} pr={0} mt={0} mb={0}>
|
||||||
<Typography variant="body2">
|
<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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>Cancel</FormButton>
|
<FormButton
|
||||||
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>Done</FormButton>
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onCancelEditing}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</FormButton>
|
||||||
|
<FormButton
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
onClick={this.submit}
|
||||||
|
>
|
||||||
|
Done
|
||||||
|
</FormButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</ValidatorForm>
|
</ValidatorForm>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import React, { Fragment } from 'react';
|
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 { FormButton } from '../components';
|
||||||
import { redirectingAuthorizedFetch } from '../authentication';
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
@@ -15,25 +24,34 @@ interface GenerateTokenState {
|
|||||||
token?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenState> {
|
class GenerateToken extends React.Component<
|
||||||
|
GenerateTokenProps,
|
||||||
|
GenerateTokenState
|
||||||
|
> {
|
||||||
state: GenerateTokenState = {};
|
state: GenerateTokenState = {};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { username } = this.props;
|
const { username } = this.props;
|
||||||
redirectingAuthorizedFetch(GENERATE_TOKEN_ENDPOINT + "?" + new URLSearchParams({ username }), { method: 'GET' })
|
redirectingAuthorizedFetch(
|
||||||
.then(response => {
|
GENERATE_TOKEN_ENDPOINT + '?' + new URLSearchParams({ username }),
|
||||||
|
{ method: 'GET' }
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else {
|
} else {
|
||||||
throw Error("Error generating token: " + response.status);
|
throw Error('Error generating token: ' + response.status);
|
||||||
}
|
}
|
||||||
}).then(generatedToken => {
|
})
|
||||||
|
.then((generatedToken) => {
|
||||||
console.log(generatedToken);
|
console.log(generatedToken);
|
||||||
this.setState({ token: generatedToken.token });
|
this.setState({ token: generatedToken.token });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
this.props.enqueueSnackbar(error.message || "Problem generating token", { variant: '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 { onClose, username } = this.props;
|
||||||
const { token } = this.state;
|
const { token } = this.state;
|
||||||
return (
|
return (
|
||||||
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open fullWidth maxWidth="sm">
|
<Dialog
|
||||||
<DialogTitle id="generate-token-dialog-title">Token for: {username}</DialogTitle>
|
onClose={onClose}
|
||||||
|
aria-labelledby="generate-token-dialog-title"
|
||||||
|
open
|
||||||
|
fullWidth
|
||||||
|
maxWidth="sm"
|
||||||
|
>
|
||||||
|
<DialogTitle id="generate-token-dialog-title">
|
||||||
|
Token for: {username}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
{token ?
|
{token ? (
|
||||||
<Fragment>
|
<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">
|
<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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box mt={2} mb={2}>
|
<Box mt={2} mb={2}>
|
||||||
<TextField label="Token" multiline value={token} fullWidth contentEditable={false} />
|
<TextField
|
||||||
|
label="Token"
|
||||||
|
multiline
|
||||||
|
value={token}
|
||||||
|
fullWidth
|
||||||
|
contentEditable={false}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
:
|
) : (
|
||||||
<Box m={4} textAlign="center">
|
<Box m={4} textAlign="center">
|
||||||
<LinearProgress />
|
<LinearProgress />
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">Generating token…</Typography>
|
||||||
Generating token…
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
|
<FormButton
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
Close
|
Close
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -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 { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import ManageUsersForm from './ManageUsersForm';
|
import ManageUsersForm from './ManageUsersForm';
|
||||||
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
|
|||||||
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
|
type ManageUsersControllerProps = RestControllerProps<SecuritySettings>;
|
||||||
|
|
||||||
class ManageUsersController extends Component<ManageUsersControllerProps> {
|
class ManageUsersController extends Component<ManageUsersControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,14 @@ class ManageUsersController extends Component<ManageUsersControllerProps> {
|
|||||||
<SectionContent title="Manage Users" titleGutter>
|
<SectionContent title="Manage Users" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <ManageUsersForm {...formProps} />}
|
render={(formProps) => <ManageUsersForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(SECURITY_SETTINGS_ENDPOINT, ManageUsersController);
|
export default restController(
|
||||||
|
SECURITY_SETTINGS_ENDPOINT,
|
||||||
|
ManageUsersController
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { ValidatorForm } from 'react-material-ui-form-validator';
|
import { ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableFooter, TableRow, withWidth, WithWidthProps, isWidthDown } from '@material-ui/core';
|
import {
|
||||||
import { Box, Button, Typography, } from '@material-ui/core';
|
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 EditIcon from '@material-ui/icons/Edit';
|
||||||
import DeleteIcon from '@material-ui/icons/Delete';
|
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 PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||||
import VpnKeyIcon from '@material-ui/icons/VpnKey';
|
import VpnKeyIcon from '@material-ui/icons/VpnKey';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import {
|
||||||
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
FormActions,
|
||||||
|
FormButton,
|
||||||
|
extractEventValue
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import UserForm from './UserForm';
|
import UserForm from './UserForm';
|
||||||
import { SecuritySettings, User } from './types';
|
import { SecuritySettings, User } from './types';
|
||||||
@@ -30,16 +48,20 @@ function compareUsers(a: User, b: User) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps & WithWidthProps;
|
type ManageUsersFormProps = RestFormProps<SecuritySettings> &
|
||||||
|
AuthenticatedContextProps &
|
||||||
|
WithWidthProps;
|
||||||
|
|
||||||
type ManageUsersFormState = {
|
type ManageUsersFormState = {
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
user?: User;
|
user?: User;
|
||||||
generateTokenFor?: string;
|
generateTokenFor?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
|
||||||
|
|
||||||
|
class ManageUsersForm extends React.Component<
|
||||||
|
ManageUsersFormProps,
|
||||||
|
ManageUsersFormState
|
||||||
|
> {
|
||||||
state: ManageUsersFormState = {
|
state: ManageUsersFormState = {
|
||||||
creating: false
|
creating: false
|
||||||
};
|
};
|
||||||
@@ -48,38 +70,38 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
this.setState({
|
this.setState({
|
||||||
creating: true,
|
creating: true,
|
||||||
user: {
|
user: {
|
||||||
username: "",
|
username: '',
|
||||||
password: "",
|
password: '',
|
||||||
admin: true
|
admin: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
uniqueUsername = (username: string) => {
|
uniqueUsername = (username: string) => {
|
||||||
return !this.props.data.users.find(u => u.username === username);
|
return !this.props.data.users.find((u) => u.username === username);
|
||||||
}
|
};
|
||||||
|
|
||||||
noAdminConfigured = () => {
|
noAdminConfigured = () => {
|
||||||
return !this.props.data.users.find(u => u.admin);
|
return !this.props.data.users.find((u) => u.admin);
|
||||||
}
|
};
|
||||||
|
|
||||||
removeUser = (user: User) => {
|
removeUser = (user: User) => {
|
||||||
const { data } = this.props;
|
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 });
|
this.props.setData({ ...data, users });
|
||||||
}
|
};
|
||||||
|
|
||||||
closeGenerateToken = () => {
|
closeGenerateToken = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
generateTokenFor: undefined
|
generateTokenFor: undefined
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
generateToken = (user: User) => {
|
generateToken = (user: User) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
generateTokenFor: user.username
|
generateTokenFor: user.username
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
startEditingUser = (user: User) => {
|
startEditingUser = (user: User) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -92,13 +114,13 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
this.setState({
|
this.setState({
|
||||||
user: undefined
|
user: undefined
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
doneEditingUser = () => {
|
doneEditingUser = () => {
|
||||||
const { user } = this.state;
|
const { user } = this.state;
|
||||||
if (user) {
|
if (user) {
|
||||||
const { data } = this.props;
|
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);
|
users.push(user);
|
||||||
this.props.setData({ ...data, users });
|
this.props.setData({ ...data, users });
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -107,14 +129,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleUserValueChange = (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
handleUserValueChange = (name: keyof User) => (
|
||||||
this.setState({ user: { ...this.state.user!, [name]: extractEventValue(event) } });
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
this.setState({
|
||||||
|
user: { ...this.state.user!, [name]: extractEventValue(event) }
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
this.props.saveData();
|
this.props.saveData();
|
||||||
this.props.authenticatedContext.refresh();
|
this.props.authenticatedContext.refresh();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { width, data } = this.props;
|
const { width, data } = this.props;
|
||||||
@@ -122,7 +148,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ValidatorForm onSubmit={this.onSubmit}>
|
<ValidatorForm onSubmit={this.onSubmit}>
|
||||||
<Table size="small" padding={isWidthDown('xs', width!) ? "none" : "default"}>
|
<Table
|
||||||
|
size="small"
|
||||||
|
padding={isWidthDown('xs', width!) ? 'none' : 'default'}
|
||||||
|
>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Username</TableCell>
|
<TableCell>Username</TableCell>
|
||||||
@@ -131,7 +160,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.users.sort(compareUsers).map(user => (
|
{data.users.sort(compareUsers).map((user) => (
|
||||||
<TableRow key={user.username}>
|
<TableRow key={user.username}>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell component="th" scope="row">
|
||||||
{user.username}
|
{user.username}
|
||||||
@@ -140,51 +169,79 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
{user.admin ? <CheckIcon /> : <CloseIcon />}
|
{user.admin ? <CheckIcon /> : <CloseIcon />}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<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 />
|
<VpnKeyIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label="Delete"
|
||||||
|
onClick={() => this.removeUser(user)}
|
||||||
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton size="small" aria-label="Edit" onClick={() => this.startEditingUser(user)}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label="Edit"
|
||||||
|
onClick={() => this.startEditingUser(user)}
|
||||||
|
>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<TableFooter >
|
<TableFooter>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={2} />
|
<TableCell colSpan={2} />
|
||||||
<TableCell align="center" padding="default">
|
<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
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableFooter>
|
</TableFooter>
|
||||||
</Table>
|
</Table>
|
||||||
{
|
{this.noAdminConfigured() && (
|
||||||
this.noAdminConfigured() &&
|
<Box
|
||||||
(
|
bgcolor="error.main"
|
||||||
<Box bgcolor="error.main" color="error.contrastText" p={2} mt={2} mb={2}>
|
color="error.contrastText"
|
||||||
|
p={2}
|
||||||
|
mt={2}
|
||||||
|
mb={2}
|
||||||
|
>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
You must have at least one admin user configured.
|
You must have at least one admin user configured.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
<FormActions>
|
<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
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</ValidatorForm>
|
</ValidatorForm>
|
||||||
{
|
{generateTokenFor && (
|
||||||
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
|
<GenerateToken
|
||||||
}
|
username={generateTokenFor}
|
||||||
{
|
onClose={this.closeGenerateToken}
|
||||||
user &&
|
/>
|
||||||
|
)}
|
||||||
|
{user && (
|
||||||
<UserForm
|
<UserForm
|
||||||
user={user}
|
user={user}
|
||||||
creating={creating}
|
creating={creating}
|
||||||
@@ -193,11 +250,10 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
handleValueChange={this.handleUserValueChange}
|
handleValueChange={this.handleUserValueChange}
|
||||||
uniqueUsername={this.uniqueUsername}
|
uniqueUsername={this.uniqueUsername}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticatedContext(withWidth()(ManageUsersForm));
|
export default withAuthenticatedContext(withWidth()(ManageUsersForm));
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import ManageUsersController from './ManageUsersController';
|
import ManageUsersController from './ManageUsersController';
|
||||||
@@ -12,25 +15,36 @@ import SecuritySettingsController from './SecuritySettingsController';
|
|||||||
type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
|
type SecurityProps = AuthenticatedContextProps & RouteComponentProps;
|
||||||
|
|
||||||
class Security extends Component<SecurityProps> {
|
class Security extends Component<SecurityProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="Security">
|
<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/users" label="Manage Users" />
|
||||||
<Tab value="/security/settings" label="Security Settings" />
|
<Tab value="/security/settings" label="Security Settings" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/security/users" component={ManageUsersController} />
|
<AuthenticatedRoute
|
||||||
<AuthenticatedRoute exact path="/security/settings" component={SecuritySettingsController} />
|
exact
|
||||||
|
path="/security/users"
|
||||||
|
component={ManageUsersController}
|
||||||
|
/>
|
||||||
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/security/settings"
|
||||||
|
component={SecuritySettingsController}
|
||||||
|
/>
|
||||||
<Redirect to="/security/users" />
|
<Redirect to="/security/users" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
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 { SECURITY_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import SecuritySettingsForm from './SecuritySettingsForm';
|
import SecuritySettingsForm from './SecuritySettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { SecuritySettings } from './types';
|
|||||||
type SecuritySettingsControllerProps = RestControllerProps<SecuritySettings>;
|
type SecuritySettingsControllerProps = RestControllerProps<SecuritySettings>;
|
||||||
|
|
||||||
class SecuritySettingsController extends Component<SecuritySettingsControllerProps> {
|
class SecuritySettingsController extends Component<SecuritySettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,14 @@ class SecuritySettingsController extends Component<SecuritySettingsControllerPro
|
|||||||
<SectionContent title="Security Settings" titleGutter>
|
<SectionContent title="Security Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <SecuritySettingsForm {...formProps} />}
|
render={(formProps) => <SecuritySettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(SECURITY_SETTINGS_ENDPOINT, SecuritySettingsController);
|
export default restController(
|
||||||
|
SECURITY_SETTINGS_ENDPOINT,
|
||||||
|
SecuritySettingsController
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,19 +4,27 @@ import { ValidatorForm } from 'react-material-ui-form-validator';
|
|||||||
import { Box, Typography } from '@material-ui/core';
|
import { Box, Typography } from '@material-ui/core';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import {
|
||||||
import { RestFormProps, PasswordValidator, FormActions, FormButton } from '../components';
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps
|
||||||
|
} from '../authentication';
|
||||||
|
import {
|
||||||
|
RestFormProps,
|
||||||
|
PasswordValidator,
|
||||||
|
FormActions,
|
||||||
|
FormButton
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
import { SecuritySettings } from './types';
|
import { SecuritySettings } from './types';
|
||||||
|
|
||||||
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> & AuthenticatedContextProps;
|
type SecuritySettingsFormProps = RestFormProps<SecuritySettings> &
|
||||||
|
AuthenticatedContextProps;
|
||||||
|
|
||||||
class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
||||||
|
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
this.props.saveData();
|
this.props.saveData();
|
||||||
this.props.authenticatedContext.refresh();
|
this.props.authenticatedContext.refresh();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, handleValueChange } = this.props;
|
const { data, handleValueChange } = this.props;
|
||||||
@@ -24,7 +32,10 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
|||||||
<ValidatorForm onSubmit={this.onSubmit}>
|
<ValidatorForm onSubmit={this.onSubmit}>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
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"
|
name="jwt_secret"
|
||||||
label="Super User Password"
|
label="Super User Password"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -33,20 +44,32 @@ class SecuritySettingsForm extends React.Component<SecuritySettingsFormProps> {
|
|||||||
onChange={handleValueChange('jwt_secret')}
|
onChange={handleValueChange('jwt_secret')}
|
||||||
margin="normal"
|
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">
|
<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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</ValidatorForm>
|
</ValidatorForm>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withAuthenticatedContext(SecuritySettingsForm);
|
export default withAuthenticatedContext(SecuritySettingsForm);
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import React, { RefObject } from 'react';
|
import React, { RefObject } from 'react';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
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';
|
import { User } from './types';
|
||||||
|
|
||||||
@@ -11,33 +21,67 @@ interface UserFormProps {
|
|||||||
creating: boolean;
|
creating: boolean;
|
||||||
user: User;
|
user: User;
|
||||||
uniqueUsername: (value: any) => boolean;
|
uniqueUsername: (value: any) => boolean;
|
||||||
handleValueChange: (name: keyof User) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
handleValueChange: (
|
||||||
|
name: keyof User
|
||||||
|
) => (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
onDoneEditing: () => void;
|
onDoneEditing: () => void;
|
||||||
onCancelEditing: () => void;
|
onCancelEditing: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserForm extends React.Component<UserFormProps> {
|
class UserForm extends React.Component<UserFormProps> {
|
||||||
|
|
||||||
formRef: RefObject<any> = React.createRef();
|
formRef: RefObject<any> = React.createRef();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('uniqueUsername', this.props.uniqueUsername);
|
ValidatorForm.addValidationRule(
|
||||||
|
'uniqueUsername',
|
||||||
|
this.props.uniqueUsername
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = () => {
|
submit = () => {
|
||||||
this.formRef.current.submit();
|
this.formRef.current.submit();
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
|
const {
|
||||||
|
user,
|
||||||
|
creating,
|
||||||
|
handleValueChange,
|
||||||
|
onDoneEditing,
|
||||||
|
onCancelEditing
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
||||||
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open fullWidth maxWidth="sm">
|
<Dialog
|
||||||
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
|
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>
|
<DialogContent dividers>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={creating ? ['required', 'uniqueUsername', 'matchRegexp:^[a-zA-Z0-9_\\.]{1,24}$'] : []}
|
validators={
|
||||||
errorMessages={creating ? ['Username is required', "Username already exists", "Must be 1-24 characters: alpha numeric, '_' or '.'"] : []}
|
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"
|
name="username"
|
||||||
label="Username"
|
label="Username"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -49,7 +93,10 @@ class UserForm extends React.Component<UserFormProps> {
|
|||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
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"
|
name="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -70,10 +117,19 @@ class UserForm extends React.Component<UserFormProps> {
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<FormButton variant="contained" color="secondary" onClick={onCancelEditing}>
|
<FormButton
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
onClick={onCancelEditing}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</FormButton>
|
</FormButton>
|
||||||
<FormButton variant="contained" color="primary" type="submit" onClick={this.submit}>
|
<FormButton
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
onClick={this.submit}
|
||||||
|
>
|
||||||
Done
|
Done
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export interface User {
|
export interface User {
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
admin: boolean
|
admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SecuritySettings {
|
export interface SecuritySettings {
|
||||||
users: User[]
|
users: User[];
|
||||||
jwt_secret: string
|
jwt_secret: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeneratedToken {
|
export interface GeneratedToken {
|
||||||
token: string
|
token: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,46 +16,46 @@ const isLocalhost = Boolean(
|
|||||||
window.location.hostname === '[::1]' ||
|
window.location.hostname === '[::1]' ||
|
||||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
window.location.hostname.match(
|
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 = {
|
type Config = {
|
||||||
onSuccess?: (registration: ServiceWorkerRegistration) => void
|
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||||
onUpdate?: (registration: ServiceWorkerRegistration) => void
|
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function register(config?: Config) {
|
export function register(config?: Config) {
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// 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) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different 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
|
// 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
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
if (isLocalhost) {
|
if (isLocalhost) {
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
// 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
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
// service worker/PWA documentation.
|
// service worker/PWA documentation.
|
||||||
navigator.serviceWorker.ready.then(() => {
|
navigator.serviceWorker.ready.then(() => {
|
||||||
console.log(
|
console.log(
|
||||||
'This web app is being served cache-first by a service ' +
|
'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 {
|
} else {
|
||||||
// Is not localhost. Just register service worker
|
// 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)
|
.register(swUrl)
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing
|
const installingWorker = registration.installing;
|
||||||
if (installingWorker == null) {
|
if (installingWorker == null) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
installingWorker.onstatechange = () => {
|
installingWorker.onstatechange = () => {
|
||||||
if (installingWorker.state === 'installed') {
|
if (installingWorker.state === 'installed') {
|
||||||
@@ -76,41 +76,41 @@ function registerValidSW(swUrl: string, config?: Config) {
|
|||||||
// content until all client tabs are closed.
|
// content until all client tabs are closed.
|
||||||
console.log(
|
console.log(
|
||||||
'New content is available and will be used when all ' +
|
'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
|
// Execute callback
|
||||||
if (config && config.onUpdate) {
|
if (config && config.onUpdate) {
|
||||||
config.onUpdate(registration)
|
config.onUpdate(registration);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// At this point, everything has been precached.
|
// At this point, everything has been precached.
|
||||||
// It's the perfect time to display a
|
// It's the perfect time to display a
|
||||||
// "Content is cached for offline use." message.
|
// "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
|
// Execute callback
|
||||||
if (config && config.onSuccess) {
|
if (config && config.onSuccess) {
|
||||||
config.onSuccess(registration)
|
config.onSuccess(registration);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error during service worker registration:', error)
|
console.error('Error during service worker registration:', error);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
fetch(swUrl, {
|
fetch(swUrl, {
|
||||||
headers: { 'Service-Worker': 'script' },
|
headers: { 'Service-Worker': 'script' }
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
// 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 (
|
if (
|
||||||
response.status === 404 ||
|
response.status === 404 ||
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
(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.
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister().then(() => {
|
registration.unregister().then(() => {
|
||||||
window.location.reload()
|
window.location.reload();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
// Service worker found. Proceed as normal.
|
// Service worker found. Proceed as normal.
|
||||||
registerValidSW(swUrl, config)
|
registerValidSW(swUrl, config);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log(
|
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() {
|
export function unregister() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.ready.then((registration) => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister()
|
registration.unregister();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { createProxyMiddleware } = require('http-proxy-middleware')
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||||
|
|
||||||
module.exports = function (app) {
|
module.exports = function (app) {
|
||||||
app.use(
|
app.use(
|
||||||
@@ -6,7 +6,7 @@ module.exports = function (app) {
|
|||||||
createProxyMiddleware({
|
createProxyMiddleware({
|
||||||
target: 'http://localhost:3080',
|
target: 'http://localhost:3080',
|
||||||
secure: false,
|
secure: false,
|
||||||
changeOrigin: true,
|
changeOrigin: true
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
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 { OTA_SETTINGS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import OTASettingsForm from './OTASettingsForm';
|
import OTASettingsForm from './OTASettingsForm';
|
||||||
@@ -9,7 +14,6 @@ import { OTASettings } from './types';
|
|||||||
type OTASettingsControllerProps = RestControllerProps<OTASettings>;
|
type OTASettingsControllerProps = RestControllerProps<OTASettings>;
|
||||||
|
|
||||||
class OTASettingsController extends Component<OTASettingsControllerProps> {
|
class OTASettingsController extends Component<OTASettingsControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class OTASettingsController extends Component<OTASettingsControllerProps> {
|
|||||||
<SectionContent title="OTA Settings" titleGutter>
|
<SectionContent title="OTA Settings" titleGutter>
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <OTASettingsForm {...formProps} />}
|
render={(formProps) => <OTASettingsForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(OTA_SETTINGS_ENDPOINT, OTASettingsController);
|
export default restController(OTA_SETTINGS_ENDPOINT, OTASettingsController);
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
|||||||
import { Checkbox } from '@material-ui/core';
|
import { Checkbox } from '@material-ui/core';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
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 { isIP, isHostname, or } from '../validators';
|
||||||
|
|
||||||
import { OTASettings } from './types';
|
import { OTASettings } from './types';
|
||||||
@@ -12,7 +18,6 @@ import { OTASettings } from './types';
|
|||||||
type OTASettingsFormProps = RestFormProps<OTASettings>;
|
type OTASettingsFormProps = RestFormProps<OTASettings>;
|
||||||
|
|
||||||
class OTASettingsForm extends React.Component<OTASettingsFormProps> {
|
class OTASettingsForm extends React.Component<OTASettingsFormProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
ValidatorForm.addValidationRule('isIPOrHostname', or(isIP, isHostname));
|
||||||
}
|
}
|
||||||
@@ -25,14 +30,24 @@ class OTASettingsForm extends React.Component<OTASettingsFormProps> {
|
|||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={data.enabled}
|
checked={data.enabled}
|
||||||
onChange={handleValueChange("enabled")}
|
onChange={handleValueChange('enabled')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Enable OTA Updates"
|
label="Enable OTA Updates"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
validators={['required', 'isNumber', 'minNumber:1025', 'maxNumber:65535']}
|
validators={[
|
||||||
errorMessages={['Port is required', "Must be a number", "Must be greater than 1024 ", "Max value is 65535"]}
|
'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"
|
name="port"
|
||||||
label="Port"
|
label="Port"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -44,7 +59,10 @@ class OTASettingsForm extends React.Component<OTASettingsFormProps> {
|
|||||||
/>
|
/>
|
||||||
<PasswordValidator
|
<PasswordValidator
|
||||||
validators={['required', 'matchRegexp:^.{1,64}$']}
|
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"
|
name="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -54,7 +72,12 @@ class OTASettingsForm extends React.Component<OTASettingsFormProps> {
|
|||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<FormButton startIcon={<SaveIcon />} variant="contained" color="primary" type="submit">
|
<FormButton
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
import { Redirect, Switch, RouteComponentProps } from 'react-router-dom';
|
||||||
|
|
||||||
import { Tabs, Tab } from '@material-ui/core';
|
import { Tabs, Tab } from '@material-ui/core';
|
||||||
|
|
||||||
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
import { WithFeaturesProps, withFeatures } from '../features/FeaturesContext';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps, AuthenticatedRoute } from '../authentication';
|
import {
|
||||||
|
withAuthenticatedContext,
|
||||||
|
AuthenticatedContextProps,
|
||||||
|
AuthenticatedRoute
|
||||||
|
} from '../authentication';
|
||||||
import { MenuAppBar } from '../components';
|
import { MenuAppBar } from '../components';
|
||||||
|
|
||||||
import SystemStatusController from './SystemStatusController';
|
import SystemStatusController from './SystemStatusController';
|
||||||
import OTASettingsController from './OTASettingsController';
|
import OTASettingsController from './OTASettingsController';
|
||||||
import UploadFirmwareController from './UploadFirmwareController';
|
import UploadFirmwareController from './UploadFirmwareController';
|
||||||
|
|
||||||
type SystemProps = AuthenticatedContextProps & RouteComponentProps & WithFeaturesProps;
|
type SystemProps = AuthenticatedContextProps &
|
||||||
|
RouteComponentProps &
|
||||||
|
WithFeaturesProps;
|
||||||
|
|
||||||
class System extends Component<SystemProps> {
|
class System extends Component<SystemProps> {
|
||||||
|
handleTabChange = (path: string) => {
|
||||||
handleTabChange = (event: React.ChangeEvent<{}>, path: string) => {
|
|
||||||
this.props.history.push(path);
|
this.props.history.push(path);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,27 +29,51 @@ class System extends Component<SystemProps> {
|
|||||||
const { authenticatedContext, features } = this.props;
|
const { authenticatedContext, features } = this.props;
|
||||||
return (
|
return (
|
||||||
<MenuAppBar sectionTitle="System">
|
<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" />
|
<Tab value="/system/status" label="System Status" />
|
||||||
{features.ota && (
|
{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 && (
|
{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>
|
</Tabs>
|
||||||
<Switch>
|
<Switch>
|
||||||
<AuthenticatedRoute exact path="/system/status" component={SystemStatusController} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/system/status"
|
||||||
|
component={SystemStatusController}
|
||||||
|
/>
|
||||||
{features.ota && (
|
{features.ota && (
|
||||||
<AuthenticatedRoute exact path="/system/ota" component={OTASettingsController} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/system/ota"
|
||||||
|
component={OTASettingsController}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{features.upload_firmware && (
|
{features.upload_firmware && (
|
||||||
<AuthenticatedRoute exact path="/system/upload" component={UploadFirmwareController} />
|
<AuthenticatedRoute
|
||||||
|
exact
|
||||||
|
path="/system/upload"
|
||||||
|
component={UploadFirmwareController}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Redirect to="/system/status" />
|
<Redirect to="/system/status" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MenuAppBar>
|
</MenuAppBar>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
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 { SYSTEM_STATUS_ENDPOINT } from '../api';
|
||||||
|
|
||||||
import SystemStatusForm from './SystemStatusForm';
|
import SystemStatusForm from './SystemStatusForm';
|
||||||
@@ -9,7 +14,6 @@ import { SystemStatus } from './types';
|
|||||||
type SystemStatusControllerProps = RestControllerProps<SystemStatus>;
|
type SystemStatusControllerProps = RestControllerProps<SystemStatus>;
|
||||||
|
|
||||||
class SystemStatusController extends Component<SystemStatusControllerProps> {
|
class SystemStatusController extends Component<SystemStatusControllerProps> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.loadData();
|
this.props.loadData();
|
||||||
}
|
}
|
||||||
@@ -19,12 +23,11 @@ class SystemStatusController extends Component<SystemStatusControllerProps> {
|
|||||||
<SectionContent title="System Status">
|
<SectionContent title="System Status">
|
||||||
<RestFormLoader
|
<RestFormLoader
|
||||||
{...this.props}
|
{...this.props}
|
||||||
render={formProps => <SystemStatusForm {...formProps} />}
|
render={(formProps) => <SystemStatusForm {...formProps} />}
|
||||||
/>
|
/>
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restController(SYSTEM_STATUS_ENDPOINT, SystemStatusController);
|
export default restController(SYSTEM_STATUS_ENDPOINT, SystemStatusController);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import { SectionContent } from '../components';
|
import { SectionContent } from '../components';
|
||||||
import { UPLOAD_FIRMWARE_ENDPOINT } from '../api';
|
import { UPLOAD_FIRMWARE_ENDPOINT } from '../api';
|
||||||
@@ -12,8 +12,10 @@ interface UploadFirmwareControllerState {
|
|||||||
progress?: ProgressEvent;
|
progress?: ProgressEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadFirmwareController extends Component<WithSnackbarProps, UploadFirmwareControllerState> {
|
class UploadFirmwareController extends Component<
|
||||||
|
WithSnackbarProps,
|
||||||
|
UploadFirmwareControllerState
|
||||||
|
> {
|
||||||
state: UploadFirmwareControllerState = {
|
state: UploadFirmwareControllerState = {
|
||||||
xhr: undefined,
|
xhr: undefined,
|
||||||
progress: undefined
|
progress: undefined
|
||||||
@@ -25,47 +27,67 @@ class UploadFirmwareController extends Component<WithSnackbarProps, UploadFirmwa
|
|||||||
|
|
||||||
updateProgress = (progress: ProgressEvent) => {
|
updateProgress = (progress: ProgressEvent) => {
|
||||||
this.setState({ progress });
|
this.setState({ progress });
|
||||||
}
|
};
|
||||||
|
|
||||||
uploadFile = (file: File) => {
|
uploadFile = (file: File) => {
|
||||||
if (this.state.xhr) {
|
if (this.state.xhr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
this.setState({ xhr });
|
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) {
|
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 });
|
this.setState({ xhr: undefined, progress: undefined });
|
||||||
}).catch((error: Error) => {
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
this.props.enqueueSnackbar("Upload cancelled by user", { variant: 'warning' });
|
this.props.enqueueSnackbar('Upload cancelled by user', {
|
||||||
|
variant: 'warning'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = error.name === 'UploadError' ? "Error during upload" : (error.message || "Unknown error");
|
const errorMessage =
|
||||||
this.props.enqueueSnackbar("Problem uploading: " + errorMessage, { variant: 'error' });
|
error.name === 'UploadError'
|
||||||
|
? 'Error during upload'
|
||||||
|
: error.message || 'Unknown error';
|
||||||
|
this.props.enqueueSnackbar('Problem uploading: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
this.setState({ xhr: undefined, progress: undefined });
|
this.setState({ xhr: undefined, progress: undefined });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
cancelUpload = () => {
|
cancelUpload = () => {
|
||||||
if (this.state.xhr) {
|
if (this.state.xhr) {
|
||||||
this.state.xhr.abort();
|
this.state.xhr.abort();
|
||||||
this.setState({ xhr: undefined, progress: undefined });
|
this.setState({ xhr: undefined, progress: undefined });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { xhr, progress } = this.state;
|
const { xhr, progress } = this.state;
|
||||||
return (
|
return (
|
||||||
<SectionContent title="Upload Firmware">
|
<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>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(UploadFirmwareController);
|
export default withSnackbar(UploadFirmwareController);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user