Skip to content

Commit

Permalink
Merge pull request #1737 from CannonLock/namespace-ui
Browse files Browse the repository at this point in the history
Namespace UI
  • Loading branch information
CannonLock authored Nov 20, 2024
2 parents f3f001d + 0124123 commit 5ccc449
Show file tree
Hide file tree
Showing 30 changed files with 756 additions and 183 deletions.
29 changes: 24 additions & 5 deletions web_ui/frontend/app/director/components/DirectorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Authenticated, secureFetch } from '@/helpers/login';
import React, { useContext, useRef, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
Avatar,
Box,
Expand All @@ -23,17 +23,21 @@ import Link from 'next/link';
import { User } from '@/index';
import { alertOnError, getErrorMessage } from '@/helpers/util';
import { DirectorDropdown } from '@/app/director/components/DirectorDropdown';
import { allowServer, filterServer } from '@/helpers/api';
import { ServerDetailed, ServerGeneral } from '@/types';
import { allowServer, filterServer, getDirectorServer } from '@/helpers/api';
import { AlertDispatchContext } from '@/components/AlertProvider';

export interface DirectorCardProps {
server: Server;
server: ServerGeneral;
authenticated?: User;
}

export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => {
const [disabled, setDisabled] = useState<boolean>(false);
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
const [detailedServer, setDetailedServer] = useState<
ServerDetailed | undefined
>();

const dispatch = useContext(AlertDispatchContext);

Expand All @@ -58,7 +62,19 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => {
server.healthStatus === 'Error' ? red[100] : 'secondary.main',
p: 1,
}}
onClick={() => setDropdownOpen(!dropdownOpen)}
onClick={async () => {
setDropdownOpen(!dropdownOpen);
if (detailedServer === undefined) {
alertOnError(
async () => {
const response = await getDirectorServer(server.name);
setDetailedServer(await response.json());
},
'Failed to fetch server details',
dispatch
);
}
}}
>
<Box my={'auto'} ml={1} display={'flex'} flexDirection={'row'}>
<NamespaceIcon
Expand Down Expand Up @@ -124,7 +140,10 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => {
</Box>
</Box>
</Paper>
<DirectorDropdown server={server} transition={dropdownOpen} />
<DirectorDropdown
server={detailedServer || server}
transition={dropdownOpen}
/>
</>
);
};
Expand Down
3 changes: 2 additions & 1 deletion web_ui/frontend/app/director/components/DirectorCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DirectorCard, DirectorCardProps } from './';
import { Server } from '@/index';
import { BooleanToggleButton, CardList } from '@/components';
import useFuse from '@/helpers/useFuse';
import { ServerGeneral } from '@/types';

interface DirectorCardListProps {
data: Partial<DirectorCardProps>[];
Expand Down Expand Up @@ -88,7 +89,7 @@ export function DirectorCardList({ data, cardProps }: DirectorCardListProps) {
);
}

const serverHasError = (server?: Server) => {
const serverHasError = (server?: ServerGeneral) => {
return server?.healthStatus === 'Error';
};

Expand Down
140 changes: 54 additions & 86 deletions web_ui/frontend/app/director/components/DirectorDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { Capabilities, Server, StringTree } from '@/index';
import {
CapabilitiesChip,
CapabilitiesDisplay,
Dropdown,
InformationSpan,
} from '@/components';
import { CapabilitiesChip, Dropdown, InformationSpan } from '@/components';
import { Box, Grid, Typography } from '@mui/material';
import DirectoryTree from '@/components/DirectoryTree';
import React from 'react';
import { SinglePointMap } from '@/components/Map';
import { ServerCapabilitiesTable } from '@/components/ServerCapabilitiesTable';
import { Capabilities, ServerDetailed, ServerGeneral } from '@/types';
import { Capability } from '@/components/configuration';

interface DirectorDropdownProps {
server: Server;
server: ServerGeneral | ServerDetailed;
transition: boolean;
}

Expand All @@ -20,97 +17,68 @@ export const DirectorDropdown = ({
transition,
}: DirectorDropdownProps) => {
return (
<Dropdown transition={transition} flexDirection={'column'}>
<Grid container spacing={1}>
<Grid item xs={12} md={7}>
<InformationSpan name={'Type'} value={server.type} />
<InformationSpan name={'Status'} value={server.healthStatus} />
<InformationSpan name={'URL'} value={server.url} />
<InformationSpan
name={'Longitude'}
value={server.longitude.toString()}
/>
<InformationSpan
name={'Latitude'}
value={server.latitude.toString()}
/>
</Grid>
<Grid item xs={12} md={5}>
<Box
borderRadius={1}
height={'100%'}
minHeight={'140px'}
overflow={'hidden'}
>
{transition && (
<SinglePointMap
point={{ lat: server.latitude, lng: server.longitude }}
/>
)}
</Box>
<>
<Dropdown transition={transition} flexDirection={'column'}>
<Grid container spacing={1}>
<Grid item xs={12} md={7}>
<InformationSpan name={'Type'} value={server.type} />
<InformationSpan name={'Status'} value={server.healthStatus} />
<InformationSpan name={'URL'} value={server.url} />
<InformationSpan
name={'Longitude'}
value={server.longitude.toString()}
/>
<InformationSpan
name={'Latitude'}
value={server.latitude.toString()}
/>
</Grid>
<Grid item xs={12} md={5}>
<Box
borderRadius={1}
height={'100%'}
minHeight={'140px'}
overflow={'hidden'}
>
{transition && (
<SinglePointMap
point={{ lat: server.latitude, lng: server.longitude }}
/>
)}
</Box>
</Grid>
</Grid>
</Grid>
{server.capabilities && (
<Box mt={1}>
<CapabilitiesRow capabilities={server.capabilities} />
<Box sx={{ my: 1 }}>
<ServerCapabilitiesTable server={server} />
</Box>
)}
<Box sx={{ my: 1 }}>
<Typography
variant={'body2'}
sx={{ fontWeight: 500, display: 'inline', mr: 2 }}
>
Namespace Prefixes
</Typography>
<DirectoryTree data={directoryListToTree(server.namespacePrefixes)} />
</Box>
</Dropdown>
</Dropdown>
</>
);
};

const CapabilitiesRow = ({ capabilities }: { capabilities: Capabilities }) => {
export const CapabilitiesRow = ({
capabilities,
parentCapabilities,
}: {
capabilities: Capabilities;
parentCapabilities?: Capabilities;
}) => {
return (
<Grid container spacing={1}>
{Object.entries(capabilities).map(([key, value]) => {
const castKey = key as keyof Capabilities;
return (
<Grid item md={12 / 5} sm={12 / 4} xs={12 / 2} key={key}>
<CapabilitiesChip name={key} value={value} />
<CapabilitiesChip
name={key}
value={value}
parentValue={
parentCapabilities ? parentCapabilities[castKey] : undefined
}
/>
</Grid>
);
})}
</Grid>
);
};

const directoryListToTree = (directoryList: string[]): StringTree => {
let tree = {};
directoryList.forEach((directory) => {
const path = directory
.split('/')
.filter((x) => x != '')
.map((x) => '/' + x);
tree = directoryListToTreeHelper(path, tree);
});

return tree;
};

const directoryListToTreeHelper = (
path: string[],
tree: StringTree
): true | StringTree => {
if (path.length == 0) {
return true;
}

if (!tree[path[0]] || tree[path[0]] === true) {
tree[path[0]] = {};
}

tree[path[0]] = directoryListToTreeHelper(
path.slice(1),
tree[path[0]] as StringTree
);

return tree;
};
77 changes: 77 additions & 0 deletions web_ui/frontend/app/director/components/NamespaceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { secureFetch } from '@/helpers/login';
import React, { useContext, useState } from 'react';
import { Box, Paper, Typography } from '@mui/material';
import { NamespaceIcon } from '@/components/Namespace/index';
import { NamespaceDropdown } from './NamespaceDropdown';
import { DirectorNamespace, ServerDetailed, ServerGeneral } from '@/types';
import { getDirectorServer } from '@/helpers/api';
import { alertOnError } from '@/helpers/util';
import { AlertDispatchContext } from '@/components/AlertProvider';

export interface NamespaceCardProps {
namespace: DirectorNamespace;
}

export const NamespaceCard = ({ namespace }: NamespaceCardProps) => {
const dispatch = useContext(AlertDispatchContext);
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
const [servers, setServers] = useState<ServerDetailed[] | undefined>(
undefined
);

return (
<>
<Paper>
<Box
sx={{
cursor: 'pointer',
display: 'flex',
width: '100%',
justifyContent: 'space-between',
border: 'solid #ececec 1px',
borderRadius: '4px',
transition: 'background-color 0.3s',
p: 1,
}}
onClick={async () => {
setDropdownOpen(!dropdownOpen);
if (servers === undefined) {
alertOnError(
async () => setServers(await getAssociatedServers(namespace)),
'Failed to fetch servers',
dispatch
);
}
}}
>
<Box my={'auto'} ml={1} display={'flex'} flexDirection={'row'}>
<NamespaceIcon serverType={'namespace'} />
<Typography sx={{ pt: '2px' }}>{namespace.path}</Typography>
</Box>
</Box>
</Paper>
<NamespaceDropdown
namespace={namespace}
servers={servers}
transition={dropdownOpen}
/>
</>
);
};

const getAssociatedServers = async (namespace: DirectorNamespace) => {
const servers = await Promise.all(
[...namespace.origins, ...namespace.caches].map(async (name) =>
(await getDirectorServer(name)).json()
)
);

// Alert the console if any servers are undefined, as this is unlikely to happen naturally
if (servers.some((s) => s === undefined)) {
console.error('Failed to fetch all servers, some are undefined');
}

return servers.filter((s) => s !== undefined) as ServerDetailed[];
};

export default NamespaceCard;
31 changes: 31 additions & 0 deletions web_ui/frontend/app/director/components/NamespaceCardList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useState } from 'react';
import { Box, TextField } from '@mui/material';
import { NamespaceCard, NamespaceCardProps } from './';
import { CardList } from '@/components';
import useFuse from '@/helpers/useFuse';

interface NamespaceCardListProps {
data?: Partial<NamespaceCardProps>[];
}

export function NamespaceCardList({ data }: NamespaceCardListProps) {
const [search, setSearch] = useState<string>('');

const searchedData = useFuse<Partial<NamespaceCardProps>>(data || [], search);

return (
<Box>
<Box sx={{ pb: 1 }}>
<TextField
size={'small'}
value={search}
onChange={(e) => setSearch(e.target.value)}
label='Search'
/>
</Box>
<CardList data={searchedData} Card={NamespaceCard} />
</Box>
);
}

export default NamespaceCardList;
Loading

0 comments on commit 5ccc449

Please sign in to comment.