diff --git a/web_ui/frontend/app/(landing)/page.tsx b/web_ui/frontend/app/(landing)/page.tsx index 014bd919d..c75db6247 100644 --- a/web_ui/frontend/app/(landing)/page.tsx +++ b/web_ui/frontend/app/(landing)/page.tsx @@ -19,33 +19,21 @@ 'use client'; import React, { useState, useEffect } from 'react'; -import { Box, Container, Grid, Skeleton, Typography } from '@mui/material'; +import { + Box, + Container, + Grid, + List, + ListItemButton, + ListItemText, + Skeleton, + Typography, +} from '@mui/material'; import Link from 'next/link'; import useSWR from 'swr'; import { getEnabledServers } from '@/helpers/util'; import { ServerType } from '@/index'; -function TextCenteredBox({ text }: { text: string }) { - return ( - - - - {text} - - - - ); -} - export default function Home() { const { data: enabledServers, isLoading } = useSWR( 'getEnabledServers', @@ -56,30 +44,26 @@ export default function Home() { - Pelican Services + Active Pelican Services - - {isLoading && ( - - - - )} + {enabledServers && enabledServers.map((service) => { return ( - - - - - + + + ); })} - + ); diff --git a/web_ui/frontend/app/(login)/components/CodeInput.tsx b/web_ui/frontend/app/(login)/components/CodeInput.tsx index e625645bf..0a2b4330d 100644 --- a/web_ui/frontend/app/(login)/components/CodeInput.tsx +++ b/web_ui/frontend/app/(login)/components/CodeInput.tsx @@ -140,9 +140,9 @@ export default function CodeInput({ +
- - {children} - +
+ {children} +
); } diff --git a/web_ui/frontend/app/(login)/login/page.tsx b/web_ui/frontend/app/(login)/login/page.tsx index a597ca523..2f8da6f24 100644 --- a/web_ui/frontend/app/(login)/login/page.tsx +++ b/web_ui/frontend/app/(login)/login/page.tsx @@ -108,12 +108,8 @@ const AdminLogin = () => {
{ setPassword(e.target.value); }, @@ -143,15 +139,13 @@ const AdminLogin = () => { ) { return ( - - - + {LoginComponent} ); @@ -205,39 +199,46 @@ export default function Home() { Login - + Administer your Pelican Platform - - {serverIntersect && - (serverIntersect.includes('registry') || - serverIntersect.includes('origin') || - serverIntersect.includes('cache') || - serverIntersect.includes('director')) && ( - <> - - - - - )} - {serverIntersect && } - {!serverIntersect && ( - + {serverIntersect && + (serverIntersect.includes('registry') || + serverIntersect.includes('origin') || + serverIntersect.includes('cache') || + serverIntersect.includes('director')) && ( + <> + + + + )} - + {serverIntersect && } + {!serverIntersect && ( + + )} ); diff --git a/web_ui/frontend/app/cache/layout.tsx b/web_ui/frontend/app/cache/layout.tsx index 197b4f6e9..6981bb4f8 100644 --- a/web_ui/frontend/app/cache/layout.tsx +++ b/web_ui/frontend/app/cache/layout.tsx @@ -16,17 +16,12 @@ * ***************************************************************/ -import { Box, Tooltip } from '@mui/material'; +import { Box } from '@mui/material'; -import { ButtonLink, Sidebar } from '@/components/layout/Sidebar'; -import Link from 'next/link'; -import Image from 'next/image'; -import PelicanLogo from '@/public/static/images/PelicanPlatformLogo_Icon.png'; -import IconButton from '@mui/material/IconButton'; -import BuildIcon from '@mui/icons-material/Build'; import Main from '@/components/layout/Main'; import { PaddedContent } from '@/components/layout'; -import { Dashboard, MapOutlined } from '@mui/icons-material'; +import { Navigation } from '@/components/layout/Navigation'; +import NavigationConfiguration from '@/app/navigation'; export const metadata = { title: 'Pelican Cache', @@ -39,18 +34,10 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - - - - - - - +
{children}
-
+ ); } diff --git a/web_ui/frontend/app/config/Config.tsx b/web_ui/frontend/app/config/Config.tsx index bfd42fbde..4bdb551dd 100644 --- a/web_ui/frontend/app/config/Config.tsx +++ b/web_ui/frontend/app/config/Config.tsx @@ -45,7 +45,6 @@ import { import useSWR from 'swr'; import { merge, isMatch, isEqual } from 'lodash'; import * as yaml from 'js-yaml'; -import { ButtonLink, Sidebar } from '@/components/layout/Sidebar'; import { Main } from '@/components/layout/Main'; import { submitConfigChange } from '@/components/configuration/util'; import { @@ -79,13 +78,6 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) { async () => await alertOnError(getConfigJson, 'Could not get config', dispatch) ); - const { data: enabledServers } = useSWR( - 'getEnabledServers', - getEnabledServers, - { - fallbackData: ['origin', 'registry', 'director', 'cache'], - } - ); const serverConfig = useMemo(() => { return flattenObject(data || {}); @@ -107,121 +99,85 @@ function Config({ metadata }: { metadata: ParameterMetadataRecord }) { }, [serverConfig, patch]); return ( - <> - - {enabledServers && enabledServers.includes('origin') && ( - - - - )} - {enabledServers && enabledServers.includes('director') && ( - - - - )} - {enabledServers && enabledServers.includes('registry') && ( - - - - )} - {enabledServers && enabledServers.includes('cache') && ( - - - - )} - -
- - - - - Configuration - {serverConfig && ( - - - - - - )} - + + + + Configuration + {serverConfig && ( + + + + - - - - {error && ( - - {(error as Error).message} - - )} - - - - - - - } - /> - {status && } - - - - - - - - - -
- + // Refresh the page after 3 seconds + setTimeout(() => { + mutate(); + setStatus(undefined); + _setPatch({}); + }, 3000); + } catch (e) { + setStatus({ + severity: 'error', + message: (e as string).toString(), + }); + } + }} + > + Save + + + + } + /> + {status && } + + + + + + + + ); } diff --git a/web_ui/frontend/app/config/layout.tsx b/web_ui/frontend/app/config/layout.tsx index ef2330f35..8342507e6 100644 --- a/web_ui/frontend/app/config/layout.tsx +++ b/web_ui/frontend/app/config/layout.tsx @@ -17,6 +17,8 @@ ***************************************************************/ import { Box } from '@mui/material'; +import { PaddedContent, Main } from '@/components/layout'; +import { Navigation } from '@/components/layout/Navigation'; export const metadata = { title: 'Pelican Configuration', @@ -29,8 +31,10 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - {children} - + +
+ {children} +
+
); } diff --git a/web_ui/frontend/app/director/layout.tsx b/web_ui/frontend/app/director/layout.tsx index 55e01cd65..f66b6cb26 100644 --- a/web_ui/frontend/app/director/layout.tsx +++ b/web_ui/frontend/app/director/layout.tsx @@ -17,11 +17,9 @@ ***************************************************************/ import { Box } from '@mui/material'; -import { ButtonLink, Sidebar } from '@/components/layout/Sidebar'; -import BuildIcon from '@mui/icons-material/Build'; import Main from '@/components/layout/Main'; -import { Block, Dashboard, Equalizer, MapOutlined } from '@mui/icons-material'; -import AuthenticatedContent from '@/components/layout/AuthenticatedContent'; +import { Navigation } from '@/components/layout/Navigation'; +import NavigationConfiguration from '@/app/navigation'; export const metadata = { title: 'Pelican Director', @@ -34,26 +32,8 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - - - - - - - - - - - - - - - - - - +
{children}
-
+ ); } diff --git a/web_ui/frontend/app/director/metrics/page.tsx b/web_ui/frontend/app/director/metrics/page.tsx index 52590a59a..1dbd515b0 100644 --- a/web_ui/frontend/app/director/metrics/page.tsx +++ b/web_ui/frontend/app/director/metrics/page.tsx @@ -19,7 +19,7 @@ const Page = () => { redirect={true} > - + {[ , @@ -31,56 +31,62 @@ const Page = () => { ))} - + - + - - - - - - - - - - - - - - - - - - - - + {[ + , + , + , + , + ].map((component, index) => ( + + {component} + + ))} - - + + + + + + - + {[ { color={green[300]} />, ].map((component, index) => ( - + {component} ))} diff --git a/web_ui/frontend/app/layout.tsx b/web_ui/frontend/app/layout.tsx index 10f5ca8b3..43cd1ec30 100644 --- a/web_ui/frontend/app/layout.tsx +++ b/web_ui/frontend/app/layout.tsx @@ -20,7 +20,6 @@ import { LocalizationProvider } from '@/clientComponents'; import { ThemeProviderClient } from '@/components/ThemeProvider'; import { AlertProvider } from '@/components/AlertProvider'; import './globals.css'; - export const metadata = { title: 'Pelican Platform', description: 'Software designed to make data distribution easy', diff --git a/web_ui/frontend/app/navigation.tsx b/web_ui/frontend/app/navigation.tsx new file mode 100644 index 000000000..2119b3150 --- /dev/null +++ b/web_ui/frontend/app/navigation.tsx @@ -0,0 +1,97 @@ +import { + Add, + Block, + Build, + Dashboard, + Equalizer, + FolderOpen, + Lock, + MapOutlined, + Public, + Storage, + TripOrigin, + AssistantDirection, + AppRegistration, + Cached, +} from '@mui/icons-material'; +import { NavigationConfiguration } from '@/components/layout/Navigation'; + +const NavigationConfig: NavigationConfiguration = { + registry: [ + { title: 'Dashboard', href: '/registry/', icon: }, + { + title: 'Denied Namespaces', + href: '/registry/denied/', + icon: , + allowedRoles: ['admin'], + }, + { + title: 'Add', + icon: , + allowedRoles: ['admin'], + children: [ + { + title: 'Namespace', + href: '/registry/namespace/register/', + icon: , + }, + { + title: 'Origin', + href: '/registry/origin/register/', + icon: , + }, + { + title: 'Cache', + href: '/registry/cache/register/', + icon: , + }, + ], + }, + { + title: 'Config', + href: '/config/', + icon: , + allowedRoles: ['admin'], + }, + ], + origin: [ + { title: 'Dashboard', href: '/origin/', icon: }, + { title: 'Metrics', href: '/origin/metrics/', icon: }, + { + title: 'Globus Configurations', + href: '/origin/globus/', + icon: , + allowedExportTypes: ['globus'], + }, + { title: 'Issuer', href: '/origin/issuer', icon: }, + { title: 'Config', href: '/config/', icon: }, + ], + director: [ + { title: 'Dashboard', href: '/director/', icon: }, + { + title: 'Metrics', + href: '/director/metrics/', + icon: , + allowedRoles: ['admin'], + }, + { title: 'Map', href: '/director/map/', icon: }, + { + title: 'Config', + href: '/config/', + icon: , + allowedRoles: ['admin'], + }, + ], + cache: [ + { title: 'Dashboard', href: '/cache/', icon: }, + { title: 'Config', href: '/config/', icon: }, + ], + shared: [ + { title: 'Origin', href: '/origin/', icon: }, + { title: 'Director', href: '/director/', icon: }, + { title: 'Registry', href: '/registry/', icon: }, + { title: 'Cache', href: '/cache/', icon: }, + ], +}; + +export default NavigationConfig; diff --git a/web_ui/frontend/app/origin/layout.tsx b/web_ui/frontend/app/origin/layout.tsx index 6ce8e929f..dffdb4483 100644 --- a/web_ui/frontend/app/origin/layout.tsx +++ b/web_ui/frontend/app/origin/layout.tsx @@ -18,8 +18,9 @@ import { Box } from '@mui/material'; import Main from '@/components/layout/Main'; -import { OriginSidebar } from '@/components/layout/OriginSidebar'; import { PaddedContent } from '@/components/layout'; +import { Navigation } from '@/components/layout/Navigation'; +import NavigationConfiguration from '@/app/navigation'; export const metadata = { title: 'Pelican Origin', @@ -32,11 +33,10 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - +
{children}
-
+ ); } diff --git a/web_ui/frontend/app/origin/metrics/page.tsx b/web_ui/frontend/app/origin/metrics/page.tsx index fb1d91cce..8810ca95f 100644 --- a/web_ui/frontend/app/origin/metrics/page.tsx +++ b/web_ui/frontend/app/origin/metrics/page.tsx @@ -17,7 +17,7 @@ import { StorageGraph } from '@/app/origin/metrics/components/StorageGraph'; const Page = () => { return ( - + { ))} - + { - + - + { color={green[300]} /> - + { color={blue[200]} /> - + { - + - + { color={green[300]} /> - + { color={green[300]} /> - + , - text: 'Namespace', - title: 'Register a new Namespace', - }, - { - href: '/registry/origin/register/', - icon: , - text: 'Origin', - title: 'Register a new Origin', - }, - { - href: '/registry/cache/register/', - icon: , - text: 'Cache', - title: 'Register a new Cache', - }, - ]; - return ( - - - - - - - - - - - - - - - - - - - +
{children}
-
+ ); } diff --git a/web_ui/frontend/components/DataExportTable.tsx b/web_ui/frontend/components/DataExportTable.tsx index a9a46748c..870bbe786 100644 --- a/web_ui/frontend/components/DataExportTable.tsx +++ b/web_ui/frontend/components/DataExportTable.tsx @@ -35,7 +35,7 @@ type ExportResCommon = { editUrl: string; }; -type ExportRes = ExportResCommon & +export type ExportRes = ExportResCommon & ( | { type: 's3'; exports: S3ExportEntry[] } | { type: 'posix'; exports: PosixExportEntry[] } @@ -167,7 +167,7 @@ export const PosixDataExportCard = ({ entry }: { entry: PosixExportEntry }) => { {entry.status != 'Completed' && } - + { label={'Sentinel Location'} /> - + @@ -191,14 +191,14 @@ export const S3DataExportCard = ({ entry }: { entry: S3ExportEntry }) => { {entry.status != 'Completed' && } - + - + @@ -215,7 +215,7 @@ export const GlobusDataExportCard = ({ {entry.status != 'Completed' && } - + { + const size = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')) + ? 'small' + : 'medium'; + const dispatch = useContext(AlertDispatchContext); const ref = useRef(null); const [transition, setTransition] = useState(false); const { mutate } = useSWRConfig(); + return ( <> @@ -50,9 +57,24 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { bgcolor={'secondary'} onClick={() => setTransition(!transition)} > - + - {namespace.prefix} + + {namespace.prefix} + @@ -62,13 +84,14 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { - + @@ -80,8 +103,9 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { e.stopPropagation()} sx={{ mx: 1 }} + size={size} > - + @@ -93,14 +117,16 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { > e.stopPropagation()} + size={size} > - + { e.stopPropagation(); @@ -115,7 +141,7 @@ export const Card = ({ namespace, authenticated, onUpdate }: CardProps) => { } }} > - + diff --git a/web_ui/frontend/components/Namespace/PendingCard.tsx b/web_ui/frontend/components/Namespace/PendingCard.tsx index 1b55f43d9..e99451d5c 100644 --- a/web_ui/frontend/components/Namespace/PendingCard.tsx +++ b/web_ui/frontend/components/Namespace/PendingCard.tsx @@ -3,6 +3,7 @@ import { Authenticated, secureFetch } from '@/helpers/login'; import { Avatar, Box, IconButton, Tooltip, Typography } from '@mui/material'; import { Block, Check, Edit, Person } from '@mui/icons-material'; import Link from 'next/link'; +import { useMediaQuery } from '@mui/material'; import { Alert, RegistryNamespace } from '@/index'; import InformationDropdown from './InformationDropdown'; @@ -11,6 +12,7 @@ import { User } from '@/index'; import { alertOnError } from '@/helpers/util'; import { AlertDispatchContext } from '@/components/AlertProvider'; import { approveNamespace, denyNamespace } from '@/helpers/api'; +import { Theme } from '@mui/system'; export interface PendingCardProps { namespace: RegistryNamespace; @@ -25,6 +27,10 @@ export const PendingCard = ({ onAlert, authenticated, }: PendingCardProps) => { + const size = useMediaQuery((theme: Theme) => theme.breakpoints.down('md')) + ? 'small' + : 'medium'; + const ref = useRef(null); const [transition, setTransition] = useState(false); @@ -48,9 +54,24 @@ export const PendingCard = ({ bgcolor={'secondary'} onClick={() => setTransition(!transition)} > - + - {namespace.prefix} + + {namespace.prefix} + @@ -59,9 +80,15 @@ export const PendingCard = ({ - + @@ -71,6 +98,7 @@ export const PendingCard = ({ { e.stopPropagation(); @@ -82,12 +110,13 @@ export const PendingCard = ({ onUpdate(); }} > - + { e.stopPropagation(); @@ -99,7 +128,7 @@ export const PendingCard = ({ onUpdate(); }} > - + @@ -112,8 +141,9 @@ export const PendingCard = ({ > e.stopPropagation()} + size={size} > - + diff --git a/web_ui/frontend/components/NamespaceCapabilitiesTable.tsx b/web_ui/frontend/components/NamespaceCapabilitiesTable.tsx index 0ba7ae659..4470973a0 100644 --- a/web_ui/frontend/components/NamespaceCapabilitiesTable.tsx +++ b/web_ui/frontend/components/NamespaceCapabilitiesTable.tsx @@ -41,14 +41,14 @@ export const NamespaceCapabilitiesTable = ({ borderRadius={1} > - + Namespace Capabilities - + @@ -61,7 +61,7 @@ export const NamespaceCapabilitiesTable = ({ - + - + - + {server.type}'s Namespace Capabilities - + @@ -52,14 +52,14 @@ export const ServerCapabilitiesTable = ({ - + {namespace.path} - + { fontSize: '1.2rem', my: 'auto', ml: 1, + overflow: 'hidden', + textOverflow: 'ellipsis', }} > {name} diff --git a/web_ui/frontend/components/graphs/GraphOverlay.tsx b/web_ui/frontend/components/graphs/GraphOverlay.tsx index 55b662adc..b7187965b 100644 --- a/web_ui/frontend/components/graphs/GraphOverlay.tsx +++ b/web_ui/frontend/components/graphs/GraphOverlay.tsx @@ -15,6 +15,7 @@ import { MenuItem, Select, Typography, + Grid, } from '@mui/material'; import { DateTimePicker } from '@mui/x-date-pickers'; import { KeyboardArrowLeft, KeyboardArrowRight } from '@mui/icons-material'; @@ -81,18 +82,20 @@ export const GraphOverlay = ({ children }: { children: ReactNode }) => { return ( <> - - - {graphStart.toFormat(format)} - {graphContext.time.toFormat('f')} - - - - - + + + + + {graphStart.toFormat(format)} - {graphContext.time.toFormat('f')} + + + + + + + + + {children} @@ -176,7 +179,10 @@ const DateTimePickerWithArrows = () => { onClick={() => { dispatch({ type: 'decrementTimeByRange' }); }} - sx={{ height: '100%' }} + sx={{ + height: '100%', + display: { xs: 'none', md: 'flex' }, + }} > @@ -196,7 +202,10 @@ const DateTimePickerWithArrows = () => { onClick={() => { dispatch({ type: 'incrementTimeByRange' }); }} - sx={{ height: '100%' }} + sx={{ + height: '100%', + display: { xs: 'none', md: 'flex' }, + }} > diff --git a/web_ui/frontend/components/layout/Main.tsx b/web_ui/frontend/components/layout/Main.tsx index 44b528807..c172c8835 100644 --- a/web_ui/frontend/components/layout/Main.tsx +++ b/web_ui/frontend/components/layout/Main.tsx @@ -6,7 +6,7 @@ export const Main = ({ children }: { children: ReactNode }) => { + + + + + setNavOpen(!navOpen)} + > + + + + {'Pelican + + Pelican +
+ Platform +
+ + + +
+
+
+ + + setNavOpen(false)} + /> + + + + ); +} +export default ResponsiveAppBar; diff --git a/web_ui/frontend/components/layout/Navigation/AppBar/BurgerMenu.tsx b/web_ui/frontend/components/layout/Navigation/AppBar/BurgerMenu.tsx new file mode 100644 index 000000000..5bc0f9a4b --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/AppBar/BurgerMenu.tsx @@ -0,0 +1,154 @@ +import React, { useState } from 'react'; +import { + List, + ListItem, + ListItemText, + Collapse, + IconButton, + Box, +} from '@mui/material'; +import { + Api, + BugReport, + Description, + GitHub, + HelpOutline, +} from '@mui/icons-material'; +import { + NavigationItemProps, + NavigationProps, + StaticNavigationBaseItemProps, + StaticNavigationItemProps, +} from '@/components/layout/Navigation/index'; +import Container from '@mui/material/Container'; +import Toolbar from '@mui/material/Toolbar'; +import Image from 'next/image'; +import PelicanLogo from '@/public/static/images/PelicanPlatformLogo_Icon.png'; +import Typography from '@mui/material/Typography'; +import UserMenu from '@/components/layout/Navigation/Sidebar/UserMenu'; +import AppBar from '@mui/material/AppBar'; +import { Close } from '@mui/icons-material'; +import { NavigationItem } from '@/components/layout/Navigation/AppBar/NavigationItem'; +import { getVersionNumber } from '@/components/layout/Navigation/Sidebar/AboutMenu'; + +const helpMenu: StaticNavigationItemProps[] = [ + { + icon: , + title: 'Help', + children: [ + { + icon: , + title: 'Documentation', + href: 'https://docs.pelicanplatform.org', + }, + { + icon: , + title: 'Pelican Server API', + href: '/api/v1.0/docs', + }, + { + icon: , + title: () => `Release ${getVersionNumber()}`, + href: () => + `https://github.com/PelicanPlatform/pelican/releases/tag/v${getVersionNumber()}`, + }, + { + icon: , + title: 'Report Bug', + href: 'https://github.com/PelicanPlatform/pelican/issues/new', + }, + ], + }, +]; + +type BurgerMenuProps = { onClose: () => void } & NavigationProps; + +const BurgerMenu: React.FC = ({ + config, + exportType, + role, + onClose, +}) => { + return ( + + + + + + + + + + {'Pelican + + Pelican +
+ Platform +
+ + + +
+
+
+ + {[...config, ...helpMenu].map((item, index) => ( + + ))} + +
+ ); +}; + +export default BurgerMenu; diff --git a/web_ui/frontend/components/layout/Navigation/AppBar/NavigationItem.tsx b/web_ui/frontend/components/layout/Navigation/AppBar/NavigationItem.tsx new file mode 100644 index 000000000..774a10389 --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/AppBar/NavigationItem.tsx @@ -0,0 +1,135 @@ +/** + * AppBar equivalent for the NavigationItem component + * Uses List component to render the navigation items + */ + +import { useState } from 'react'; +import { + Box, + Collapse, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Skeleton, +} from '@mui/material'; +import Link from 'next/link'; +import { + NavigationItemProps, + NavigationProps, + StaticNavigationChildItemProps, + StaticNavigationItemProps, + StaticNavigationParentItemProps, +} from '@/components/layout/Navigation'; +import { ExportRes } from '@/components/DataExportTable'; +import { evaluateOrReturn } from '@/helpers/util'; + +export const NavigationItem = ({ + exportType, + role, + config, + onClose, +}: { onClose: () => void } & NavigationItemProps) => { + // If the role or export has yet to propogate, show a skeleton + if ( + (config?.allowedRoles && role === undefined) || + (config?.allowedExportTypes && exportType === undefined) + ) { + return ; + } + + // If the role or export is not allowed, return null + if ( + (config?.allowedRoles && !config.allowedRoles.includes(role)) || + (config?.allowedExportTypes && + !config.allowedExportTypes.includes(exportType as ExportRes['type'])) + ) { + return null; + } + + // If the item has children, render a menu + if ('children' in config) { + return ; + } + + // Otherwise, render the navigation item + return ; +}; + +const NavigationChildItem = ({ + title, + href, + icon, + onClose, +}: { onClose: () => void } & StaticNavigationChildItemProps) => { + return ( + + + {icon} + + + + ); +}; + +const NavigationItemSkeleton = () => { + return ( + + + + + + ); +}; + +const NavigationMenu = ({ + onClose, + config, +}: { + onClose: () => void; + config: StaticNavigationParentItemProps; +}) => { + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(!open)} + style={{ backgroundColor: open ? '#d1f4ff' : 'inherit' }} + > + {config.icon} + + + + + + {config.children.map((config) => ( + + ))} + + + + + ); +}; + +const NavigationMenuItem = ({ + onClose, + config, +}: { + onClose: () => void; + config: StaticNavigationItemProps; +}) => { + // If this item has children, render a nested menu + if ('children' in config) { + return ; + } + + // Otherwise, render the navigation item + return ; +}; diff --git a/web_ui/frontend/components/layout/Navigation/AppBar/index.ts b/web_ui/frontend/components/layout/Navigation/AppBar/index.ts new file mode 100644 index 000000000..7a331d484 --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/AppBar/index.ts @@ -0,0 +1 @@ +export { default as AppBar } from './AppBar'; diff --git a/web_ui/frontend/components/layout/Navigation/Navigation.tsx b/web_ui/frontend/components/layout/Navigation/Navigation.tsx new file mode 100644 index 000000000..8254912d4 --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/Navigation.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { StaticNavigationItemProps } from '@/components/layout/Navigation/index'; +import useSWR from 'swr'; +import { getExportData } from '@/components/DataExportTable'; +import { getUser } from '@/helpers/login'; +import { Sidebar } from '@/components/layout/Navigation/Sidebar'; +import NavigationConfig from '@/app/navigation'; +import { getEnabledServers } from '@/helpers/util'; +import { Box } from '@mui/material'; +import { AppBar } from '@/components/layout/Navigation/AppBar'; +import { ReactNode } from 'react'; + +const Navigation = ({ + children, + config, + sharedPage, +}: { + children: ReactNode; + config?: StaticNavigationItemProps[]; + sharedPage?: boolean; +}) => { + // Check either config or sharedPage is defined but not both + if ((config && sharedPage) || (!config && !sharedPage)) { + throw new Error('Either config xor sharedPage must be defined'); + } + + const { data: exports } = useSWR('getDataExport', getExportData); + const { data: user } = useSWR('getUser', getUser); + const { data: servers } = useSWR('getServers', getEnabledServers); + + // Handle navigation for shared pages + // Best we can do is sending them to root if there are many running servers + // If there is just one then we can render that navigation + if (sharedPage) { + const multipleServersActive = servers && servers.length > 1; + if (multipleServersActive) { + config = NavigationConfig['shared']; + } else { + config = NavigationConfig[servers ? servers[0] : 'shared']; + } + } + + console.log(user); + + return ( + <> + + + + + + + + {children} + + + ); +}; + +export { Navigation }; diff --git a/web_ui/frontend/components/layout/Sidebar/AboutMenu.tsx b/web_ui/frontend/components/layout/Navigation/Sidebar/AboutMenu.tsx similarity index 95% rename from web_ui/frontend/components/layout/Sidebar/AboutMenu.tsx rename to web_ui/frontend/components/layout/Navigation/Sidebar/AboutMenu.tsx index cdaf1d20e..4d2b21ab1 100644 --- a/web_ui/frontend/components/layout/Sidebar/AboutMenu.tsx +++ b/web_ui/frontend/components/layout/Navigation/Sidebar/AboutMenu.tsx @@ -21,8 +21,7 @@ import { } from '@mui/icons-material'; import useSWR from 'swr'; -import { evaluateOrReturn, getEnabledServers } from '@/helpers/util'; -import { ServerType } from '@/index'; +import { evaluateOrReturn } from '@/helpers/util'; const AboutMenu = () => { const [open, setOpen] = useState(false); @@ -177,7 +176,7 @@ const actions: MenuItemProps[] = [ ]; export const getVersionNumber = () => { - const { version } = require('../../../package.json'); + const { version } = require('../../../../package.json'); return version; }; diff --git a/web_ui/frontend/components/layout/Navigation/Sidebar/Menu.tsx b/web_ui/frontend/components/layout/Navigation/Sidebar/Menu.tsx new file mode 100644 index 000000000..683a67225 --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/Sidebar/Menu.tsx @@ -0,0 +1,90 @@ +'use client'; + +import React, { ReactNode, useMemo, useRef, useState } from 'react'; +import { + BoxProps, + IconButton, + Menu, + ListItemIcon, + MenuItem, + ListItemText, +} from '@mui/material'; + +import { + StaticNavigationItemProps, + StaticNavigationParentItemProps, +} from '@/components/layout/Navigation'; +import Link from 'next/link'; +import { evaluateOrReturn } from '@/helpers/util'; + +const NavigationMenu = ({ + config, +}: { + config: StaticNavigationParentItemProps; +}) => { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + const buttonId = `${config.title}-menu-button`; + const menuId = `${config.title}-menu`; + + return ( + <> + setOpen(!open)} + sx={{ mt: 1 }} + > + {config.icon} + + setOpen(false)} + onClick={() => setOpen(false)} + anchorOrigin={{ + vertical: 'center', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'center', + horizontal: 'left', + }} + > + {config.children.map((config) => ( + + ))} + + + ); +}; + +const NavigationMenuItem = ({ + config, +}: { + config: StaticNavigationItemProps; +}) => { + // If this item has children, render a menu + if ('children' in config) { + return ; + } + + // Otherwise, render the navigation item + return ( + + + {config.icon} + + + + ); +}; + +export default NavigationMenu; diff --git a/web_ui/frontend/components/layout/Navigation/Sidebar/NavigationItem.tsx b/web_ui/frontend/components/layout/Navigation/Sidebar/NavigationItem.tsx new file mode 100644 index 000000000..3c1eda6ea --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/Sidebar/NavigationItem.tsx @@ -0,0 +1,77 @@ +/** + * Navigation Component for Pelican Sidebar + */ +import { Box, Button, Skeleton, Tooltip } from '@mui/material'; +import Link from 'next/link'; +import IconButton from '@mui/material/IconButton'; +import { Air } from '@mui/icons-material'; +import { + NavigationItemProps, + StaticNavigationChildItemProps, + StaticNavigationParentItemProps, +} from '@/components/layout/Navigation'; +import { ExportRes } from '@/components/DataExportTable'; +import NavigationMenu from '@/components/layout/Navigation/Sidebar/Menu'; +import { evaluateOrReturn } from '@/helpers/util'; + +export const NavigationItem = ({ + exportType, + role, + config, +}: NavigationItemProps) => { + // If the role or export has yet to propogate, show a skeleton + if ( + (config?.allowedRoles && role === undefined) || + (config?.allowedExportTypes && exportType === undefined) + ) { + return ; + } + + // If the role or export is not allowed, return null + if ( + (config?.allowedRoles && !config.allowedRoles.includes(role)) || + (config?.allowedExportTypes && + !config.allowedExportTypes.includes(exportType as ExportRes['type'])) + ) { + return null; + } + + // If the item has children, render a menu + if ('children' in config) { + return ; + } + + // Otherwise, render the navigation item + return ; +}; + +const NavigationChildItem = ({ + title, + href, + icon, + showTitle, +}: StaticNavigationChildItemProps) => { + return ( + + + + {showTitle ? ( + + ) : ( + {icon} + )} + + + + ); +}; + +const NavigationItemSkeleton = () => { + return ( + + + + + + ); +}; diff --git a/web_ui/frontend/components/layout/Sidebar/Sidebar.tsx b/web_ui/frontend/components/layout/Navigation/Sidebar/Sidebar.tsx similarity index 78% rename from web_ui/frontend/components/layout/Sidebar/Sidebar.tsx rename to web_ui/frontend/components/layout/Navigation/Sidebar/Sidebar.tsx index 136ae53f9..401cc4576 100644 --- a/web_ui/frontend/components/layout/Sidebar/Sidebar.tsx +++ b/web_ui/frontend/components/layout/Navigation/Sidebar/Sidebar.tsx @@ -18,7 +18,7 @@ import { Box } from '@mui/material'; -import styles from '../../../app/page.module.css'; +import styles from '../../../../app/page.module.css'; import React, { ReactNode } from 'react'; import UserMenu from './UserMenu'; @@ -26,8 +26,11 @@ import { default as NextLink } from 'next/link'; import Image from 'next/image'; import PelicanLogo from '@/public/static/images/PelicanPlatformLogo_Icon.png'; import AboutMenu from './AboutMenu'; +import { NavigationItem } from '@/components/layout/Navigation/Sidebar/NavigationItem'; +import { NavigationProps } from '@/components/layout/Navigation'; +import { evaluateOrReturn } from '@/helpers/util'; -export const Sidebar = ({ children }: { children: ReactNode }) => { +export const Sidebar = ({ config, exportType, role }: NavigationProps) => { return ( { loading={'eager'} /> - {children} + {config.map((navItem) => { + return ( + + ); + })} { +const UserMenu = ({ menuOptions }: { menuOptions?: Partial }) => { const userMenuRef = React.useRef(null); const { @@ -100,11 +100,14 @@ const UserMenu = () => { anchorOrigin={{ vertical: 'center', horizontal: 'right', + ...menuOptions?.anchorOrigin, }} transformOrigin={{ vertical: 'center', horizontal: 'left', + ...menuOptions?.transformOrigin, }} + {...menuOptions} > {user.role === 'admin' ? ( Admin User diff --git a/web_ui/frontend/components/layout/Navigation/Sidebar/index.tsx b/web_ui/frontend/components/layout/Navigation/Sidebar/index.tsx new file mode 100644 index 000000000..c167c49f6 --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/Sidebar/index.tsx @@ -0,0 +1 @@ +export * from './Sidebar'; diff --git a/web_ui/frontend/components/layout/Navigation/index.ts b/web_ui/frontend/components/layout/Navigation/index.ts new file mode 100644 index 000000000..798a8fc37 --- /dev/null +++ b/web_ui/frontend/components/layout/Navigation/index.ts @@ -0,0 +1,40 @@ +import { ServerType, User } from '@/index'; +import { ReactNode } from 'react'; +export { Navigation } from './Navigation'; +import { ExportRes } from '@/components/DataExportTable'; + +export type NavigationConfiguration = { + [key in ServerType | 'shared']: StaticNavigationItemProps[]; +}; + +export type StaticNavigationItemProps = + | StaticNavigationParentItemProps + | StaticNavigationChildItemProps; + +export type StaticNavigationBaseItemProps = { + title: string | (() => string); + icon: ReactNode; + showTitle?: boolean; + allowedRoles?: User['role'][]; + allowedExportTypes?: ExportRes['type'][]; +}; + +export type StaticNavigationChildItemProps = StaticNavigationBaseItemProps & { + href: string | (() => string); +}; + +export type StaticNavigationParentItemProps = StaticNavigationBaseItemProps & { + children: StaticNavigationItemProps[]; +}; + +export type NavigationItemProps = { + exportType?: ExportRes['type']; + role?: User['role']; + config: StaticNavigationItemProps; +}; + +export type NavigationProps = { + exportType?: ExportRes['type']; + role?: User['role']; + config: StaticNavigationItemProps[]; +}; diff --git a/web_ui/frontend/components/layout/OriginSidebar.tsx b/web_ui/frontend/components/layout/OriginSidebar.tsx deleted file mode 100644 index 5ff6ff013..000000000 --- a/web_ui/frontend/components/layout/OriginSidebar.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/*************************************************************** - * - * Copyright (C) 2024, Pelican Project, Morgridge Institute for Research - * - * Licensed under the Apache License, Version 2.0 (the "License"); you - * may not use this file except in compliance with the License. You may - * obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ***************************************************************/ -'use client'; - -import { Box, Tooltip } from '@mui/material'; -import Link from 'next/link'; -import { Build, Dashboard, Public, Equalizer, Lock } from '@mui/icons-material'; -import IconButton from '@mui/material/IconButton'; -import useSWR from 'swr'; - -import { ButtonLink, Sidebar } from '@/components/layout/Sidebar'; -import { getExportData } from '../DataExportTable'; - -export const OriginSidebar = () => { - const { data, error } = useSWR('getDataExport', getExportData); - - if (error) { - console.log('Error fetching data exports: ' + error); - } - - return ( - - - - - - - - {data?.type === 'globus' && ( - - - - )} - - - - - - - - ); -}; diff --git a/web_ui/frontend/components/layout/PaddedContent.tsx b/web_ui/frontend/components/layout/PaddedContent.tsx index 7059ddc50..39e518643 100644 --- a/web_ui/frontend/components/layout/PaddedContent.tsx +++ b/web_ui/frontend/components/layout/PaddedContent.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; export const PaddedContent = ({ children }: { children: ReactNode }) => { return ( - + {children} ); diff --git a/web_ui/frontend/components/layout/Sidebar/ButtonLink.tsx b/web_ui/frontend/components/layout/Sidebar/ButtonLink.tsx deleted file mode 100644 index f2af13fdc..000000000 --- a/web_ui/frontend/components/layout/Sidebar/ButtonLink.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Box, Tooltip } from '@mui/material'; -import Link from 'next/link'; -import IconButton from '@mui/material/IconButton'; -import { Dashboard } from '@mui/icons-material'; -import { ReactNode } from 'react'; - -interface ButtonLinkProps { - title: string; - href: string; - children: ReactNode; -} - -export const ButtonLink = ({ title, href, children }: ButtonLinkProps) => { - return ( - - - - {children} - - - - ); -}; diff --git a/web_ui/frontend/components/layout/Sidebar/index.tsx b/web_ui/frontend/components/layout/Sidebar/index.tsx deleted file mode 100644 index 64f44763e..000000000 --- a/web_ui/frontend/components/layout/Sidebar/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ButtonLink'; -export * from './Sidebar'; diff --git a/web_ui/frontend/components/layout/index.ts b/web_ui/frontend/components/layout/index.ts index 42f48d2b8..d6c7de324 100644 --- a/web_ui/frontend/components/layout/index.ts +++ b/web_ui/frontend/components/layout/index.ts @@ -2,6 +2,5 @@ export * from './AuthenticatedContent'; export * from './Drawer'; export * from './Header'; export * from './Main'; -export * from './OriginSidebar'; export * from './SidebarSpeedDial'; export * from './PaddedContent'; diff --git a/web_ui/frontend/helpers/login.ts b/web_ui/frontend/helpers/login.ts index 1f3d7a50c..69ea707e2 100644 --- a/web_ui/frontend/helpers/login.ts +++ b/web_ui/frontend/helpers/login.ts @@ -96,8 +96,8 @@ export const getUser = async (): Promise => { // If authenticated, store status and csrf token const user = { authenticated: json['authenticated'], - user: json['user'] == '' ? undefined : json['user'], - role: json['role'] == '' ? undefined : json['role'], + user: json['user'] == '' ? null : json['user'], + role: json['role'] == '' ? null : json['role'], csrf_token: response.headers.get('X-CSRF-Token'), };