import { useState, useCallback, createContext, Suspense, lazy } from 'react'; // Cloudscape import { AppLayout, Box, BreadcrumbGroup, Flashbar, SideNavigation } from '@cloudscape-design/components'; // Router import { Route, Routes, Navigate, useNavigate } from 'react-router-dom'; // AWS Amplify import { Amplify } from 'aws-amplify'; import { useAuthenticator } from '@aws-amplify/ui-react'; // Utils import dayjs from 'dayjs'; import { isUserAuth } from '../../utils/Auth'; import { nowTime } from '../../utils/DateTime'; import { listDatastores } from '../../utils/AwsHealthImagingApi'; // App import { sideNavItems } from './sideNavItems'; import SuspenseLoader from '../SuspenseLoader'; import TopNav from '../TopNav'; import Welcome from '../Welcome'; import ToolsContent from '../ToolsContent'; // Hooks import { useLocalStorage } from '../../hooks/useLocalStorage'; import { useSettings } from '../../hooks/useSettings'; import Debug from '../Debug'; // Configure AWS Amplify import awsExports from '../../aws-exports'; Amplify.configure(awsExports); // Lazy components const Datastores = lazy(() => import('../Datastores')); const DatastoresDetails = lazy(() => import('../DatastoresDetails')); const Search = lazy(() => import('../Search')); const Metadata = lazy(() => import('../Metadata')); const ImageViewer = lazy(() => import('../ImageViewer')); const Settings = lazy(() => import('../Settings')); // App Context const AppContext = createContext(); export default function App() { const [activeHref, setActiveHref] = useState('/'); // active link for SideNavigation const [activeBreadcrumbs, setActiveBreadcrumbs] = useState([{ text: 'Home', href: '/' }]); // active breadcrumbs for BreadcrumGroup const [flashbarItems, setFlashbaritems] = useState([]); // items for the flashbar const [datastores, setDatastores] = useState([]); // keep datastore state here because it's needed by multiple child components const [datastoreLoadStatus, setDatastoreLoadStatus] = useState({ card: true, // status for Card component: true, false select: 'loading', // status for Select component: pending, loading, finished, error }); // datastore loading status based on component const [toolsOpen, setToolsOpen] = useState(false); // whether the tools drawer is open const [appSettings, setAppSettings] = useSettings(); const [appTheme, setAppTheme] = useLocalStorage('App-Theme', 'theme.light'); // Cognito user context and signOut from AWS Amplify const { user, signOut } = useAuthenticator((context) => [context.user]); // Navigation const navigate = useNavigate(); // Build and set crumbs from a link and text // Text can be a literal string or an array of strings // Returns an array of breadcrumbs (object: text, href) const buildCrumb = useCallback((href, text = null) => { const hrefArray = ['/', ...href.split('/').filter((h) => h !== '')]; // Only set the first two elements of href as the active href if (hrefArray.length <= 2) { setActiveHref(href); } else { setActiveHref(hrefArray.slice(0, 2).join('')); } setActiveBreadcrumbs( hrefArray.map((h, i) => { // build full href from beginning to index const fullHref = hrefArray .slice(0, i + 1) .join('/') .replace('//', '/'); if (h === '/') { return { text: 'Home', href: '/' }; } else if (Array.isArray(text)) { // if crumbText is an array, use it - should be set for crumb depths > 1 return { text: text[i - 1], href: fullHref }; } else { // otherwise use the link text return { text: text, href: fullHref }; } }) ); }, []); // Add flash message // Type can be success, error, warning, info. const addFlashMessage = useCallback(({ id = null, header = null, content, type = 'info', dismissible = true, otherParams }) => { if (!content) return; const idValue = id ? id : `${header} ${content}`; const headerItem = header ? { header } : `${dayjs().format('H:mm')} - ${header}`; const newMessage = { id: idValue, ...headerItem, type: type, content: typeof content === 'string' ? `${nowTime()}: ${content}` : content, dismissible: dismissible, onDismiss: () => setFlashbaritems((items) => items.filter((item) => item.id !== idValue)), ...otherParams, }; setFlashbaritems((currentMessages) => [...currentMessages, newMessage]); }, []); // Get datastores const getDatastores = useCallback(async () => { try { setDatastoreLoadStatus({ card: true, select: 'loading', }); const datastoreResult = await listDatastores(); const sortedDatastoreResult = datastoreResult.data?.datastoreSummaries?.sort((a, b) => a.createdAt - b.createdAt); setDatastores(sortedDatastoreResult); setDatastoreLoadStatus({ card: false, select: 'finished', }); return sortedDatastoreResult; } catch (e) { setDatastoreLoadStatus({ card: false, select: 'error', }); addFlashMessage({ content: `Unable to retrieve datastores: ${e.message}. See debug console for more information.`, type: 'error', }); } }, [addFlashMessage]); // Navigation // * if external URL, open in a new window and return // * set active href to event for SideNavigation // * update breadcrumbs // * navigate to url function onNavigate(e) { e.preventDefault(); if (e.detail.external === true) { window.open(e.detail.href, '_blank', 'noopener'); return; } if (e.detail.href === '/') buildCrumb(e.detail.href, e.detail.text); navigate(e.detail.href); } // Set default context values const appContextValue = { appTheme: appTheme, user: user, buildCrumb: buildCrumb, addFlashMessage: addFlashMessage, setToolsOpen: setToolsOpen, datastores: datastores, datastoreLoadStatus: datastoreLoadStatus, getDatastores: getDatastores, appSettings: appSettings, }; return ( <AppContext.Provider value={appContextValue}> <TopNav signOut={signOut} setAppTheme={setAppTheme} /> <AppLayout tools={<ToolsContent />} toolsOpen={toolsOpen} onToolsChange={({ detail }) => setToolsOpen(detail.open)} notifications={ <Box> <Flashbar items={flashbarItems} /> </Box> } breadcrumbs={<BreadcrumbGroup onFollow={onNavigate} items={activeBreadcrumbs} />} navigation={<SideNavigation activeHref={activeHref} onFollow={(e) => onNavigate(e)} items={sideNavItems} />} content={ <Suspense fallback={<SuspenseLoader />}> {isUserAuth(user) ? ( <Routes> <Route index element={<Welcome />} /> <Route path="/debug" element={<Debug />} /> <Route path="/datastores" element={<Datastores />} /> <Route path="/datastores/:datastoreId/*" element={<DatastoresDetails />} /> <Route path="/search" element={<Search />} /> <Route path="/metadata" element={<Metadata />} /> <Route path="/metadata/edit" element={<Metadata />} /> <Route path="/viewer" element={<ImageViewer />} /> <Route path="/settings" element={<Settings setAppSettings={setAppSettings} />} /> <Route path="*" element={<Navigate to="/" replace />} /> </Routes> ) : ( <Routes> <Route path="*" element={<Welcome />} /> </Routes> )} </Suspense> } /> </AppContext.Provider> ); } export { AppContext };