mirror of https://github.com/cocosolos/ixion.git
Change ServerCard to accept children
This allows me to pass in chips for summary view or data grid for detailed view. Disable accordion features when data grid child is present.
This commit is contained in:
parent
5d041d5e77
commit
ddf74e8343
|
|
@ -1,5 +1,5 @@
|
|||
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||
import { ServerData } from '../data/ServerData';
|
||||
import { ServerData } from '../../data/ServerData';
|
||||
|
||||
const expansions = [
|
||||
<ToggleButton
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import { Check, Close } from '@mui/icons-material';
|
||||
import { Box, Chip, Tooltip } from '@mui/material';
|
||||
import {
|
||||
ServerData,
|
||||
ServerSetting,
|
||||
ServerSettingsInfo,
|
||||
} from '../../data/ServerData';
|
||||
|
||||
export default function SettingsChipCloud({ server }: { server: ServerData }) {
|
||||
const renderSettingsChip = ([key, value]: [
|
||||
string,
|
||||
boolean | string | number,
|
||||
]) => {
|
||||
if (!ServerSettingsInfo[key]) return null;
|
||||
|
||||
const transformValue = (
|
||||
v: boolean | string | number,
|
||||
setting: ServerSetting
|
||||
): string | number | boolean => {
|
||||
return setting.transform?.(v) ?? v;
|
||||
};
|
||||
|
||||
let chipValue: string | number | boolean | JSX.Element = transformValue(
|
||||
value,
|
||||
ServerSettingsInfo[key]
|
||||
);
|
||||
if (typeof chipValue === 'boolean') {
|
||||
chipValue = chipValue ? (
|
||||
<Check color="success" />
|
||||
) : (
|
||||
<Close color="error" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={key}
|
||||
arrow
|
||||
disableInteractive
|
||||
title={ServerSettingsInfo[key].description}
|
||||
>
|
||||
<Chip
|
||||
label={`${ServerSettingsInfo[key].name === '' ? key : ServerSettingsInfo[key].name}`}
|
||||
avatar={
|
||||
typeof chipValue === 'object' ? undefined : (
|
||||
<Chip label={chipValue} size="small" />
|
||||
)
|
||||
}
|
||||
icon={typeof chipValue === 'object' ? chipValue : undefined}
|
||||
size="small"
|
||||
className="m-1 pr-1"
|
||||
sx={{
|
||||
'& .MuiChip-avatar': {
|
||||
width: 'auto',
|
||||
marginX: 0,
|
||||
order: 2,
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
marginX: 0,
|
||||
order: 2,
|
||||
},
|
||||
'&> .MuiChip-label': {
|
||||
paddingRight: 0.5,
|
||||
},
|
||||
boxShadow: '0px 3px 3px rgba(0, 0, 0, .25)',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{Object.entries(server.customizations).map(renderSettingsChip)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import {
|
|||
ServerSetting,
|
||||
ServerSettings,
|
||||
ServerSettingsInfo,
|
||||
} from '../data/ServerData';
|
||||
} from '../../data/ServerData';
|
||||
|
||||
type KeyValueRow = {
|
||||
id: number;
|
||||
|
|
@ -23,11 +23,13 @@ const columns: GridColDef[] = [
|
|||
{ field: 'description', headerName: 'Description', flex: 6 },
|
||||
];
|
||||
|
||||
export default function ServerSettingsDataGrid({
|
||||
serverSettings,
|
||||
}: {
|
||||
type SettingsDataGridProps = {
|
||||
serverSettings: ServerSettings;
|
||||
}) {
|
||||
};
|
||||
|
||||
export default function SettingsDataGrid({
|
||||
serverSettings,
|
||||
}: SettingsDataGridProps) {
|
||||
const transformValue = (
|
||||
v: boolean | string | number,
|
||||
setting: ServerSetting
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
Check,
|
||||
Close,
|
||||
ArrowCircleUp,
|
||||
ContentCopy,
|
||||
ExpandMore,
|
||||
Info,
|
||||
|
|
@ -13,29 +12,40 @@ import {
|
|||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
} from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
ServerData,
|
||||
ServerSetting,
|
||||
ServerSettingsInfo,
|
||||
} from '../data/ServerData';
|
||||
Children,
|
||||
ElementType,
|
||||
ReactNode,
|
||||
isValidElement,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ServerData } from '../data/ServerData';
|
||||
import CopyImageIcon from '../images/copy-image.png';
|
||||
import { Accordion, AccordionDetails, AccordionSummary } from './Accordion';
|
||||
import ExpansionBar from './ExpansionsBar';
|
||||
import ExpansionBar from './Server/ExpansionsBar';
|
||||
import SettingsDataGrid from './Server/SettingsDataGrid';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
} from './Themed/Accordion';
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
export default function ServerCard({ server }: { server: ServerData }) {
|
||||
type ServerCardProps = {
|
||||
server: ServerData;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function ServerCard({ server, children }: ServerCardProps) {
|
||||
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
|
||||
const [clipboardTooltipOpen, setClipboardTooltipOpen] = useState(false);
|
||||
const handleClipboardTooltipClose = () => {
|
||||
|
|
@ -77,72 +87,39 @@ export default function ServerCard({ server }: { server: ServerData }) {
|
|||
return url;
|
||||
}
|
||||
|
||||
const renderSettingsChip = ([key, value]: [
|
||||
string,
|
||||
boolean | string | number,
|
||||
]) => {
|
||||
if (!ServerSettingsInfo[key]) return null;
|
||||
|
||||
const transformValue = (
|
||||
v: boolean | string | number,
|
||||
setting: ServerSetting
|
||||
): string | number | boolean => {
|
||||
return setting.transform?.(v) ?? v;
|
||||
// Check if this is a detailed server card with a data grid
|
||||
const hasSpecificComponent = (
|
||||
childrenProps: ReactNode,
|
||||
componentType: ElementType
|
||||
): boolean => {
|
||||
const childrenArray = Children.toArray(childrenProps);
|
||||
return childrenArray.some(
|
||||
(child) => isValidElement(child) && child.type === componentType
|
||||
);
|
||||
};
|
||||
|
||||
let chipValue: string | number | boolean | JSX.Element = transformValue(
|
||||
value,
|
||||
ServerSettingsInfo[key]
|
||||
);
|
||||
if (typeof chipValue === 'boolean') {
|
||||
chipValue = chipValue ? (
|
||||
<Check color="success" />
|
||||
) : (
|
||||
<Close color="error" />
|
||||
const isSettingsDataGridChild = hasSpecificComponent(
|
||||
children,
|
||||
SettingsDataGrid
|
||||
);
|
||||
// Manually handle the accordion state to disable closing when detailed view
|
||||
const [expand, setExpand] = useState(isSettingsDataGridChild);
|
||||
const toggleAcordion = () => {
|
||||
if (!isSettingsDataGridChild) {
|
||||
setExpand((prev) => !prev);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={key}
|
||||
arrow
|
||||
disableInteractive
|
||||
title={ServerSettingsInfo[key].description}
|
||||
>
|
||||
<Chip
|
||||
label={`${ServerSettingsInfo[key].name === '' ? key : ServerSettingsInfo[key].name}`}
|
||||
avatar={
|
||||
typeof chipValue === 'object' ? undefined : (
|
||||
<Chip label={chipValue} size="small" />
|
||||
)
|
||||
}
|
||||
icon={typeof chipValue === 'object' ? chipValue : undefined}
|
||||
size="small"
|
||||
className="m-1 pr-1"
|
||||
sx={{
|
||||
'& .MuiChip-avatar': {
|
||||
width: 'auto',
|
||||
marginX: 0,
|
||||
order: 2,
|
||||
},
|
||||
'& .MuiChip-icon': {
|
||||
marginX: 0,
|
||||
order: 2,
|
||||
},
|
||||
'&> .MuiChip-label': {
|
||||
paddingRight: 0.5,
|
||||
},
|
||||
boxShadow: '0px 3px 3px rgba(0, 0, 0, .25)',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-2">
|
||||
<Accordion className="my-0">
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Accordion
|
||||
className="my-0"
|
||||
defaultExpanded={isSettingsDataGridChild}
|
||||
expanded={expand}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={!isSettingsDataGridChild && <ExpandMore />}
|
||||
onClick={toggleAcordion}
|
||||
>
|
||||
<Box className="flex flex-col items-center justify-center">
|
||||
<Box className="flex content-center">
|
||||
{server.customizations['LOGIN.MAINT_MODE'] === 1 ? (
|
||||
|
|
@ -290,16 +267,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
|
|||
<Divider sx={{ marginY: 1 }} />
|
||||
</>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
>
|
||||
{Object.entries(server.customizations).map(renderSettingsChip)}
|
||||
</Box>
|
||||
{children}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Divider />
|
||||
|
|
@ -328,7 +296,13 @@ export default function ServerCard({ server }: { server: ServerData }) {
|
|||
</Box>
|
||||
<Box className="flex items-center whitespace-nowrap">
|
||||
{/* <ServerDetailsModal id={server.id} /> */}
|
||||
<Tooltip title="View full settings." arrow disableInteractive>
|
||||
<Tooltip
|
||||
title={
|
||||
isSettingsDataGridChild ? 'Scroll to top.' : 'View full settings.'
|
||||
}
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={`/server/${encodeURIComponent(server.url)}`}
|
||||
|
|
@ -336,7 +310,11 @@ export default function ServerCard({ server }: { server: ServerData }) {
|
|||
className="p-0"
|
||||
disableRipple
|
||||
>
|
||||
{isSettingsDataGridChild ? (
|
||||
<ArrowCircleUp className="p-1" />
|
||||
) : (
|
||||
<Info className="p-1" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography variant="caption">
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
import { Info } from '@mui/icons-material';
|
||||
import { Card, IconButton, Tooltip, Typography } from '@mui/material';
|
||||
import Modal from '@mui/material/Modal';
|
||||
import { useState } from 'react';
|
||||
import { fetchDataFromBackend } from '../apiUtil';
|
||||
import { ServerData } from '../data/ServerData';
|
||||
import ServerSettingsDataGrid from './ServerSettingsDataGrid';
|
||||
|
||||
export default function ServerDetailsModal({ id }: { id: number }) {
|
||||
const [server, setServer] = useState<ServerData>();
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleOpen = () => {
|
||||
let data: ServerData;
|
||||
const fetchServerData = async () => {
|
||||
try {
|
||||
data = await fetchDataFromBackend(`server/${id}`);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError('An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
setServer(data);
|
||||
};
|
||||
fetchServerData();
|
||||
setOpen(true);
|
||||
};
|
||||
const handleClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title="View full settings." arrow disableInteractive>
|
||||
<IconButton className="p-0" disableRipple onClick={handleOpen}>
|
||||
<Info className="p-1" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
position: 'absolute' as const,
|
||||
top: '10%',
|
||||
left: '10%',
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
overflow: 'hidden',
|
||||
overflowX: 'hidden',
|
||||
boxShadow: 24,
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
position: 'absolute' as const,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'scroll',
|
||||
overflowX: 'hidden',
|
||||
}}
|
||||
>
|
||||
{!server || !server.settings ? (
|
||||
<Typography align="center" variant="subtitle1">
|
||||
{error}
|
||||
</Typography>
|
||||
) : (
|
||||
<ServerSettingsDataGrid serverSettings={server.settings} />
|
||||
)}
|
||||
</Card>
|
||||
</Card>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Box } from '@mui/material';
|
||||
import ErrorCard from '../components/ErrorCard';
|
||||
import SettingsChipCloud from '../components/Server/SettingsChipCloud';
|
||||
import ServerCard from '../components/ServerCard';
|
||||
import { SearchState } from '../data/SearchState';
|
||||
import { ServerData } from '../data/ServerData';
|
||||
|
|
@ -129,7 +130,9 @@ export default function Home({
|
|||
<ErrorCard error="" />
|
||||
) : (
|
||||
filteredServers.map((server: ServerData) => (
|
||||
<ServerCard key={server.url} server={server} />
|
||||
<ServerCard key={server.url} server={server}>
|
||||
<SettingsChipCloud server={server} />
|
||||
</ServerCard>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,12 @@
|
|||
import {
|
||||
ContentCopy,
|
||||
Launch,
|
||||
Public,
|
||||
PublicOff,
|
||||
Warning,
|
||||
} from '@mui/icons-material';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
} from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { fetchDataFromBackend, fetchDemo } from '../apiUtil';
|
||||
import { AlertResponse } from '../components/Alert';
|
||||
import ErrorCard from '../components/ErrorCard';
|
||||
import ExpansionBar from '../components/ExpansionsBar';
|
||||
import ServerSettingsDataGrid from '../components/ServerSettingsDataGrid';
|
||||
import SettingsDataGrid from '../components/Server/SettingsDataGrid';
|
||||
import ServerCard from '../components/ServerCard';
|
||||
import { ServerData } from '../data/ServerData';
|
||||
import CopyImageIcon from '../images/copy-image.png';
|
||||
|
||||
export default function ServerDetails({
|
||||
setAlertInfo,
|
||||
|
|
@ -60,229 +43,16 @@ export default function ServerDetails({
|
|||
fetchServerData();
|
||||
}, [url, setServer, setAlertInfo, setError]);
|
||||
|
||||
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
|
||||
const [clipboardTooltipOpen, setClipboardTooltipOpen] = useState(false);
|
||||
const handleClipboardTooltipClose = () => {
|
||||
setClipboardTooltipOpen(false);
|
||||
};
|
||||
|
||||
const handleClipboardTooltipOpen = () => {
|
||||
setClipboardTooltipOpen(true);
|
||||
};
|
||||
|
||||
const copyServerUrlToClipboard = async (text: string) => {
|
||||
try {
|
||||
navigator.clipboard.writeText(text);
|
||||
setClipboardTooltip('Copied server URL!');
|
||||
handleClipboardTooltipOpen();
|
||||
setTimeout(() => {
|
||||
handleClipboardTooltipClose();
|
||||
setClipboardTooltip('Copy server URL.');
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
handleClipboardTooltipOpen();
|
||||
if (err instanceof Error) {
|
||||
setClipboardTooltip(err.message);
|
||||
} else {
|
||||
setClipboardTooltip('Something went wrong!');
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleClipboardTooltipClose();
|
||||
setClipboardTooltip('Copy server URL.');
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
function formatExternalUrl(serverUrl: string): string {
|
||||
// prepend 'https://' to the URL if it's not already there
|
||||
if (!/^https?:\/\//i.test(serverUrl)) {
|
||||
return `https://${serverUrl}`;
|
||||
}
|
||||
return serverUrl;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{error || !server ? (
|
||||
<ErrorCard error={error} />
|
||||
) : (
|
||||
<Card className="mb-2">
|
||||
<Box className="flex px-4">
|
||||
<Box className="flex flex-col items-center justify-center">
|
||||
<Box className="flex content-center">
|
||||
{server.customizations['LOGIN.MAINT_MODE'] === 1 ? (
|
||||
<Tooltip
|
||||
arrow
|
||||
disableInteractive
|
||||
title="Server is undergoing maintenance."
|
||||
>
|
||||
<Warning color="warning" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div>
|
||||
{!server.up ? (
|
||||
<Tooltip
|
||||
arrow
|
||||
disableInteractive
|
||||
title="Server is offline."
|
||||
>
|
||||
<PublicOff color="error" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
arrow
|
||||
disableInteractive
|
||||
title="Server is online."
|
||||
>
|
||||
<Public color="success" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
<Box className="flex content-center">
|
||||
<Tooltip
|
||||
arrow
|
||||
disableInteractive
|
||||
title="Estimated server geolocation provided by MaxMind"
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component={Link}
|
||||
to="https://www.maxmind.com/"
|
||||
target="_blank"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
color={(theme) => alpha(theme.palette.text.primary, 0.5)}
|
||||
>
|
||||
{server.location}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<CardContent className="grow py-1">
|
||||
<Box className="flex content-center">
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={(theme) => alpha(theme.palette.text.primary, 0.87)}
|
||||
sx={{ lineHeight: 1.0 }}
|
||||
>
|
||||
{server.name}
|
||||
</Typography>
|
||||
{typeof server.customizations['API.WEBSITE'] === 'string' &&
|
||||
server.customizations['API.WEBSITE'] !== '' && (
|
||||
<Tooltip arrow disableInteractive title="Visit website.">
|
||||
<IconButton
|
||||
component={Link}
|
||||
to={formatExternalUrl(
|
||||
server.customizations['API.WEBSITE']
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
size="small"
|
||||
className="py-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
<Launch
|
||||
sx={{
|
||||
fontSize: (theme) =>
|
||||
theme.typography.caption.fontSize,
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
<Box className="flex content-center">
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
color={(theme) => alpha(theme.palette.text.primary, 0.6)}
|
||||
>
|
||||
{server.url}
|
||||
</Typography>
|
||||
{window.isSecureContext && (
|
||||
<Tooltip
|
||||
arrow
|
||||
disableInteractive
|
||||
title={clipboardTooltip}
|
||||
open={
|
||||
clipboardTooltip === 'Copied server URL!' ||
|
||||
clipboardTooltipOpen
|
||||
}
|
||||
onOpen={handleClipboardTooltipOpen}
|
||||
onClose={handleClipboardTooltipClose}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
className="py-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
copyServerUrlToClipboard(server.url);
|
||||
}}
|
||||
disableRipple
|
||||
>
|
||||
<ContentCopy
|
||||
sx={{
|
||||
fontSize: (theme) =>
|
||||
theme.typography.caption.fontSize,
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
<ExpansionBar server={server} />
|
||||
</CardContent>
|
||||
<Box className="flex items-center justify-center">
|
||||
<Typography
|
||||
variant="h5"
|
||||
color={(theme) => theme.palette.text.secondary}
|
||||
>
|
||||
{`Lv.${server.max_level}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider />
|
||||
<CardContent className="flex justify-between py-1">
|
||||
<ServerCard server={server}>
|
||||
{server.settings && (
|
||||
<ServerSettingsDataGrid serverSettings={server.settings} />
|
||||
<SettingsDataGrid serverSettings={server.settings} />
|
||||
)}
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardContent className="flex justify-between py-1">
|
||||
<Typography variant="caption">
|
||||
{server.active_sessions} active sessions
|
||||
{server.login_limit !== 1 && (
|
||||
<Tooltip
|
||||
title={`Server allows ${server.login_limit === 0 ? 'unlimited' : server.login_limit} simultaneous game sessions per IP.`}
|
||||
arrow
|
||||
disableInteractive
|
||||
>
|
||||
<img
|
||||
src={CopyImageIcon}
|
||||
alt=""
|
||||
style={{
|
||||
maxHeight: '1.5em',
|
||||
marginLeft: '0.5em',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
onContextMenu={(event) => event.preventDefault()}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
Updated: {new Date(server.updated).toLocaleString()}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ServerCard>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue