Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cache and Server Cards to Map #1947

Merged
merged 1 commit into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions web_ui/frontend/app/director/map/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,24 @@ import { Box } from '@mui/material';
import useSWR from 'swr';
import { Server } from '@/index';
import { ServerMap } from '@/components/Map';
import { useContext } from 'react';
import { AlertDispatchContext } from '@/components/AlertProvider';
import { ServerGeneral } from '@/types';
import { alertOnError } from '@/helpers/util';
import { getDirectorServers } from '@/helpers/get';

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

return (
<Box width={'100%'}>
<ServerMap servers={data} />
</Box>
const { data } = useSWR<ServerGeneral[] | undefined>(
'getDirectorServers',
async () =>
await alertOnError(
getDirectorServers,
'Failed to fetch servers',
dispatch
)
);
}

const getServers = async (): Promise<Server[]> => {
const url = new URL('/api/v1.0/director_ui/servers', window.location.origin);

let response = await fetch(url);
if (response.ok) {
const responseData: Server[] = await response.json();
responseData.sort((a, b) => a.name.localeCompare(b.name));
return responseData;
}

throw new Error('Failed to fetch servers');
};
return <ServerMap servers={data} />;
}
14 changes: 6 additions & 8 deletions web_ui/frontend/components/InformationSpan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,13 @@ export const InformationSpan = ({
<Tooltip title={name} placement={'right'}>
<Box
sx={{
'&:nth-of-type(odd)': {
bgcolor: grey[300],
p: '4px 6px',
borderRadius: '4px',
},
'&:nth-of-type(even)': {
p: '4px 6px',
},
borderTop: '1px solid',
p: '4px 6px',
display: 'flex',
// On the final element add a bottom border
'&:last-child': {
borderBottom: '1px solid',
},
}}
>
<Typography variant={'body2'} sx={{ display: 'inline', mr: 2 }}>
Expand Down
65 changes: 65 additions & 0 deletions web_ui/frontend/components/Map/PopOutCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { FC, ReactNode, useEffect } from 'react';
import { Box, Grow, IconButton, Typography } from '@mui/material';
import { Close } from '@mui/icons-material';

const PopOutCard = ({
title,
children,
active,
onClose,
}: {
title?: string;
children: ReactNode;
active: boolean;
onClose: () => void;
}) => {
useEffect(() => {
if (active) {
const handler = (event: KeyboardEvent) => {
if (event.key == 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handler);
return () => {
document.removeEventListener('keydown', handler);
};
}
}, []);

return (
<Grow in={active} style={{ transformOrigin: '100% 0 0' }}>
<Box
sx={{
position: 'absolute',
top: 0,
right: 0,
zIndex: 99,
maxWidth: { xs: '100vw', md: '50vw' },
m: { xs: 0, md: 1 },
borderRadius: 1,
borderColor: 'white',
p: 1,
}}
bgcolor={'white'}
p={1}
>
<Box display={'flex'}>
<Typography variant={'h6'}>{title}</Typography>
<CloseButton onClose={onClose} />
</Box>
{children}
</Box>
</Grow>
);
};

const CloseButton = ({ onClose }: { onClose: () => void }) => {
return (
<IconButton onClick={onClose} sx={{ ml: 'auto' }} size={'small'}>
<Close />
</IconButton>
);
};

export default PopOutCard;
43 changes: 43 additions & 0 deletions web_ui/frontend/components/Map/ServerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Box, Grid, Typography } from '@mui/material';
import { InformationSpan } from '@/components';
import { ServerCapabilitiesTable } from '@/components/ServerCapabilitiesTable';
import React from 'react';
import { ServerDetailed, ServerGeneral } from '@/types';

const ServerCard = ({
server,
}: {
server?: ServerGeneral | ServerDetailed;
}) => {
// If there is no server, return null
if (!server) {
return null;
}

return (
<Box>
<Grid container spacing={1}>
<Grid item xs={12}>
<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()}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vaguely remember from my React class that in JSX, React can automatically convert numbers to strings when rendering them. So toString() here and the one on line 30 can be waived.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it does but I have the InformationSpan typed like so.

export const InformationSpan = ({
  name,
  value,
  indent = 0,
}: {
  name: string;
  value: string;
  indent?: number;
}) => {

So I just cast it to pass the type check. To be fair I could accept quite a few more values and have more lenient typing I suppose.

/>
<InformationSpan
name={'Latitude'}
value={server.latitude.toString()}
/>
</Grid>
</Grid>
{server.type == 'Origin' && (
<Box sx={{ my: 1 }}>
<ServerCapabilitiesTable server={server} />
</Box>
)}
</Box>
);
};

export default ServerCard;
114 changes: 79 additions & 35 deletions web_ui/frontend/components/Map/ServerMap.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,111 @@
import React, { useState } from 'react';
import React, { useContext, useMemo, useState } from 'react';
import {
AttributionControl,
LngLat,
LngLatLike,
Marker,
Popup,
} from 'react-map-gl/maplibre';
import { FmdGood } from '@mui/icons-material';
import { ClickAwayListener } from '@mui/base';
import { TripOrigin, Storage } from '@mui/icons-material';

import { DefaultMap } from './';
import { DefaultMap, PopOutCard, ServerCard } from '@/components/Map';
import { Server } from '@/index';

import 'maplibre-gl/dist/maplibre-gl.css';
import { Box, Typography } from '@mui/material';
import { alertOnError } from '@/helpers/util';
import { getDirectorServer } from '@/helpers/api';
import { AlertDispatchContext } from '@/components/AlertProvider';
import { ServerDetailed, ServerGeneral } from '@/types';
import { Box } from '@mui/material';

interface ServerMapProps {
servers?: Server[];
servers?: ServerGeneral[];
}

export const ServerMap = ({ servers }: ServerMapProps) => {
const dispatch = useContext(AlertDispatchContext);

const [activeServer, setActiveServer] = useState<
ServerGeneral | ServerDetailed | undefined
>(undefined);

const _setActiveServer = (server: ServerGeneral | undefined) => {
setActiveServer(server);

if (server?.type == 'Origin') {
alertOnError(
async () => {
const response = await getDirectorServer(server.name);
setActiveServer(await response.json());
},
'Failed to fetch server details',
dispatch
);
}
};

const serverMarkers = useMemo(() => {
return servers?.map((server) => {
return (
<ServerMarker
server={server}
onClick={(x) => {
_setActiveServer(x);
}}
key={server.name}
/>
);
});
}, [servers]);

return (
<DefaultMap style={{ width: '100%', height: '100%' }}>
{servers &&
servers.map((server) => {
return <ServerMarker server={server} key={server.name} />;
})}
</DefaultMap>
<>
<Box position={'relative'} flexGrow={1}>
<PopOutCard
title={activeServer?.name}
active={activeServer != undefined}
onClose={() => _setActiveServer(undefined)}
>
<ServerCard server={activeServer} />
</PopOutCard>
<DefaultMap style={{ width: '100%', height: '100%' }}>
{serverMarkers}
</DefaultMap>
</Box>
</>
);
};

const ServerMarker = ({ server }: { server: Server }) => {
const [showPopup, setShowPopup] = useState(false);

const ServerMarker = ({
server,
onClick,
}: {
server: ServerGeneral;
onClick: (server: ServerGeneral) => void;
}) => {
return (
<>
<Marker
offset={[0, -10]}
longitude={server.longitude}
latitude={server.latitude}
longitude={jitter(server.longitude)}
latitude={jitter(server.latitude)}
key={server.name}
onClick={() => {
setShowPopup(true);
onClick(server);
}}
style={{ cursor: 'pointer' }}
>
<FmdGood />
{server.type == 'Origin' ? <TripOrigin /> : <Storage />}
</Marker>
{showPopup && (
<ClickAwayListener onClickAway={() => setShowPopup(false)}>
<Popup
longitude={server.longitude}
latitude={server.latitude}
closeOnClick={false}
onClose={() => setShowPopup(false)}
offset={[0, -24] as [number, number]}
maxWidth={'auto'}
>
<Box>
<Typography variant={'body1'}>{server.name}</Typography>
</Box>
</Popup>
</ClickAwayListener>
)}
</>
);
};

/**
* Jitter the coordinates of a server to prevent markers from overlapping
* @param n The latitude or longitude to jitter
* @param distance The ~ # of meters to jitter the coordinates by
*/
const jitter = (n: number, distance: number = 1000) => {
return n + (Math.random() - 0.5) * (0.000009 * distance);
};
2 changes: 2 additions & 0 deletions web_ui/frontend/components/Map/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './SinglePointMap';
export * from './DefaultMap';
export * from './ServerMap';
export { default as PopOutCard } from './PopOutCard';
export { default as ServerCard } from './ServerCard';
Loading