|
|
|
import Head from 'next/head';
|
|
|
|
import { NextRouter, useRouter } from 'next/router';
|
|
|
|
import { FC, useEffect, useRef, useState } from 'react';
|
|
|
|
import { Box, Divider } from '@mui/material';
|
|
|
|
import { Add as AddIcon } from '@mui/icons-material';
|
|
|
|
|
|
|
|
import API_BASE_URL from '../lib/consts/API_BASE_URL';
|
|
|
|
import { DIVIDER } from '../lib/consts/DEFAULT_THEME';
|
|
|
|
|
|
|
|
import { Preview } from '../components/Display';
|
|
|
|
import fetchJSON from '../lib/fetchers/fetchJSON';
|
|
|
|
import Header from '../components/Header';
|
|
|
|
import IconButton from '../components/IconButton';
|
|
|
|
import Link from '../components/Link';
|
|
|
|
import OutlinedInput from '../components/OutlinedInput';
|
|
|
|
import { Panel, PanelHeader } from '../components/Panels';
|
|
|
|
import periodicFetch from '../lib/fetchers/periodicFetch';
|
|
|
|
import ProvisionServerDialog from '../components/ProvisionServerDialog';
|
|
|
|
import Spinner from '../components/Spinner';
|
|
|
|
import { last } from '../lib/time';
|
|
|
|
|
|
|
|
type ServerListItem = ServerOverviewMetadata & {
|
|
|
|
isScreenshotStale?: boolean;
|
|
|
|
loading?: boolean;
|
|
|
|
screenshot: string;
|
|
|
|
timestamp: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
const createServerPreviewContainer = (
|
|
|
|
servers: ServerListItem[],
|
|
|
|
router: NextRouter,
|
|
|
|
) => (
|
|
|
|
<Box
|
|
|
|
sx={{
|
|
|
|
display: 'flex',
|
|
|
|
flexDirection: 'row',
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
|
|
'& > *': {
|
|
|
|
width: { xs: '20em', md: '24em' },
|
|
|
|
},
|
|
|
|
|
|
|
|
'& > :not(:last-child)': {
|
|
|
|
marginRight: '2em',
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{servers.map(
|
|
|
|
({
|
|
|
|
anvilName,
|
|
|
|
anvilUUID,
|
|
|
|
isScreenshotStale,
|
|
|
|
loading,
|
|
|
|
screenshot,
|
|
|
|
serverName,
|
|
|
|
serverState,
|
|
|
|
serverUUID,
|
|
|
|
timestamp,
|
|
|
|
}) => (
|
|
|
|
<Preview
|
|
|
|
externalPreview={screenshot}
|
|
|
|
externalTimestamp={timestamp}
|
|
|
|
headerEndAdornment={[
|
|
|
|
<Link
|
|
|
|
href={`/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}`}
|
|
|
|
key={`server_list_to_server_${serverUUID}`}
|
|
|
|
>
|
|
|
|
{serverName}
|
|
|
|
</Link>,
|
|
|
|
<Link
|
|
|
|
href={`/anvil?anvil_uuid=${anvilUUID}`}
|
|
|
|
key={`server_list_server_${serverUUID}_to_anvil_${anvilUUID}`}
|
|
|
|
sx={{
|
|
|
|
opacity: 0.7,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{anvilName}
|
|
|
|
</Link>,
|
|
|
|
]}
|
|
|
|
isExternalLoading={loading}
|
|
|
|
isExternalPreviewStale={isScreenshotStale}
|
|
|
|
isFetchPreview={false}
|
|
|
|
isShowControls={false}
|
|
|
|
isUseInnerPanel
|
|
|
|
key={`server-preview-${serverUUID}`}
|
|
|
|
onClickPreview={() => {
|
|
|
|
router.push(
|
|
|
|
`/server?uuid=${serverUUID}&server_name=${serverName}&server_state=${serverState}&vnc=1`,
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
serverState={serverState}
|
|
|
|
serverUUID={serverUUID}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
)}
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
|
|
|
|
const filterServers = (allServers: ServerListItem[], searchTerm: string) =>
|
|
|
|
searchTerm === ''
|
|
|
|
? {
|
|
|
|
exclude: allServers,
|
|
|
|
include: [],
|
|
|
|
}
|
|
|
|
: allServers.reduce<{
|
|
|
|
exclude: ServerListItem[];
|
|
|
|
include: ServerListItem[];
|
|
|
|
}>(
|
|
|
|
(reduceContainer, server) => {
|
|
|
|
const { serverName } = server;
|
|
|
|
|
|
|
|
if (serverName.includes(searchTerm)) {
|
|
|
|
reduceContainer.include.push(server);
|
|
|
|
} else {
|
|
|
|
reduceContainer.exclude.push(server);
|
|
|
|
}
|
|
|
|
|
|
|
|
return reduceContainer;
|
|
|
|
},
|
|
|
|
{ exclude: [], include: [] },
|
|
|
|
);
|
|
|
|
|
|
|
|
const Dashboard: FC = () => {
|
|
|
|
const componentMountedRef = useRef(true);
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
const [allServers, setAllServers] = useState<ServerListItem[]>([]);
|
|
|
|
const [excludeServers, setExcludeServers] = useState<ServerListItem[]>([]);
|
|
|
|
const [includeServers, setIncludeServers] = useState<ServerListItem[]>([]);
|
|
|
|
|
|
|
|
const [inputSearchTerm, setInputSearchTerm] = useState<string>('');
|
|
|
|
|
|
|
|
const [isOpenProvisionServerDialog, setIsOpenProvisionServerDialog] =
|
|
|
|
useState<boolean>(false);
|
|
|
|
|
|
|
|
const updateServerList = (
|
|
|
|
...filterArgs: Parameters<typeof filterServers>
|
|
|
|
) => {
|
|
|
|
const { exclude, include } = filterServers(...filterArgs);
|
|
|
|
|
|
|
|
if (!componentMountedRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setExcludeServers(exclude);
|
|
|
|
setIncludeServers(include);
|
|
|
|
};
|
|
|
|
|
|
|
|
const { isLoading } = periodicFetch<ServerListItem[]>(
|
|
|
|
`${API_BASE_URL}/server`,
|
|
|
|
{
|
|
|
|
onSuccess: (data = []) => {
|
|
|
|
const serverListItems: ServerListItem[] = (
|
|
|
|
data as ServerOverviewMetadata[]
|
|
|
|
).map((serverOverview) => {
|
|
|
|
const { serverUUID } = serverOverview;
|
|
|
|
const previousScreenshot: string =
|
|
|
|
allServers.find(({ serverUUID: uuid }) => uuid === serverUUID)
|
|
|
|
?.screenshot || '';
|
|
|
|
const item: ServerListItem = {
|
|
|
|
...serverOverview,
|
|
|
|
loading: true,
|
|
|
|
screenshot: previousScreenshot,
|
|
|
|
timestamp: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
fetchJSON<{ screenshot: string; timestamp: number }>(
|
|
|
|
`${API_BASE_URL}/server/${serverUUID}?ss=1`,
|
|
|
|
)
|
|
|
|
.then(({ screenshot, timestamp }) => {
|
|
|
|
if (screenshot.length === 0) return;
|
|
|
|
|
|
|
|
item.isScreenshotStale = !last(timestamp, 300);
|
|
|
|
item.loading = false;
|
|
|
|
item.screenshot = screenshot;
|
|
|
|
item.timestamp = timestamp;
|
|
|
|
|
|
|
|
const allServersWithScreenshots = [...serverListItems];
|
|
|
|
|
|
|
|
if (!componentMountedRef.current) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setAllServers(allServersWithScreenshots);
|
|
|
|
// Don't update servers to include or exclude here to avoid
|
|
|
|
// updating using an outdated input search term. Remember this
|
|
|
|
// block is async and takes a lot longer to complete compared to
|
|
|
|
// the overview fetch.
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
item.isScreenshotStale = true;
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
item.loading = false;
|
|
|
|
});
|
|
|
|
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
|
|
|
|
setAllServers(serverListItems);
|
|
|
|
updateServerList(serverListItems, inputSearchTerm);
|
|
|
|
},
|
|
|
|
refreshInterval: 60000,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
useEffect(
|
|
|
|
() => () => {
|
|
|
|
componentMountedRef.current = false;
|
|
|
|
},
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Box>
|
|
|
|
<Head>
|
|
|
|
<title>Dashboard</title>
|
|
|
|
</Head>
|
|
|
|
<Header />
|
|
|
|
<Panel>
|
|
|
|
{isLoading ? (
|
|
|
|
<Spinner />
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<PanelHeader>
|
|
|
|
<OutlinedInput
|
|
|
|
placeholder="Search by server name"
|
|
|
|
onChange={({ target: { value } }) => {
|
|
|
|
setInputSearchTerm(value);
|
|
|
|
updateServerList(allServers, value);
|
|
|
|
}}
|
|
|
|
sx={{ marginRight: '.6em' }}
|
|
|
|
value={inputSearchTerm}
|
|
|
|
/>
|
|
|
|
<IconButton onClick={() => setIsOpenProvisionServerDialog(true)}>
|
|
|
|
<AddIcon />
|
|
|
|
</IconButton>
|
|
|
|
</PanelHeader>
|
|
|
|
{createServerPreviewContainer(includeServers, router)}
|
|
|
|
{includeServers.length > 0 && (
|
|
|
|
<Divider sx={{ backgroundColor: DIVIDER }} />
|
|
|
|
)}
|
|
|
|
{createServerPreviewContainer(excludeServers, router)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</Panel>
|
|
|
|
<ProvisionServerDialog
|
|
|
|
dialogProps={{ open: isOpenProvisionServerDialog }}
|
|
|
|
onClose={() => {
|
|
|
|
setIsOpenProvisionServerDialog(false);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default Dashboard;
|