Skip to content

Commit

Permalink
Add Namespace capabilities view to Director
Browse files Browse the repository at this point in the history
- Show namespace capabilities
- Show interaction between namespace and server capabilities
  • Loading branch information
CannonLock committed Nov 14, 2024
1 parent eb02c40 commit 093cf49
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 113 deletions.
40 changes: 36 additions & 4 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, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Avatar,
Box,
Expand All @@ -23,9 +23,10 @@ import Link from 'next/link';
import { User } from '@/index';
import { getErrorMessage } from '@/helpers/util';
import { DirectorDropdown } from '@/app/director/components/DirectorDropdown';
import { ServerDetailed, ServerGeneral } from '@/types';

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

Expand All @@ -34,9 +35,19 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => {
const [error, setError] = useState<string | undefined>(undefined);
const [disabled, setDisabled] = useState<boolean>(false);
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
const [detailedServer, setDetailedServer] = useState<
ServerDetailed | undefined
>();

const { mutate } = useSWR<Server[]>('getServers');

// TODO: REMOVE
useEffect(() => {
(async () => {
setDetailedServer(await getServer(server.name));
})();
}, []);

return (
<>
<Paper>
Expand All @@ -56,7 +67,12 @@ 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) {
setDetailedServer(await getServer(server.name));
}
}}
>
<Box my={'auto'} ml={1} display={'flex'} flexDirection={'row'}>
<NamespaceIcon
Expand Down Expand Up @@ -126,7 +142,10 @@ export const DirectorCard = ({ server, authenticated }: DirectorCardProps) => {
</Box>
</Box>
</Paper>
<DirectorDropdown server={server} transition={dropdownOpen} />
<DirectorDropdown
server={detailedServer || server}
transition={dropdownOpen}
/>
<Portal>
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
Expand Down Expand Up @@ -192,4 +211,17 @@ const allowServer = async (name: string): Promise<string | undefined> => {
}
};

const getServer = async (name: string): Promise<ServerDetailed | undefined> => {
try {
const response = await secureFetch(`/api/v1.0/director_ui/servers/${name}`);
if (response.ok) {
return await response.json();
} else {
return undefined;
}
} catch (e) {
return undefined;
}
};

export default DirectorCard;
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
138 changes: 55 additions & 83 deletions web_ui/frontend/app/director/components/DirectorDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
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 { directoryListToTree } from '@/app/director/components/index';
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,94 +18,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]]);

return tree;
};
32 changes: 32 additions & 0 deletions web_ui/frontend/app/director/components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,34 @@
import { StringTree } from '@/index';

export * from './DirectorCard';
export * from './DirectorCardList';

export 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;
};

export 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]]);

return tree;
};
12 changes: 4 additions & 8 deletions web_ui/frontend/app/director/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,15 @@
import { Box, Grid, Skeleton, Typography } from '@mui/material';
import { useMemo } from 'react';
import useSWR from 'swr';
import { Server } from '@/index';
import {
DirectorCardList,
DirectorCard,
DirectorCardProps,
} from './components';
import { DirectorCardList } from './components';
import { getUser } from '@/helpers/login';
import FederationOverview from '@/components/FederationOverview';
import AuthenticatedContent from '@/components/layout/AuthenticatedContent';
import { PaddedContent } from '@/components/layout';
import { ServerGeneral } from '@/types';

export default function Page() {
const { data } = useSWR<Server[]>('getServers', getServers);
const { data } = useSWR<ServerGeneral[]>('getServers', getServers);

const { data: user, error } = useSWR('getUser', getUser);

Expand Down Expand Up @@ -99,7 +95,7 @@ const getServers = async () => {

let response = await fetch(url);
if (response.ok) {
const responseData: Server[] = await response.json();
const responseData: ServerGeneral[] = await response.json();
responseData.sort((a, b) => a.name.localeCompare(b.name));
return responseData;
}
Expand Down
28 changes: 22 additions & 6 deletions web_ui/frontend/components/CapabilitiesDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Capabilities } from '@/index';
import { Capabilities } from '@/types';
import { Box, Tooltip, Typography } from '@mui/material';
import { grey } from '@mui/material/colors';
import { green, grey } from '@mui/material/colors';
import { Check, Clear } from '@mui/icons-material';
import React from 'react';
import React, { useMemo } from 'react';

export const CapabilitiesDisplay = ({
capabilities,
Expand All @@ -14,21 +14,37 @@ export const CapabilitiesDisplay = ({
{Object.entries(capabilities).map(([key, value]) => {
return (
<Tooltip title={key} key={key}>
<CapabilitiesChip key={key} name={key} value={value} />
<CapabilitiesChip key={key} name={key} value={value as boolean} />
</Tooltip>
);
})}
</>
);
};

/**
* Capabilities chip used to convey the capabilities of a server or namespace
* There are two levels of activity to help represent the relationship between
* activity and the server or namespace.
* @param name
* @param value
* @param active
* @constructor
*/
export const CapabilitiesChip = ({
name,
value,
parentValue,
}: {
name: string;
value: boolean;
parentValue?: boolean;
}) => {
// Switch statement to determine the color of the chip
const isActive = useMemo(() => {
return parentValue !== undefined ? value && parentValue : value;
}, [value, parentValue]);

return (
<Box
sx={{
Expand All @@ -38,8 +54,8 @@ export const CapabilitiesChip = ({
py: 0.4,
px: 1,
mb: 0.2,
backgroundColor: value ? grey[300] : grey[100],
color: value ? 'black' : grey[700],
backgroundColor: isActive ? green[300] : grey[100],
color: isActive ? 'black' : grey[700],
border: '1px 1px solid black',
}}
>
Expand Down
Loading

0 comments on commit 093cf49

Please sign in to comment.