diff --git a/app/src/gui/components/notebook/datagrid_toolbar.tsx b/app/src/gui/components/notebook/datagrid_toolbar.tsx index 6884efec7..a472eea00 100644 --- a/app/src/gui/components/notebook/datagrid_toolbar.tsx +++ b/app/src/gui/components/notebook/datagrid_toolbar.tsx @@ -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; } diff --git a/app/src/gui/components/ui/ExampleOnlineOnlyComponent.tsx b/app/src/gui/components/ui/ExampleOnlineOnlyComponent.tsx new file mode 100644 index 000000000..f0072dd42 --- /dev/null +++ b/app/src/gui/components/ui/ExampleOnlineOnlyComponent.tsx @@ -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) => { + /** + * 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 ( + + + You're online! + + + This component is visible because you have an active internet + connection. + + + + ); +}; + +export default ExampleOnlineComponent; diff --git a/app/src/gui/components/ui/OfflineFallback.tsx b/app/src/gui/components/ui/OfflineFallback.tsx new file mode 100644 index 000000000..a053db847 --- /dev/null +++ b/app/src/gui/components/ui/OfflineFallback.tsx @@ -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: { + 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 ( + + + + + Uh oh... you're offline. + + + This part of the app doesn't work offline. + + + + + + + + ); +}; diff --git a/app/src/utils/customHooks.tsx b/app/src/utils/customHooks.tsx new file mode 100644 index 000000000..080fd18aa --- /dev/null +++ b/app/src/utils/customHooks.tsx @@ -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 = (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(); + 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: ( + { + checkIsOnline(); + }} + onReturnHome={() => { + navigate(ROUTES.INDEX); + }} + > + ), + }; +} diff --git a/app/src/utils/custom_hooks.tsx b/app/src/utils/custom_hooks.tsx deleted file mode 100644 index 9b224a808..000000000 --- a/app/src/utils/custom_hooks.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import {useRef, useEffect} from 'react'; - -export const usePrevious = (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(); - useEffect(() => { - ref.current = value; - }); - return ref.current; -};