Skip to content

Commit

Permalink
feat: custom hook for offline/online status and fallback component (#…
Browse files Browse the repository at this point in the history
…1142)

Adds a custom hook for offline/online status and fallback component
which can be rendered to display connectivity issues.

Also adds example component which can be added to pages to test
functionality. Will need to remove before merging.
  • Loading branch information
PeterBaker0 authored Sep 12, 2024
2 parents fbed98e + f692a1c commit b403889
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 14 deletions.
2 changes: 1 addition & 1 deletion app/src/gui/components/notebook/datagrid_toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import React, {useEffect} from 'react';
import {Divider, Box, Grid, TextField, Button} from '@mui/material';
import {GridToolbarContainer, GridToolbarFilterButton} from '@mui/x-data-grid';
import SearchIcon from '@mui/icons-material/Search';
import {usePrevious} from '../../../utils/custom_hooks';
import {usePrevious} from '../../../utils/customHooks';
interface ToolbarProps {
handleQueryFunction: any;
}
Expand Down
48 changes: 48 additions & 0 deletions app/src/gui/components/ui/ExampleOnlineOnlyComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {useIsOnline} from '../../../utils/customHooks';
import {Typography, Button, Box} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';

interface ExampleOnlineComponentProps {}

const ExampleOnlineComponent = (props: ExampleOnlineComponentProps) => {

Check warning on line 7 in app/src/gui/components/ui/ExampleOnlineOnlyComponent.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

'props' is defined but never used
/**
* Component: ExampleOnlineComponent
* Debugging example component which can be used to test areas of the app
* sensitive to being online. Utilizes the useIsOnline hook which provides a
* fallback component.
*/
const {isOnline, fallback, checkIsOnline} = useIsOnline();

if (!isOnline) {
return fallback;
}

return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
minHeight="100vh"
>
<Typography variant="h4" gutterBottom>
You're online!
</Typography>
<Typography variant="body1" gutterBottom>
This component is visible because you have an active internet
connection.
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<RefreshIcon />}
onClick={checkIsOnline}
style={{marginTop: '16px'}}
>
Check Connection
</Button>
</Box>
);
};

export default ExampleOnlineComponent;
133 changes: 133 additions & 0 deletions app/src/gui/components/ui/OfflineFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import HomeIcon from '@mui/icons-material/Home';
import RefreshIcon from '@mui/icons-material/Refresh';
import SignalWifiConnectedNoInternet4Icon from '@mui/icons-material/SignalWifiConnectedNoInternet4';
import {Box, Button, CircularProgress, Typography} from '@mui/material';
import {useState} from 'react';
import {createUseStyles as makeStyles} from 'react-jss';
import {theme} from '../../themes';

const useStyles = makeStyles({
root: {

Check warning on line 10 in app/src/gui/components/ui/OfflineFallback.tsx

View workflow job for this annotation

GitHub Actions / Build and Test

Insert `··`
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
backgroundColor: theme.palette.background.default,
},
container: {
padding: theme.spacing(4),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[1],
textAlign: 'center',
},
logo: {
width: theme.spacing(12),
height: theme.spacing(12),
marginBottom: theme.spacing(3),
color: theme.palette.text.secondary,
},
title: {
color: theme.palette.text.primary,
marginBottom: theme.spacing(1),
},
subtitle: {
color: theme.palette.text.secondary,
marginBottom: theme.spacing(3),
},
buttonContainer: {
'& > :not(:last-child)': {
marginRight: theme.spacing(2),
},
},
primaryButton: {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
'&:hover': {
backgroundColor: theme.palette.primary.dark,
},
},
secondaryButton: {
color: theme.palette.text.primary,
borderColor: theme.palette.text.primary,
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
},
buttonProgress: {
color: theme.palette.primary.contrastText,
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12,
},
});

export interface OfflineFallbackComponentProps {
/** What should happen when refresh button is pressed? */
onRefresh: () => void;
/** What should happen when return home button is pressed? */
onReturnHome: () => void;
}

/**
* A fallback component to use when part of app is offline.
* @param props controls for buttons in the fallback
* @returns A splashscreen which lays over the given view with an offline logo,
* refresh and return home button.
*/
export const OfflineFallbackComponent = (
props: OfflineFallbackComponentProps
) => {
const classes = useStyles();
const {onRefresh, onReturnHome} = props;
// A fake loading state to reassure user that something happened.
const [loading, setLoading] = useState(false);

// Just show loading for 500ms when button is pressed
const handleRefresh = () => {
setLoading(true);
setTimeout(() => {
setLoading(false);
onRefresh();
}, 500);
};

return (
<Box className={classes.root}>
<Box className={classes.container}>
<SignalWifiConnectedNoInternet4Icon className={classes.logo} />
<Typography variant="h4" component="h1" className={classes.title}>
Uh oh... you're offline.
</Typography>
<Typography variant="subtitle1" className={classes.subtitle}>
This part of the app doesn't work offline.
</Typography>
<Box className={classes.buttonContainer}>
<Button
variant="contained"
className={classes.primaryButton}
startIcon={loading ? null : <RefreshIcon />}
onClick={handleRefresh}
disabled={loading}
>
{loading ? 'Refreshing...' : 'Refresh'}
{loading && (
<CircularProgress size={24} className={classes.buttonProgress} />
)}
</Button>
<Button
variant="outlined"
className={classes.secondaryButton}
startIcon={<HomeIcon />}
onClick={onReturnHome}
>
Return Home
</Button>
</Box>
</Box>
</Box>
);
};
83 changes: 83 additions & 0 deletions app/src/utils/customHooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, {useEffect, useRef, useState} from 'react';
import {useNavigate} from 'react-router';
import * as ROUTES from '../constants/routes';
import {OfflineFallbackComponent} from '../gui/components/ui/OfflineFallback';

export const usePrevious = <T extends {}>(value: T): T | undefined => {
/**
* Capture the previous value of a state variable (useful for functional components
* in place of class-based lifecycle method componentWillUpdate)
*/
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
};

export interface UseIsOnlineResponse {
// does the browser have network connectivity?
isOnline: boolean;
// forcefully recheck online status
checkIsOnline: () => boolean;
// fallback component which can be used when the browser is offline to hide
// components
fallback: React.ReactNode;
}

/**
* Custom hook to provide a remount for offline/online status change. Should be
* used in situations where the app only works online. Provides a fallback
* component.
* @returns offline/online status as well as a fallback component to render over
* the page if offline
*/
export function useIsOnline(): UseIsOnlineResponse {
// online/offline state
const [online, setOnline] = useState(window.navigator.onLine);
// Routing
const navigate = useNavigate();

// Create handlers for window online/offline events and clean up from use effect
useEffect(() => {
function handleOnline() {
setOnline(true);
}
function handleOffline() {
setOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);

/**
* Forcefully checks is online and uses setOnline to prompt remount where
* necessary. Should be redundant given event handlers but might be useful for
* critical manual checkpoints.
* @returns Current online status
*/
const checkIsOnline = () => {
const online = window.navigator.onLine;
setOnline(online);
return online;
};

return {
isOnline: online,
checkIsOnline: checkIsOnline,
fallback: (
<OfflineFallbackComponent
onRefresh={() => {
checkIsOnline();
}}
onReturnHome={() => {
navigate(ROUTES.INDEX);
}}
></OfflineFallbackComponent>
),
};
}
13 changes: 0 additions & 13 deletions app/src/utils/custom_hooks.tsx

This file was deleted.

0 comments on commit b403889

Please sign in to comment.