mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
replace class components (HOCs) with React Hooks
This commit is contained in:
@@ -3,13 +3,13 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.11.4",
|
"@material-ui/core": "^4.12.3",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@msgpack/msgpack": "^2.7.0",
|
"@msgpack/msgpack": "^2.7.0",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.172",
|
||||||
"@types/node": "^15.0.1",
|
"@types/node": "^12.20.20",
|
||||||
"@types/react": "^17.0.4",
|
"@types/react": "^17.0.19",
|
||||||
"@types/react-dom": "^17.0.3",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-material-ui-form-validator": "^2.1.0",
|
"@types/react-material-ui-form-validator": "^2.1.0",
|
||||||
"@types/react-router": "^5.1.13",
|
"@types/react-router": "^5.1.13",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"typescript": "4.2.4",
|
"typescript": "4.3.5",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -22,25 +22,13 @@ interface UnauthenticatedRouteProps
|
|||||||
| React.ComponentType<any>;
|
| React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
|
||||||
|
|
||||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const { authenticationContext, features, ...rest } = this.props;
|
||||||
authenticationContext,
|
if (authenticationContext.me) {
|
||||||
component: Component,
|
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
|
||||||
features,
|
}
|
||||||
...rest
|
return <Route {...rest} />;
|
||||||
} = this.props;
|
|
||||||
const renderComponent: RenderComponent = (props) => {
|
|
||||||
if (authenticationContext.me) {
|
|
||||||
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
|
|
||||||
}
|
|
||||||
if (Component) {
|
|
||||||
return <Component {...props} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return <Route {...rest} render={renderComponent} />;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
interface/src/components/FormLoader.tsx
Normal file
56
interface/src/components/FormLoader.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
|
||||||
|
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||||
|
import { Button, LinearProgress, Typography } from '@material-ui/core';
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
interface FormLoaderProps {
|
||||||
|
errorMessage?: string;
|
||||||
|
loadData: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, loadData }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classes.loadingSettings}>
|
||||||
|
<LinearProgress className={classes.loadingSettingsDetails} />
|
||||||
|
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||||
|
Loading…
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FormLoader;
|
||||||
@@ -5,6 +5,7 @@ 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 FormLoader } from './FormLoader';
|
||||||
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';
|
||||||
|
|||||||
@@ -1,58 +1,31 @@
|
|||||||
import { Component } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { Features } from './types';
|
|
||||||
import { FeaturesContext } from './FeaturesContext';
|
|
||||||
import FullScreenLoading from '../components/FullScreenLoading';
|
import FullScreenLoading from '../components/FullScreenLoading';
|
||||||
import ApplicationError from '../components/ApplicationError';
|
import ApplicationError from '../components/ApplicationError';
|
||||||
import { FEATURES_ENDPOINT } from '../api';
|
import { FEATURES_ENDPOINT } from '../api';
|
||||||
|
import { useRest } from '../hooks';
|
||||||
|
|
||||||
interface FeaturesWrapperState {
|
import { Features } from './types';
|
||||||
features?: Features;
|
import { FeaturesContext } from './FeaturesContext';
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
const FeaturesWrapper: FC = ({ children }) => {
|
||||||
state: FeaturesWrapperState = {};
|
const { data: features, errorMessage: error } = useRest<Features>({
|
||||||
|
endpoint: FEATURES_ENDPOINT
|
||||||
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
if (features) {
|
||||||
this.fetchFeaturesDetails();
|
return (
|
||||||
|
<FeaturesContext.Provider value={{ features }}>
|
||||||
|
{children}
|
||||||
|
</FeaturesContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchFeaturesDetails = () => {
|
if (error) {
|
||||||
fetch(FEATURES_ENDPOINT)
|
return <ApplicationError error={error} />;
|
||||||
.then((response) => {
|
|
||||||
if (response.status === 200) {
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
throw Error('Unexpected status code: ' + response.status);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((features) => {
|
|
||||||
this.setState({ features });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.setState({ error: error.message });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { features, error } = this.state;
|
|
||||||
if (features) {
|
|
||||||
return (
|
|
||||||
<FeaturesContext.Provider
|
|
||||||
value={{
|
|
||||||
features
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{this.props.children}
|
|
||||||
</FeaturesContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
return <ApplicationError error={error} />;
|
|
||||||
}
|
|
||||||
return <FullScreenLoading />;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return <FullScreenLoading />;
|
||||||
|
};
|
||||||
|
|
||||||
export default FeaturesWrapper;
|
export default FeaturesWrapper;
|
||||||
|
|||||||
2
interface/src/hooks/index.ts
Normal file
2
interface/src/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as useRest } from './useRest';
|
||||||
|
export { default as useAuthorizedRest } from './useAuthorizedRest';
|
||||||
12
interface/src/hooks/useAuthorizedRest.ts
Normal file
12
interface/src/hooks/useAuthorizedRest.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { redirectingAuthorizedFetch } from '../authentication';
|
||||||
|
import useRest, { RestRequestOptions } from './useRest';
|
||||||
|
|
||||||
|
const useAuthorizedRest = <D>({
|
||||||
|
endpoint
|
||||||
|
}: Omit<RestRequestOptions, 'fetchFunction'>) =>
|
||||||
|
useRest<D>({
|
||||||
|
endpoint,
|
||||||
|
fetchFunction: redirectingAuthorizedFetch
|
||||||
|
});
|
||||||
|
|
||||||
|
export default useAuthorizedRest;
|
||||||
79
interface/src/hooks/useRest.ts
Normal file
79
interface/src/hooks/useRest.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
|
export interface RestRequestOptions {
|
||||||
|
endpoint: string;
|
||||||
|
fetchFunction?: typeof fetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useRest = <D>({
|
||||||
|
endpoint,
|
||||||
|
fetchFunction = fetch
|
||||||
|
}: RestRequestOptions) => {
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
|
||||||
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
|
const [data, setData] = useState<D>();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string>();
|
||||||
|
|
||||||
|
const handleError = useCallback(
|
||||||
|
(error: any) => {
|
||||||
|
const errorMessage = error.message || 'Unknown error';
|
||||||
|
enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||||
|
variant: 'error'
|
||||||
|
});
|
||||||
|
setErrorMessage(errorMessage);
|
||||||
|
},
|
||||||
|
[enqueueSnackbar]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
setData(undefined);
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
try {
|
||||||
|
const response = await fetchFunction(endpoint);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw Error('Invalid status code: ' + response.status);
|
||||||
|
}
|
||||||
|
setData(await response.json());
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
}, [handleError, fetchFunction, endpoint]);
|
||||||
|
|
||||||
|
const save = useCallback(
|
||||||
|
async (data: D) => {
|
||||||
|
setSaving(true);
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
try {
|
||||||
|
const response = await fetchFunction(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw Error('Invalid status code: ' + response.status);
|
||||||
|
}
|
||||||
|
enqueueSnackbar('Update successful.', { variant: 'success' });
|
||||||
|
setData(await response.json());
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[enqueueSnackbar, handleError, fetchFunction, endpoint]
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveData = () => data && save(data);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
return { loadData, saveData, saving, setData, data, errorMessage } as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useRest;
|
||||||
33
interface/src/utils/binding.ts
Normal file
33
interface/src/utils/binding.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateValue = <S>(updateEntity: UpdateEntity<S>) => (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
updateEntity((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[event.target.name]: extractEventValue(event)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateBooleanValue = <S>(updateEntity: UpdateEntity<S>) => (
|
||||||
|
name: string,
|
||||||
|
value?: boolean
|
||||||
|
) => {
|
||||||
|
updateEntity((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
1
interface/src/utils/index.ts
Normal file
1
interface/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './binding';
|
||||||
Reference in New Issue
Block a user