From ba0f53517ef0089f72acc35b2dcb16c3abe576d2 Mon Sep 17 00:00:00 2001 From: Peter Baker Date: Tue, 10 Sep 2024 09:36:27 +1000 Subject: [PATCH 1/4] Initial commit Signed-off-by: Peter Baker --- .../ui/exampleOnlineOnlyComponent.tsx | 48 +++++ app/src/gui/pages/workspace.tsx | 2 + app/src/utils/custom_hooks.tsx | 193 +++++++++++++++++- 3 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 app/src/gui/components/ui/exampleOnlineOnlyComponent.tsx diff --git a/app/src/gui/components/ui/exampleOnlineOnlyComponent.tsx b/app/src/gui/components/ui/exampleOnlineOnlyComponent.tsx new file mode 100644 index 000000000..551fcf04c --- /dev/null +++ b/app/src/gui/components/ui/exampleOnlineOnlyComponent.tsx @@ -0,0 +1,48 @@ +import {useIsOnline} from '../../../utils/custom_hooks'; +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/pages/workspace.tsx b/app/src/gui/pages/workspace.tsx index a369eaaf6..cdec073bb 100644 --- a/app/src/gui/pages/workspace.tsx +++ b/app/src/gui/pages/workspace.tsx @@ -24,12 +24,14 @@ import Notebooks from '../components/workspace/notebooks'; import Breadcrumbs from '../components/ui/breadcrumbs'; import {NOTEBOOK_NAME_CAPITALIZED} from '../../buildconfig'; import {projectListVerbose} from '../themes'; +import ExampleOnlineComponent from '../components/ui/exampleOnlineOnlyComponent'; export default function Workspace() { const breadcrumbs = [{title: 'Workspace'}]; return ( + {} {projectListVerbose && } diff --git a/app/src/utils/custom_hooks.tsx b/app/src/utils/custom_hooks.tsx index 9b224a808..ec87b3b00 100644 --- a/app/src/utils/custom_hooks.tsx +++ b/app/src/utils/custom_hooks.tsx @@ -1,4 +1,139 @@ -import {useRef, useEffect} from 'react'; +import HomeIcon from '@mui/icons-material/Home'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import {Box, Button, CircularProgress, Typography} from '@mui/material'; +import React, {useEffect, useRef, useState} from 'react'; +import {createUseStyles as makeStyles} from 'react-jss'; +import {useNavigate} from 'react-router'; +import * as ROUTES from '../constants/routes'; + +const useStyles = makeStyles((theme: any) => { + console.log(JSON.stringify(theme)); + return { + root: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + minHeight: '100vh', + backgroundColor: '#f5f5f5', + }, + container: { + padding: '32px', + backgroundColor: '#ffffff', + borderRadius: '8px', + boxShadow: '0 3px 6px rgba(0,0,0,0.1)', + textAlign: 'center', + }, + logo: { + width: 100, + height: 100, + marginBottom: '24px', + filter: 'grayscale(100%)', + }, + title: { + color: '#333333', + marginBottom: '8px', + }, + subtitle: { + color: '#666666', + marginBottom: '24px', + }, + buttonContainer: { + '& > :not(:last-child)': { + marginRight: '16px', + }, + }, + primaryButton: { + backgroundColor: '#4a4a4a', + color: '#ffffff', + '&:hover': { + backgroundColor: '#333333', + }, + }, + secondaryButton: { + color: '#4a4a4a', + borderColor: '#4a4a4a', + '&:hover': { + backgroundColor: 'rgba(74, 74, 74, 0.04)', + }, + }, + buttonProgress: { + color: '#ffffff', + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, + }; +}); + +interface OfflineFallbackComponentProps { + onRefresh: () => void; + onReturnHome: () => void; +} + +const OfflineFallback = (props: OfflineFallbackComponentProps) => { + const classes = useStyles(); + const {onRefresh, onReturnHome} = props; + const [loading, setLoading] = useState(false); + + const handleRefresh = () => { + setLoading(true); + setTimeout(() => { + setLoading(false); + onRefresh(); + }, 500); + }; + + return ( + + + + + + + + Uh oh... you're offline. + + + This part of the app doesn't work offline. + + + + + + + + ); +}; export const usePrevious = (value: T): T | undefined => { /** @@ -11,3 +146,59 @@ export const usePrevious = (value: T): T | undefined => { }); 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; +} + +export function useIsOnline(): UseIsOnlineResponse { + const [online, setOnline] = useState(window.navigator.onLine); + const navigate = useNavigate(); + 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 checks. + * @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); + }} + > + ), + }; +} From 9bce0b21e3a1b55e20e1560b93b1ca47961ac901 Mon Sep 17 00:00:00 2001 From: Peter Baker Date: Tue, 10 Sep 2024 09:58:58 +1000 Subject: [PATCH 2/4] Refactoring component and using prebuilt log Signed-off-by: Peter Baker --- app/src/gui/components/ui/OfflineFallback.tsx | 135 ++++++++++++++++ app/src/utils/custom_hooks.tsx | 151 ++---------------- 2 files changed, 150 insertions(+), 136 deletions(-) create mode 100644 app/src/gui/components/ui/OfflineFallback.tsx diff --git a/app/src/gui/components/ui/OfflineFallback.tsx b/app/src/gui/components/ui/OfflineFallback.tsx new file mode 100644 index 000000000..ec215a940 --- /dev/null +++ b/app/src/gui/components/ui/OfflineFallback.tsx @@ -0,0 +1,135 @@ +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'; + +const useStyles = makeStyles((theme: any) => { + console.log(JSON.stringify(theme)); + return { + root: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + minHeight: '100vh', + backgroundColor: '#f5f5f5', + }, + container: { + padding: '32px', + backgroundColor: '#ffffff', + borderRadius: '8px', + boxShadow: '0 3px 6px rgba(0,0,0,0.1)', + textAlign: 'center', + }, + logo: { + width: 100, + height: 100, + marginBottom: '24px', + color: '#9E9E9E', + }, + title: { + color: '#333333', + marginBottom: '8px', + }, + subtitle: { + color: '#666666', + marginBottom: '24px', + }, + buttonContainer: { + '& > :not(:last-child)': { + marginRight: '16px', + }, + }, + primaryButton: { + backgroundColor: '#4a4a4a', + color: '#ffffff', + '&:hover': { + backgroundColor: '#333333', + }, + }, + secondaryButton: { + color: '#4a4a4a', + borderColor: '#4a4a4a', + '&:hover': { + backgroundColor: 'rgba(74, 74, 74, 0.04)', + }, + }, + buttonProgress: { + color: '#ffffff', + 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/custom_hooks.tsx b/app/src/utils/custom_hooks.tsx index ec87b3b00..080fd18aa 100644 --- a/app/src/utils/custom_hooks.tsx +++ b/app/src/utils/custom_hooks.tsx @@ -1,139 +1,7 @@ -import HomeIcon from '@mui/icons-material/Home'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import {Box, Button, CircularProgress, Typography} from '@mui/material'; import React, {useEffect, useRef, useState} from 'react'; -import {createUseStyles as makeStyles} from 'react-jss'; import {useNavigate} from 'react-router'; import * as ROUTES from '../constants/routes'; - -const useStyles = makeStyles((theme: any) => { - console.log(JSON.stringify(theme)); - return { - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - minHeight: '100vh', - backgroundColor: '#f5f5f5', - }, - container: { - padding: '32px', - backgroundColor: '#ffffff', - borderRadius: '8px', - boxShadow: '0 3px 6px rgba(0,0,0,0.1)', - textAlign: 'center', - }, - logo: { - width: 100, - height: 100, - marginBottom: '24px', - filter: 'grayscale(100%)', - }, - title: { - color: '#333333', - marginBottom: '8px', - }, - subtitle: { - color: '#666666', - marginBottom: '24px', - }, - buttonContainer: { - '& > :not(:last-child)': { - marginRight: '16px', - }, - }, - primaryButton: { - backgroundColor: '#4a4a4a', - color: '#ffffff', - '&:hover': { - backgroundColor: '#333333', - }, - }, - secondaryButton: { - color: '#4a4a4a', - borderColor: '#4a4a4a', - '&:hover': { - backgroundColor: 'rgba(74, 74, 74, 0.04)', - }, - }, - buttonProgress: { - color: '#ffffff', - position: 'absolute', - top: '50%', - left: '50%', - marginTop: -12, - marginLeft: -12, - }, - }; -}); - -interface OfflineFallbackComponentProps { - onRefresh: () => void; - onReturnHome: () => void; -} - -const OfflineFallback = (props: OfflineFallbackComponentProps) => { - const classes = useStyles(); - const {onRefresh, onReturnHome} = props; - const [loading, setLoading] = useState(false); - - const handleRefresh = () => { - setLoading(true); - setTimeout(() => { - setLoading(false); - onRefresh(); - }, 500); - }; - - return ( - - - - - - - - Uh oh... you're offline. - - - This part of the app doesn't work offline. - - - - - - - - ); -}; +import {OfflineFallbackComponent} from '../gui/components/ui/OfflineFallback'; export const usePrevious = (value: T): T | undefined => { /** @@ -157,9 +25,20 @@ export interface UseIsOnlineResponse { 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); @@ -178,7 +57,7 @@ export function useIsOnline(): UseIsOnlineResponse { /** * Forcefully checks is online and uses setOnline to prompt remount where * necessary. Should be redundant given event handlers but might be useful for - * critical checks. + * critical manual checkpoints. * @returns Current online status */ const checkIsOnline = () => { @@ -191,14 +70,14 @@ export function useIsOnline(): UseIsOnlineResponse { isOnline: online, checkIsOnline: checkIsOnline, fallback: ( - { checkIsOnline(); }} onReturnHome={() => { navigate(ROUTES.INDEX); }} - > + > ), }; } From fde27a9174205273a3d32cf8ea04c1cbab3282dd Mon Sep 17 00:00:00 2001 From: Peter Baker Date: Tue, 10 Sep 2024 12:05:56 +1000 Subject: [PATCH 3/4] Refactoring name of custom hook file, and using theme colouring spacing etc Signed-off-by: Peter Baker --- .../components/notebook/datagrid_toolbar.tsx | 2 +- ...ent.tsx => ExampleOnlineOnlyComponent.tsx} | 2 +- app/src/gui/components/ui/OfflineFallback.tsx | 112 +++++++++--------- app/src/gui/pages/workspace.tsx | 2 +- .../{custom_hooks.tsx => customHooks.tsx} | 0 5 files changed, 58 insertions(+), 60 deletions(-) rename app/src/gui/components/ui/{exampleOnlineOnlyComponent.tsx => ExampleOnlineOnlyComponent.tsx} (95%) rename app/src/utils/{custom_hooks.tsx => customHooks.tsx} (100%) 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 similarity index 95% rename from app/src/gui/components/ui/exampleOnlineOnlyComponent.tsx rename to app/src/gui/components/ui/ExampleOnlineOnlyComponent.tsx index 551fcf04c..f0072dd42 100644 --- a/app/src/gui/components/ui/exampleOnlineOnlyComponent.tsx +++ b/app/src/gui/components/ui/ExampleOnlineOnlyComponent.tsx @@ -1,4 +1,4 @@ -import {useIsOnline} from '../../../utils/custom_hooks'; +import {useIsOnline} from '../../../utils/customHooks'; import {Typography, Button, Box} from '@mui/material'; import RefreshIcon from '@mui/icons-material/Refresh'; diff --git a/app/src/gui/components/ui/OfflineFallback.tsx b/app/src/gui/components/ui/OfflineFallback.tsx index ec215a940..a053db847 100644 --- a/app/src/gui/components/ui/OfflineFallback.tsx +++ b/app/src/gui/components/ui/OfflineFallback.tsx @@ -4,67 +4,65 @@ import SignalWifiConnectedNoInternet4Icon from '@mui/icons-material/SignalWifiCo 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((theme: any) => { - console.log(JSON.stringify(theme)); - return { - root: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - minHeight: '100vh', - backgroundColor: '#f5f5f5', +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), }, - container: { - padding: '32px', - backgroundColor: '#ffffff', - borderRadius: '8px', - boxShadow: '0 3px 6px rgba(0,0,0,0.1)', - textAlign: 'center', + }, + primaryButton: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.dark, }, - logo: { - width: 100, - height: 100, - marginBottom: '24px', - color: '#9E9E9E', + }, + secondaryButton: { + color: theme.palette.text.primary, + borderColor: theme.palette.text.primary, + '&:hover': { + backgroundColor: theme.palette.action.hover, }, - title: { - color: '#333333', - marginBottom: '8px', - }, - subtitle: { - color: '#666666', - marginBottom: '24px', - }, - buttonContainer: { - '& > :not(:last-child)': { - marginRight: '16px', - }, - }, - primaryButton: { - backgroundColor: '#4a4a4a', - color: '#ffffff', - '&:hover': { - backgroundColor: '#333333', - }, - }, - secondaryButton: { - color: '#4a4a4a', - borderColor: '#4a4a4a', - '&:hover': { - backgroundColor: 'rgba(74, 74, 74, 0.04)', - }, - }, - buttonProgress: { - color: '#ffffff', - position: 'absolute', - top: '50%', - left: '50%', - marginTop: -12, - marginLeft: -12, - }, - }; + }, + buttonProgress: { + color: theme.palette.primary.contrastText, + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, }); export interface OfflineFallbackComponentProps { diff --git a/app/src/gui/pages/workspace.tsx b/app/src/gui/pages/workspace.tsx index cdec073bb..a9015e937 100644 --- a/app/src/gui/pages/workspace.tsx +++ b/app/src/gui/pages/workspace.tsx @@ -24,7 +24,7 @@ import Notebooks from '../components/workspace/notebooks'; import Breadcrumbs from '../components/ui/breadcrumbs'; import {NOTEBOOK_NAME_CAPITALIZED} from '../../buildconfig'; import {projectListVerbose} from '../themes'; -import ExampleOnlineComponent from '../components/ui/exampleOnlineOnlyComponent'; +import ExampleOnlineComponent from '../components/ui/ExampleOnlineOnlyComponent'; export default function Workspace() { const breadcrumbs = [{title: 'Workspace'}]; diff --git a/app/src/utils/custom_hooks.tsx b/app/src/utils/customHooks.tsx similarity index 100% rename from app/src/utils/custom_hooks.tsx rename to app/src/utils/customHooks.tsx From f692a1ca6e22547878de74a0700698656104ae61 Mon Sep 17 00:00:00 2001 From: Peter Baker Date: Wed, 11 Sep 2024 12:36:53 +1000 Subject: [PATCH 4/4] Remove example online only component Signed-off-by: Peter Baker --- app/src/gui/pages/workspace.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/gui/pages/workspace.tsx b/app/src/gui/pages/workspace.tsx index a9015e937..a369eaaf6 100644 --- a/app/src/gui/pages/workspace.tsx +++ b/app/src/gui/pages/workspace.tsx @@ -24,14 +24,12 @@ import Notebooks from '../components/workspace/notebooks'; import Breadcrumbs from '../components/ui/breadcrumbs'; import {NOTEBOOK_NAME_CAPITALIZED} from '../../buildconfig'; import {projectListVerbose} from '../themes'; -import ExampleOnlineComponent from '../components/ui/ExampleOnlineOnlyComponent'; export default function Workspace() { const breadcrumbs = [{title: 'Workspace'}]; return ( - {} {projectListVerbose && }