initial commit

This commit is contained in:
proddy
2020-07-05 18:29:08 +02:00
parent 26b201ea2f
commit c5933e8c14
739 changed files with 86566 additions and 20952 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,286 @@
import React, { RefObject, Fragment } from 'react';
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, Box, IconButton } from '@material-ui/core';
import { ClickAwayListener, Popper, Hidden, Typography } from '@material-ui/core';
import { List, ListItem, ListItemIcon, ListItemText, ListItemAvatar } from '@material-ui/core';
import { Card, CardContent, CardActions } from '@material-ui/core';
import { withStyles, createStyles, Theme, WithTheme, WithStyles, withTheme } from '@material-ui/core/styles';
import WifiIcon from '@material-ui/icons/Wifi';
import SettingsIcon from '@material-ui/icons/Settings';
import AccessTimeIcon from '@material-ui/icons/AccessTime';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna';
import DeviceHubIcon from '@material-ui/icons/DeviceHub';
import LockIcon from '@material-ui/icons/Lock';
import MenuIcon from '@material-ui/icons/Menu';
import ProjectMenu from '../project/ProjectMenu';
import { PROJECT_NAME } from '../api';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import { withFeatures, WithFeaturesProps } from '../features/FeaturesContext';
const drawerWidth = 290;
const styles = (theme: Theme) => createStyles({
root: {
display: 'flex',
},
drawer: {
[theme.breakpoints.up('md')]: {
width: drawerWidth,
flexShrink: 0,
},
},
title: {
flexGrow: 1
},
appBar: {
marginLeft: drawerWidth,
[theme.breakpoints.up('md')]: {
width: `calc(100% - ${drawerWidth}px)`,
},
},
toolbarImage: {
[theme.breakpoints.up('xs')]: {
height: 24,
marginRight: theme.spacing(2)
},
[theme.breakpoints.up('sm')]: {
height: 36,
marginRight: theme.spacing(3)
},
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up('md')]: {
display: 'none',
},
},
toolbar: theme.mixins.toolbar,
drawerPaper: {
width: drawerWidth,
},
content: {
flexGrow: 1
},
authMenu: {
zIndex: theme.zIndex.tooltip,
maxWidth: 400,
},
authMenuActions: {
padding: theme.spacing(2),
"& > * + *": {
marginLeft: theme.spacing(2),
}
},
});
interface MenuAppBarState {
mobileOpen: boolean;
authMenuOpen: boolean;
}
interface MenuAppBarProps extends WithFeaturesProps, AuthenticatedContextProps, WithTheme, WithStyles<typeof styles>, RouteComponentProps {
sectionTitle: string;
}
class MenuAppBar extends React.Component<MenuAppBarProps, MenuAppBarState> {
constructor(props: MenuAppBarProps) {
super(props);
this.state = {
mobileOpen: false,
authMenuOpen: false
};
}
anchorRef: RefObject<HTMLButtonElement> = React.createRef();
handleToggle = () => {
this.setState({ authMenuOpen: !this.state.authMenuOpen });
}
handleClose = (event: React.MouseEvent<Document>) => {
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
return;
}
this.setState({ authMenuOpen: false });
}
handleDrawerToggle = () => {
this.setState({ mobileOpen: !this.state.mobileOpen });
};
render() {
const { classes, theme, children, sectionTitle, authenticatedContext, features } = this.props;
const { mobileOpen, authMenuOpen } = this.state;
const path = this.props.match.url;
const drawer = (
<div>
<Toolbar>
<Box display="flex">
<img src="/app/icon.png" className={classes.toolbarImage} alt={PROJECT_NAME} />
</Box>
<Typography variant="h6" color="textPrimary">
{PROJECT_NAME}
</Typography>
<Divider absolute />
</Toolbar>
{features.project && (
<Fragment>
<ProjectMenu />
<Divider />
</Fragment>
)}
<List>
<ListItem to='/wifi/' selected={path.startsWith('/wifi/')} button component={Link}>
<ListItemIcon>
<WifiIcon />
</ListItemIcon>
<ListItemText primary="WiFi Connection" />
</ListItem>
<ListItem to='/ap/' selected={path.startsWith('/ap/')} button component={Link}>
<ListItemIcon>
<SettingsInputAntennaIcon />
</ListItemIcon>
<ListItemText primary="Access Point" />
</ListItem>
{features.ntp && (
<ListItem to='/ntp/' selected={path.startsWith('/ntp/')} button component={Link}>
<ListItemIcon>
<AccessTimeIcon />
</ListItemIcon>
<ListItemText primary="Network Time" />
</ListItem>
)}
{features.mqtt && (
<ListItem to='/mqtt/' selected={path.startsWith('/mqtt/')} button component={Link}>
<ListItemIcon>
<DeviceHubIcon />
</ListItemIcon>
<ListItemText primary="MQTT" />
</ListItem>
)}
{features.security && (
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
<ListItemIcon>
<LockIcon />
</ListItemIcon>
<ListItemText primary="Security" />
</ListItem>
)}
<ListItem to='/system/' selected={path.startsWith('/system/')} button component={Link} >
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary="System" />
</ListItem>
</List>
</div>
);
const userMenu = (
<div>
<IconButton
ref={this.anchorRef}
aria-owns={authMenuOpen ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={this.handleToggle}
color="inherit"
>
<AccountCircleIcon />
</IconButton>
<Popper open={authMenuOpen} anchorEl={this.anchorRef.current} transition className={classes.authMenu}>
<ClickAwayListener onClickAway={this.handleClose}>
<Card id="menu-list-grow">
<CardContent>
<List disablePadding>
<ListItem disableGutters>
<ListItemAvatar>
<Avatar>
<AccountCircleIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={"Signed in as: " + authenticatedContext.me.username} secondary={authenticatedContext.me.admin ? "Admin User" : undefined} />
</ListItem>
</List>
</CardContent>
<Divider />
<CardActions className={classes.authMenuActions}>
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
</CardActions>
</Card>
</ClickAwayListener>
</Popper>
</div>
);
return (
<div className={classes.root}>
<AppBar position="fixed" className={classes.appBar} elevation={0}>
<Toolbar>
<IconButton
color="inherit"
aria-label="Open drawer"
edge="start"
onClick={this.handleDrawerToggle}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" color="inherit" noWrap className={classes.title}>
{sectionTitle}
</Typography>
{features.security && userMenu}
</Toolbar>
</AppBar>
<nav className={classes.drawer}>
<Hidden mdUp implementation="css">
<Drawer
variant="temporary"
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={mobileOpen}
onClose={this.handleDrawerToggle}
classes={{
paper: classes.drawerPaper,
}}
ModalProps={{
keepMounted: true,
}}
>
{drawer}
</Drawer>
</Hidden>
<Hidden smDown implementation="css">
<Drawer
classes={{
paper: classes.drawerPaper,
}}
variant="permanent"
open
>
{drawer}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar} />
{children}
</main>
</div>
);
}
}
export default withRouter(
withTheme(
withFeatures(
withAuthenticatedContext(
withStyles(styles)(MenuAppBar)
)
)
)
);

View File

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

View File

@@ -0,0 +1,113 @@
import React from 'react';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import { redirectingAuthorizedFetch } from '../authentication';
export interface RestControllerProps<D> extends WithSnackbarProps {
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
setData: (data: D, callback?: () => void) => void;
saveData: () => void;
loadData: () => void;
data?: D;
loading: boolean;
errorMessage?: string;
}
export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) => {
switch (event.target.type) {
case "number":
return event.target.valueAsNumber;
case "checkbox":
return event.target.checked;
default:
return event.target.value
}
}
interface RestControllerState<D> {
data?: D;
loading: boolean;
errorMessage?: string;
}
export function restController<D, P extends RestControllerProps<D>>(endpointUrl: string, RestController: React.ComponentType<P & RestControllerProps<D>>) {
return withSnackbar(
class extends React.Component<Omit<P, keyof RestControllerProps<D>> & WithSnackbarProps, RestControllerState<D>> {
state: RestControllerState<D> = {
data: undefined,
loading: false,
errorMessage: undefined
};
setData = (data: D, callback?: () => void) => {
this.setState({
data,
loading: false,
errorMessage: undefined
}, callback);
}
loadData = () => {
this.setState({
data: undefined,
loading: true,
errorMessage: undefined
});
redirectingAuthorizedFetch(endpointUrl).then(response => {
if (response.status === 200) {
return response.json();
}
throw Error("Invalid status code: " + response.status);
}).then(json => {
this.setState({ data: json, loading: false })
}).catch(error => {
const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
this.setState({ data: undefined, loading: false, errorMessage });
});
}
saveData = () => {
this.setState({ loading: true });
redirectingAuthorizedFetch(endpointUrl, {
method: 'POST',
body: JSON.stringify(this.state.data),
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.status === 200) {
return response.json();
}
throw Error("Invalid status code: " + response.status);
}).then(json => {
this.props.enqueueSnackbar("Changes successfully applied.", { variant: 'success' });
this.setState({ data: json, loading: false });
}).catch(error => {
const errorMessage = error.message || "Unknown error";
this.props.enqueueSnackbar("Problem saving: " + errorMessage, { variant: 'error' });
this.setState({ data: undefined, loading: false, errorMessage });
});
}
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
const data = { ...this.state.data!, [name]: extractEventValue(event) };
this.setState({ data });
}
render() {
return <RestController
{...this.state}
{...this.props as P}
handleValueChange={this.handleValueChange}
setData={this.setData}
saveData={this.saveData}
loadData={this.loadData}
/>;
}
});
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
import { Button, LinearProgress, Typography } from '@material-ui/core';
import { RestControllerProps } from '.';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
loadingSettings: {
margin: theme.spacing(0.5),
},
loadingSettingsDetails: {
margin: theme.spacing(4),
textAlign: "center"
},
button: {
marginRight: theme.spacing(2),
marginTop: theme.spacing(2),
}
})
);
export type RestFormProps<D> = Omit<RestControllerProps<D>, "loading" | "errorMessage"> & { data: D };
interface RestFormLoaderProps<D> extends RestControllerProps<D> {
render: (props: RestFormProps<D>) => JSX.Element;
}
export default function RestFormLoader<D>(props: RestFormLoaderProps<D>) {
const { loading, errorMessage, loadData, render, data, ...rest } = props;
const classes = useStyles();
if (loading || !data) {
return (
<div className={classes.loadingSettings}>
<LinearProgress className={classes.loadingSettingsDetails} />
<Typography variant="h6" className={classes.loadingSettingsDetails}>
Loading&hellip;
</Typography>
</div>
);
}
if (errorMessage) {
return (
<div className={classes.loadingSettings}>
<Typography variant="h6" className={classes.loadingSettingsDetails}>
{errorMessage}
</Typography>
<Button variant="contained" color="secondary" className={classes.button} onClick={loadData}>
Reset
</Button>
</div>
);
}
return render({ ...rest, loadData, data });
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { Typography, Paper } from '@material-ui/core';
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
content: {
padding: theme.spacing(2),
margin: theme.spacing(3),
}
})
);
interface SectionContentProps {
title: string;
titleGutter?: boolean;
}
const SectionContent: React.FC<SectionContentProps> = (props) => {
const { children, title, titleGutter } = props;
const classes = useStyles();
return (
<Paper className={classes.content}>
<Typography variant="h6" gutterBottom={titleGutter}>
{title}
</Typography>
{children}
</Paper>
);
};
export default SectionContent;

View File

@@ -0,0 +1,97 @@
import React, { FC, Fragment } from 'react';
import { useDropzone, DropzoneState } from 'react-dropzone';
import { makeStyles, createStyles } from '@material-ui/styles';
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
import CancelIcon from '@material-ui/icons/Cancel';
import { Theme, Box, Typography, LinearProgress, Button } from '@material-ui/core';
import ErrorButton from './ErrorButton';
interface SingleUploadStyleProps extends DropzoneState {
uploading: boolean;
}
const progressPercentage = (progress: ProgressEvent) => Math.round((progress.loaded * 100) / progress.total);
const getBorderColor = (theme: Theme, props: SingleUploadStyleProps) => {
if (props.isDragAccept) {
return theme.palette.success.main;
}
if (props.isDragReject) {
return theme.palette.error.main;
}
if (props.isDragActive) {
return theme.palette.info.main;
}
return theme.palette.grey[700];
}
const useStyles = makeStyles((theme: Theme) => createStyles({
dropzone: {
padding: theme.spacing(8, 2),
borderWidth: 2,
borderRadius: 2,
borderStyle: 'dashed',
color: theme.palette.grey[700],
transition: 'border .24s ease-in-out',
cursor: (props: SingleUploadStyleProps) => props.uploading ? 'default' : 'pointer',
width: '100%',
borderColor: (props: SingleUploadStyleProps) => getBorderColor(theme, props)
}
}));
export interface SingleUploadProps {
onDrop: (acceptedFiles: File[]) => void;
onCancel: () => void;
accept?: string | string[];
uploading: boolean;
progress?: ProgressEvent;
}
const SingleUpload: FC<SingleUploadProps> = ({ onDrop, onCancel, accept, uploading, progress }) => {
const dropzoneState = useDropzone({ onDrop, accept, disabled: uploading, multiple: false });
const { getRootProps, getInputProps } = dropzoneState;
const classes = useStyles({ ...dropzoneState, uploading });
const renderProgressText = () => {
if (uploading) {
if (progress?.lengthComputable) {
return `Uploading: ${progressPercentage(progress)}%`;
}
return "Uploading\u2026";
}
return "Drop file here or click to browse";
}
const renderProgress = (progress?: ProgressEvent) => (
<LinearProgress
variant={!progress || progress.lengthComputable ? "determinate" : "indeterminate"}
value={!progress ? 0 : progress.lengthComputable ? progressPercentage(progress) : 0}
/>
);
return (
<div {...getRootProps({ className: classes.dropzone })}>
<input {...getInputProps()} />
<Box flexDirection="column" display="flex" alignItems="center">
<CloudUploadIcon fontSize='large' />
<Typography variant="h6">
{renderProgressText()}
</Typography>
{uploading && (
<Fragment>
<Box width="100%" p={2}>
{renderProgress(progress)}
</Box>
<ErrorButton startIcon={<CancelIcon />} variant="contained" color="primary" onClick={onCancel}>
Cancel
</ErrorButton>
</Fragment>
)}
</Box>
</div>
);
}
export default SingleUpload;

View File

@@ -0,0 +1,133 @@
import React from 'react';
import Sockette from 'sockette';
import throttle from 'lodash/throttle';
import { withSnackbar, WithSnackbarProps } from 'notistack';
import { addAccessTokenParameter } from '../authentication';
import { extractEventValue } from '.';
export interface WebSocketControllerProps<D> extends WithSnackbarProps {
handleValueChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => void;
setData: (data: D, callback?: () => void) => void;
saveData: () => void;
saveDataAndClear(): () => void;
connected: boolean;
data?: D;
}
interface WebSocketControllerState<D> {
ws: Sockette;
connected: boolean;
clientId?: string;
data?: D;
}
enum WebSocketMessageType {
ID = "id",
PAYLOAD = "payload"
}
interface WebSocketIdMessage {
type: typeof WebSocketMessageType.ID;
id: string;
}
interface WebSocketPayloadMessage<D> {
type: typeof WebSocketMessageType.PAYLOAD;
origin_id: string;
payload: 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>>) {
return withSnackbar(
class extends React.Component<Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps, WebSocketControllerState<D>> {
constructor(props: Omit<P, keyof WebSocketControllerProps<D>> & WithSnackbarProps) {
super(props);
this.state = {
ws: new Sockette(addAccessTokenParameter(wsUrl), {
onmessage: this.onMessage,
onopen: this.onOpen,
onclose: this.onClose,
}),
connected: false
}
}
componentWillUnmount() {
this.state.ws.close();
}
onMessage = (event: MessageEvent) => {
const rawData = event.data;
if (typeof rawData === 'string' || rawData instanceof String) {
this.handleMessage(JSON.parse(rawData as string) as WebSocketMessage<D>);
}
}
handleMessage = (message: WebSocketMessage<D>) => {
switch (message.type) {
case WebSocketMessageType.ID:
this.setState({ clientId: message.id });
break;
case WebSocketMessageType.PAYLOAD:
const { clientId, data } = this.state;
if (clientId && (!data || clientId !== message.origin_id)) {
this.setState(
{ data: message.payload }
);
}
break;
}
}
onOpen = () => {
this.setState({ connected: true });
}
onClose = () => {
this.setState({ connected: false, clientId: undefined, data: undefined });
}
setData = (data: D, callback?: () => void) => {
this.setState({ data }, callback);
}
saveData = throttle(() => {
const { ws, connected, data } = this.state;
if (connected) {
ws.json(data);
}
}, wsThrottle);
saveDataAndClear = throttle(() => {
const { ws, connected, data } = this.state;
if (connected) {
this.setState({
data: undefined
}, () => ws.json(data));
}
}, wsThrottle);
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
const data = { ...this.state.data!, [name]: extractEventValue(event) };
this.setState({ data });
}
render() {
return <WebSocketController
{...this.props as P}
handleValueChange={this.handleValueChange}
setData={this.setData}
saveData={this.saveData}
saveDataAndClear={this.saveDataAndClear}
connected={this.state.connected}
data={this.state.data}
/>;
}
});
}

View File

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

View File

@@ -0,0 +1,17 @@
export { default as BlockFormControlLabel } from './BlockFormControlLabel';
export { default as FormActions } from './FormActions';
export { default as FormButton } from './FormButton';
export { default as HighlightAvatar } from './HighlightAvatar';
export { default as MenuAppBar } from './MenuAppBar';
export { default as PasswordValidator } from './PasswordValidator';
export { default as RestFormLoader } from './RestFormLoader';
export { default as SectionContent } from './SectionContent';
export { default as WebSocketFormLoader } from './WebSocketFormLoader';
export { default as ErrorButton } from './ErrorButton';
export { default as SingleUpload } from './SingleUpload';
export * from './RestFormLoader';
export * from './RestController';
export * from './WebSocketFormLoader';
export * from './WebSocketController';