Re-engineer UI in TypeScript (#89)
* Re-engineer UI in TypeScript * Switch to named imports where possible * Restructure file system layout * Update depencencies * Update README.md * Change explicit colors for better support for dark theme
This commit is contained in:
10
interface/src/components/BlockFormControlLabel.tsx
Normal file
10
interface/src/components/BlockFormControlLabel.tsx
Normal 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;
|
7
interface/src/components/FormActions.tsx
Normal file
7
interface/src/components/FormActions.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { styled, Box } from "@material-ui/core";
|
||||
|
||||
const FormActions = styled(Box)(({ theme }) => ({
|
||||
marginTop: theme.spacing(1)
|
||||
}));
|
||||
|
||||
export default FormActions;
|
13
interface/src/components/FormButton.tsx
Normal file
13
interface/src/components/FormButton.tsx
Normal 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;
|
23
interface/src/components/HighlightAvatar.tsx
Normal file
23
interface/src/components/HighlightAvatar.tsx
Normal 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;
|
@ -1,58 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
loadingSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
},
|
||||
loadingSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
}
|
||||
}));
|
||||
|
||||
export default function LoadingNotification(props) {
|
||||
const classes = useStyles();
|
||||
const { fetched, errorMessage, onReset, render } = props;
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
fetched ?
|
||||
errorMessage ?
|
||||
<div className={classes.loadingSettings}>
|
||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
<Button variant="contained" color="secondary" className={classes.button} onClick={onReset}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
render()
|
||||
:
|
||||
<div className={classes.loadingSettings}>
|
||||
<LinearProgress className={classes.loadingSettingsDetails} />
|
||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||
Loading...
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LoadingNotification.propTypes = {
|
||||
fetched: PropTypes.bool.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
render: PropTypes.func.isRequired
|
||||
};
|
@ -1,42 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import React, { RefObject } from 'react';
|
||||
import { Link, withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { Drawer, AppBar, Toolbar, Avatar, Divider, Button, 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 { withStyles } from '@material-ui/core/styles';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Hidden from '@material-ui/core/Hidden';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
||||
import Popper from '@material-ui/core/Popper';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
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 LockIcon from '@material-ui/icons/Lock';
|
||||
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import CardActions from '@material-ui/core/CardActions';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
|
||||
import ProjectMenu from '../project/ProjectMenu';
|
||||
import { PROJECT_NAME } from '../constants/Env';
|
||||
import { withAuthenticationContext } from '../authentication/Context.js';
|
||||
import { PROJECT_NAME } from '../api';
|
||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||
|
||||
const drawerWidth = 290;
|
||||
|
||||
const styles = theme => ({
|
||||
const styles = (theme: Theme) => createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
@ -77,26 +63,38 @@ const styles = theme => ({
|
||||
"& > * + *": {
|
||||
marginLeft: theme.spacing(2),
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
class MenuAppBar extends React.Component {
|
||||
state = {
|
||||
mobileOpen: false,
|
||||
authMenuOpen: false
|
||||
};
|
||||
interface MenuAppBarState {
|
||||
mobileOpen: boolean;
|
||||
authMenuOpen: boolean;
|
||||
}
|
||||
|
||||
anchorRef = React.createRef();
|
||||
interface MenuAppBarProps extends 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) => {
|
||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.target)) {
|
||||
handleClose = (event: React.MouseEvent<Document>) => {
|
||||
if (this.anchorRef.current && this.anchorRef.current.contains(event.currentTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ authMenuOpen: false });
|
||||
}
|
||||
|
||||
@ -105,13 +103,13 @@ class MenuAppBar extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { classes, theme, children, sectionTitle, authenticationContext } = this.props;
|
||||
const { classes, theme, children, sectionTitle, authenticatedContext } = this.props;
|
||||
const { mobileOpen, authMenuOpen } = this.state;
|
||||
const path = this.props.match.url;
|
||||
const drawer = (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" color="primary">
|
||||
<Typography variant="h6" color="textPrimary">
|
||||
{PROJECT_NAME}
|
||||
</Typography>
|
||||
<Divider absolute />
|
||||
@ -138,7 +136,7 @@ class MenuAppBar extends React.Component {
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Network Time" />
|
||||
</ListItem>
|
||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticationContext.isAdmin()}>
|
||||
<ListItem to='/security/' selected={path.startsWith('/security/')} button component={Link} disabled={!authenticatedContext.me.admin}>
|
||||
<ListItemIcon>
|
||||
<LockIcon />
|
||||
</ListItemIcon>
|
||||
@ -156,7 +154,7 @@ class MenuAppBar extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<AppBar position="fixed" className={classes.appBar}>
|
||||
<AppBar position="fixed" className={classes.appBar} elevation={0}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
@ -191,13 +189,13 @@ class MenuAppBar extends React.Component {
|
||||
<AccountCircleIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={"Signed in as: " + authenticationContext.user.username} secondary={authenticationContext.isAdmin() ? "Admin User" : undefined} />
|
||||
<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" color="primary" onClick={authenticationContext.signOut}>Sign Out</Button>
|
||||
<Button variant="contained" fullWidth color="primary" onClick={authenticatedContext.signOut}>Sign Out</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</ClickAwayListener>
|
||||
@ -243,14 +241,10 @@ class MenuAppBar extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
MenuAppBar.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
sectionTitle: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default withAuthenticationContext(
|
||||
withRouter(
|
||||
withStyles(styles, { withTheme: true })(MenuAppBar)
|
||||
export default withRouter(
|
||||
withTheme(
|
||||
withAuthenticatedContext(
|
||||
withStyles(styles)(MenuAppBar)
|
||||
)
|
||||
)
|
||||
);
|
@ -1,21 +1,25 @@
|
||||
import React from 'react';
|
||||
import { TextValidator } from 'react-material-ui-form-validator';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { InputAdornment } from '@material-ui/core';
|
||||
import Visibility from '@material-ui/icons/Visibility';
|
||||
import VisibilityOff from '@material-ui/icons/VisibilityOff';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { TextValidator, ValidatorComponentProps } from 'react-material-ui-form-validator';
|
||||
|
||||
const styles = theme => (
|
||||
{
|
||||
input: {
|
||||
"&::-ms-reveal": {
|
||||
display: "none"
|
||||
}
|
||||
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"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
class PasswordValidator extends React.Component {
|
||||
type PasswordValidatorProps = WithStyles<typeof styles> & Exclude<ValidatorComponentProps, "type" | "InputProps">;
|
||||
|
||||
interface PasswordValidatorState {
|
||||
showPassword: boolean;
|
||||
}
|
||||
|
||||
class PasswordValidator extends React.Component<PasswordValidatorProps, PasswordValidatorState> {
|
||||
|
||||
state = {
|
||||
showPassword: false
|
@ -1,125 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withSnackbar } from 'notistack';
|
||||
import { redirectingAuthorizedFetch } from '../authentication/Authentication';
|
||||
|
||||
/*
|
||||
* It is unlikely this application will grow complex enough to require redux.
|
||||
*
|
||||
* This HOC acts as an interface to a REST service, providing data and change
|
||||
* event callbacks to the wrapped components along with a function to persist the
|
||||
* changes.
|
||||
*/
|
||||
export const restComponent = (endpointUrl, FormComponent) => {
|
||||
|
||||
return withSnackbar(
|
||||
class extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
data: null,
|
||||
fetched: false,
|
||||
errorMessage: null
|
||||
};
|
||||
|
||||
this.setState = this.setState.bind(this);
|
||||
this.loadData = this.loadData.bind(this);
|
||||
this.saveData = this.saveData.bind(this);
|
||||
this.setData = this.setData.bind(this);
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.setState({
|
||||
data: data,
|
||||
fetched: true,
|
||||
errorMessage: null
|
||||
});
|
||||
}
|
||||
|
||||
loadData() {
|
||||
this.setState({
|
||||
data: null,
|
||||
fetched: false,
|
||||
errorMessage: null
|
||||
});
|
||||
redirectingAuthorizedFetch(endpointUrl)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
})
|
||||
.then(json => { this.setState({ data: json, fetched: true }) })
|
||||
.catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, {
|
||||
variant: 'error',
|
||||
});
|
||||
this.setState({ data: null, fetched: true, errorMessage });
|
||||
});
|
||||
}
|
||||
|
||||
saveData(e) {
|
||||
this.setState({ fetched: false });
|
||||
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, fetched: true });
|
||||
}).catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem saving: " + errorMessage, {
|
||||
variant: 'error',
|
||||
});
|
||||
this.setState({ data: null, fetched: true, errorMessage });
|
||||
});
|
||||
}
|
||||
|
||||
handleValueChange = name => (event) => {
|
||||
const { data } = this.state;
|
||||
data[name] = event.target.value;
|
||||
this.setState({ data });
|
||||
};
|
||||
|
||||
handleSliderChange = name => (event, newValue) => {
|
||||
const { data } = this.state;
|
||||
data[name] = newValue;
|
||||
this.setState({ data });
|
||||
};
|
||||
|
||||
handleCheckboxChange = name => event => {
|
||||
const { data } = this.state;
|
||||
data[name] = event.target.checked;
|
||||
this.setState({ data });
|
||||
}
|
||||
|
||||
render() {
|
||||
return <FormComponent
|
||||
handleValueChange={this.handleValueChange}
|
||||
handleCheckboxChange={this.handleCheckboxChange}
|
||||
handleSliderChange={this.handleSliderChange}
|
||||
setData={this.setData}
|
||||
saveData={this.saveData}
|
||||
loadData={this.loadData}
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
/>;
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
}
|
116
interface/src/components/RestController.tsx
Normal file
116
interface/src/components/RestController.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
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;
|
||||
handleCheckboxChange: (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => void;
|
||||
handleSliderChange: (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => void;
|
||||
|
||||
setData: (data: D) => void;
|
||||
saveData: () => void;
|
||||
loadData: () => void;
|
||||
|
||||
data?: D;
|
||||
loading: boolean;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
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) => {
|
||||
this.setState({
|
||||
data,
|
||||
loading: false,
|
||||
errorMessage: undefined
|
||||
});
|
||||
}
|
||||
|
||||
loadData = () => {
|
||||
this.setState({
|
||||
data: undefined,
|
||||
loading: true,
|
||||
errorMessage: undefined
|
||||
});
|
||||
redirectingAuthorizedFetch(endpointUrl).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}).then(json => {
|
||||
this.setState({ data: json, loading: false })
|
||||
}).catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem fetching: " + errorMessage, { variant: 'error' });
|
||||
this.setState({ data: undefined, loading: false, errorMessage });
|
||||
});
|
||||
}
|
||||
|
||||
saveData = () => {
|
||||
this.setState({ loading: true });
|
||||
redirectingAuthorizedFetch(endpointUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(this.state.data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error("Invalid status code: " + response.status);
|
||||
}).then(json => {
|
||||
this.props.enqueueSnackbar("Changes successfully applied.", { variant: 'success' });
|
||||
this.setState({ data: json, loading: false });
|
||||
}).catch(error => {
|
||||
const errorMessage = error.message || "Unknown error";
|
||||
this.props.enqueueSnackbar("Problem saving: " + errorMessage, { variant: 'error' });
|
||||
this.setState({ data: undefined, loading: false, errorMessage });
|
||||
});
|
||||
}
|
||||
|
||||
handleValueChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const data = { ...this.state.data!, [name]: event.target.value };
|
||||
this.setState({ data });
|
||||
}
|
||||
|
||||
handleCheckboxChange = (name: keyof D) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const data = { ...this.state.data!, [name]: event.target.checked };
|
||||
this.setState({ data });
|
||||
}
|
||||
|
||||
handleSliderChange = (name: keyof D) => (event: React.ChangeEvent<{}>, value: number | number[]) => {
|
||||
const data = { ...this.state.data!, [name]: value };
|
||||
this.setState({ data });
|
||||
};
|
||||
|
||||
render() {
|
||||
return <RestController
|
||||
handleValueChange={this.handleValueChange}
|
||||
handleCheckboxChange={this.handleCheckboxChange}
|
||||
handleSliderChange={this.handleSliderChange}
|
||||
setData={this.setData}
|
||||
saveData={this.saveData}
|
||||
loadData={this.loadData}
|
||||
{...this.state}
|
||||
{...this.props as P}
|
||||
/>;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
55
interface/src/components/RestFormLoader.tsx
Normal file
55
interface/src/components/RestFormLoader.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
import { Button, LinearProgress, Typography } from '@material-ui/core';
|
||||
import { RestControllerProps } from './RestController';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
loadingSettings: {
|
||||
margin: theme.spacing(0.5),
|
||||
},
|
||||
loadingSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: "center"
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2),
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export type RestFormProps<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...
|
||||
</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 });
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
const styles = theme => ({
|
||||
content: {
|
||||
padding: theme.spacing(2),
|
||||
margin: theme.spacing(3),
|
||||
}
|
||||
});
|
||||
|
||||
function SectionContent(props) {
|
||||
const { children, classes, title, titleGutter } = props;
|
||||
return (
|
||||
<Paper className={classes.content}>
|
||||
<Typography variant="h6" gutterBottom={titleGutter}>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
SectionContent.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
]).isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
titleGutter: PropTypes.bool
|
||||
};
|
||||
|
||||
export default withStyles(styles)(SectionContent);
|
33
interface/src/components/SectionContent.tsx
Normal file
33
interface/src/components/SectionContent.tsx
Normal 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;
|
11
interface/src/components/index.ts
Normal file
11
interface/src/components/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export { default as BlockFormControlLabel } from './BlockFormControlLabel';
|
||||
export { default as FormActions } from './FormActions';
|
||||
export { default as FormButton } from './FormButton';
|
||||
export { default as HighlightAvatar } from './HighlightAvatar';
|
||||
export { default as MenuAppBar } from './MenuAppBar';
|
||||
export { default as PasswordValidator } from './PasswordValidator';
|
||||
export { default as RestFormLoader } from './RestFormLoader';
|
||||
export { default as SectionContent } from './SectionContent';
|
||||
|
||||
export * from './RestFormLoader';
|
||||
export * from './RestController';
|
Reference in New Issue
Block a user