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.
+
+ }
+ onClick={checkIsOnline}
+ style={{marginTop: '16px'}}
+ >
+ Check 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.
+
+
+ }
+ onClick={handleRefresh}
+ disabled={loading}
+ >
+ {loading ? 'Refreshing...' : 'Refresh'}
+ {loading && (
+
+ )}
+
+ }
+ onClick={onReturnHome}
+ >
+ Return Home
+
+
+
+
+ );
+};
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;
-};