Add detailed settings view

This commit is contained in:
Corey 2024-04-16 04:42:41 -04:00
parent fb5e67924d
commit ed229129c9
Signed by: coco
GPG Key ID: 051138DA6AEE3E60
11 changed files with 490 additions and 120 deletions

103
client/package-lock.json generated
View File

@ -12,6 +12,7 @@
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.6",
"@mui/material": "^5.15.6",
"@mui/x-data-grid": "^7.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3"
@ -1051,14 +1052,14 @@
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.39",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.39.tgz",
"integrity": "sha512-puyUptF7VJ+9/dMIRLF+DLR21cWfvejsA6OnatfJfqFp8aMhya7xQtvYLEfCch6ahvFZvNC9FFEGGR+qkgFjUg==",
"version": "5.0.0-beta.40",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
"integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@floating-ui/react-dom": "^2.0.8",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.13",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.14",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
@ -1082,9 +1083,9 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "5.15.13",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.13.tgz",
"integrity": "sha512-ERsk9EWpiitSiKnmUdFJGshtFk647l4p7r+mjRWe/F1l5kT1NTTKkaeDLcK3/lsy0udXjMgcG0bNwzbYBdDdhQ==",
"version": "5.15.15",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.15.tgz",
"integrity": "sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
@ -1116,16 +1117,16 @@
}
},
"node_modules/@mui/material": {
"version": "5.15.13",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.13.tgz",
"integrity": "sha512-E+QisOJcIzTTyeJ0o3lgYMcyrmCydb2S4cn9vTtGpIB9uR6fQ6La3dIGsXgYEGyeOB9YkWzQbNzYzvyODGEWKA==",
"version": "5.15.15",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.15.tgz",
"integrity": "sha512-3zvWayJ+E1kzoIsvwyEvkTUKVKt1AjchFFns+JtluHCuvxgKcLSRJTADw37k0doaRtVAsyh8bz9Afqzv+KYrIA==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.39",
"@mui/core-downloads-tracker": "^5.15.13",
"@mui/system": "^5.15.13",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.13",
"@mui/base": "5.0.0-beta.40",
"@mui/core-downloads-tracker": "^5.15.15",
"@mui/system": "^5.15.15",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.14",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
@ -1160,12 +1161,12 @@
}
},
"node_modules/@mui/private-theming": {
"version": "5.15.13",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.13.tgz",
"integrity": "sha512-j5Z2pRi6talCunIRIzpQERSaHwLd5EPdHMwIKDVCszro1RAzRZl7WmH68IMCgQmJMeglr+FalqNuq048qptGAg==",
"version": "5.15.14",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz",
"integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/utils": "^5.15.13",
"@mui/utils": "^5.15.14",
"prop-types": "^15.8.1"
},
"engines": {
@ -1186,9 +1187,9 @@
}
},
"node_modules/@mui/styled-engine": {
"version": "5.15.11",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.11.tgz",
"integrity": "sha512-So21AhAngqo07ces4S/JpX5UaMU2RHXpEA6hNzI6IQjd/1usMPxpgK8wkGgTe3JKmC2KDmH8cvoycq5H3Ii7/w==",
"version": "5.15.14",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz",
"integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.11.0",
@ -1217,15 +1218,15 @@
}
},
"node_modules/@mui/system": {
"version": "5.15.13",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.13.tgz",
"integrity": "sha512-eHaX3sniZXNWkxX0lmcLxROhQ5La0HkOuF7zxbSdAoHUOk07gboQYmF6hSJ/VBFx/GLanIw67FMTn88vc8niLg==",
"version": "5.15.15",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz",
"integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/private-theming": "^5.15.13",
"@mui/styled-engine": "^5.15.11",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.13",
"@mui/private-theming": "^5.15.14",
"@mui/styled-engine": "^5.15.14",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.14",
"clsx": "^2.1.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@ -1256,9 +1257,9 @@
}
},
"node_modules/@mui/types": {
"version": "7.2.13",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz",
"integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==",
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz",
"integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0"
},
@ -1269,9 +1270,9 @@
}
},
"node_modules/@mui/utils": {
"version": "5.15.13",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.13.tgz",
"integrity": "sha512-qNlR9FLEhORC4zVZ3fzF48213EhP/92N71AcFbhHN73lPJjAbq9lUv+71P7uEdRHdrrOlm8+1zE8/OBy6MUqdg==",
"version": "5.15.14",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz",
"integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@types/prop-types": "^15.7.11",
@ -1295,6 +1296,31 @@
}
}
},
"node_modules/@mui/x-data-grid": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.2.0.tgz",
"integrity": "sha512-WKmFo0eKhj3W7Fv8u5n2XP4UcdzuJ+mEYALiMUDAYsah/hPBH9mA1miXn9DjXF3i3dxgzrTjdJemTgTJxAQZKg==",
"dependencies": {
"@babel/runtime": "^7.24.0",
"@mui/system": "^5.15.14",
"@mui/utils": "^5.15.14",
"clsx": "^2.1.0",
"prop-types": "^15.8.1",
"reselect": "^4.1.8"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^5.15.14",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -6699,6 +6725,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"dev": true
},
"node_modules/reselect": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",

View File

@ -15,6 +15,7 @@
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.6",
"@mui/material": "^5.15.6",
"@mui/x-data-grid": "^7.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.3"

View File

@ -9,6 +9,7 @@ import ServerData from './data/ServerData';
import About from './pages/About';
import Home from './pages/Home';
import NotFound from './pages/NotFound';
import ServerDetails from './pages/ServerDetails';
export function App() {
const [alertInfo, setAlertInfo] = useState<{
@ -58,6 +59,10 @@ export function App() {
/>
}
/>
<Route
path="/server/:id"
element={<ServerDetails setAlertInfo={setAlertInfo} />}
/>
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>

View File

@ -21,9 +21,9 @@ export const fetchDemo = async () => {
}
};
export const fetchData = async (queryParams = {}) => {
export const fetchData = async (dir = 'servers', queryParams = {}) => {
const queryString = new URLSearchParams(queryParams).toString();
const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/servers/${queryString ? `?${queryString}` : ''}`;
const url = `${import.meta.env.PROD ? 'https://api.ixion.dev' : 'http://localhost:8000'}/${dir}/${queryString ? `?${queryString}` : ''}`;
try {
const response = await fetch(url, {
signal: AbortSignal.timeout(5000),

View File

@ -10,6 +10,7 @@ import {
styled,
} from '@mui/material';
import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { postData } from '../apiUtil';
import ServerData from '../data/ServerData';
import { AlertResponse } from './Alert';
@ -51,6 +52,7 @@ export default function AddServer({
servers: ServerData[];
setServers: React.Dispatch<React.SetStateAction<ServerData[]>>;
}) {
const navigate = useNavigate();
const containerRef = useRef<HTMLElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
@ -71,6 +73,7 @@ export default function AddServer({
severity: 'success',
});
if (response.data && typeof response.data === 'object') {
navigate(`/server/${response.data.id}`);
setServers([response.data, ...servers]);
}
if (inputRef.current) {

View File

@ -0,0 +1,68 @@
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
import ServerData from '../data/ServerData';
const expansions = [
<ToggleButton
key="rotz"
value="rotz"
aria-label="rotz-enabled"
className="py-0"
disabled
>
RotZ
</ToggleButton>,
<ToggleButton
key="cop"
value="cop"
aria-label="cop-enabled"
className="py-0"
disabled
>
CoP
</ToggleButton>,
<ToggleButton
key="toau"
value="toau"
aria-label="toau-enabled"
className="py-0"
disabled
>
ToAU
</ToggleButton>,
<ToggleButton
key="wotg"
value="wotg"
aria-label="wotg-enabled"
className="py-0"
disabled
>
WotG
</ToggleButton>,
<ToggleButton
key="soa"
value="soa"
aria-label="soa-enabled"
className="py-0"
disabled
>
SoA
</ToggleButton>,
];
export default function ExpansionBar({ server }: { server: ServerData }) {
return (
<ToggleButtonGroup
size="small"
value={[
server.customizations['LOGIN.RISE_OF_ZILART'] && 'rotz',
server.customizations['LOGIN.CHAINS_OF_PROMATHIA'] && 'cop',
server.customizations['LOGIN.TREASURES_OF_AHT_URGHAN'] && 'toau',
server.customizations['LOGIN.WINGS_OF_THE_GODDESS'] && 'wotg',
server.customizations['LOGIN.SEEKERS_OF_ADOULIN'] && 'soa',
]}
sx={{ '& button': { lineHeight: 1.0 } }}
>
{expansions}
</ToggleButtonGroup>
);
}

View File

@ -10,7 +10,7 @@ import {
import IconButton from '@mui/material/IconButton';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { fetchData, fetchDemo } from '../apiUtil';
import { fetchData } from '../apiUtil';
import SearchState from '../data/SearchState';
import ServerData from '../data/ServerData';
import AddServer from './AddServer';
@ -52,11 +52,6 @@ export default function Header({
});
}
}
setFetchLoading(50);
if (data.length === 0) {
data.push(await fetchDemo());
}
setFetchLoading(75);
setServers(data);
setFetchLoading(100);
setTimeout(() => {

View File

@ -3,6 +3,7 @@ import {
Close,
ContentCopy,
ExpandMore,
Info,
Launch,
Public,
PublicOff,
@ -15,8 +16,6 @@ import {
Chip,
Divider,
IconButton,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography,
alpha,
@ -29,6 +28,11 @@ import ServerData, {
} from '../data/ServerData';
import CopyImageIcon from '../images/copy-image.png';
import { Accordion, AccordionDetails, AccordionSummary } from './Accordion';
import ExpansionBar from './ExpansionsBar';
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
export default function ServerCard({ server }: { server: ServerData }) {
const [clipboardTooltip, setClipboardTooltip] = useState('Copy server URL.');
@ -48,7 +52,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
handleClipboardTooltipOpen();
setTimeout(() => {
handleClipboardTooltipClose();
setClipboardTooltip('Copy Server URL.');
setClipboardTooltip('Copy server URL.');
}, 3000);
} catch (error) {
handleClipboardTooltipOpen();
@ -59,7 +63,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
}
setTimeout(() => {
handleClipboardTooltipClose();
setClipboardTooltip('Copy Server URL.');
setClipboardTooltip('Copy server URL.');
}, 3000);
}
};
@ -134,54 +138,6 @@ export default function ServerCard({ server }: { server: ServerData }) {
);
};
const expansions = [
<ToggleButton
key="rotz"
value="rotz"
aria-label="rotz-enabled"
className="py-0"
disabled
>
RotZ
</ToggleButton>,
<ToggleButton
key="cop"
value="cop"
aria-label="cop-enabled"
className="py-0"
disabled
>
CoP
</ToggleButton>,
<ToggleButton
key="toau"
value="toau"
aria-label="toau-enabled"
className="py-0"
disabled
>
ToAU
</ToggleButton>,
<ToggleButton
key="wotg"
value="wotg"
aria-label="wotg-enabled"
className="py-0"
disabled
>
WotG
</ToggleButton>,
<ToggleButton
key="soa"
value="soa"
aria-label="soa-enabled"
className="py-0"
disabled
>
SoA
</ToggleButton>,
];
return (
<Card className="mb-2">
<Accordion className="my-0">
@ -286,20 +242,7 @@ export default function ServerCard({ server }: { server: ServerData }) {
</Tooltip>
)}
</Box>
<ToggleButtonGroup
size="small"
value={[
server.customizations['LOGIN.RISE_OF_ZILART'] && 'rotz',
server.customizations['LOGIN.CHAINS_OF_PROMATHIA'] && 'cop',
server.customizations['LOGIN.TREASURES_OF_AHT_URGHAN'] &&
'toau',
server.customizations['LOGIN.WINGS_OF_THE_GODDESS'] && 'wotg',
server.customizations['LOGIN.SEEKERS_OF_ADOULIN'] && 'soa',
]}
sx={{ '& button': { lineHeight: 1.0 } }}
>
{expansions}
</ToggleButtonGroup>
<ExpansionBar server={server} />
</CardContent>
<Box className="flex items-center justify-center">
<Typography
@ -334,8 +277,8 @@ export default function ServerCard({ server }: { server: ServerData }) {
</AccordionDetails>
</Accordion>
<Divider />
<CardContent className="flex justify-between py-1">
<Typography variant="caption">
<CardContent className="flex justify-between py-0">
<Typography variant="caption" className="flex items-center">
{server.active_sessions} active sessions
{server.login_limit !== 1 && (
<Tooltip
@ -356,7 +299,18 @@ export default function ServerCard({ server }: { server: ServerData }) {
</Tooltip>
)}
</Typography>
<Typography variant="caption">
<Typography variant="caption" className="flex items-center">
<Tooltip title="View full settings." arrow disableInteractive>
<IconButton
component={Link}
to={`/server/${server.id}`}
onClick={scrollToTop}
className="p-0"
disableRipple
>
<Info className="p-1" />
</IconButton>
</Tooltip>
Updated: {new Date(server.updated).toLocaleString()}
</Typography>
</CardContent>

View File

@ -7,7 +7,7 @@ export interface ServerSetting {
transform?: (arg: boolean | string | number) => boolean | string | number;
}
interface ServerSettings {
export interface ServerSettings {
[key: string]: boolean | string | number;
}
@ -16,6 +16,7 @@ export default interface ServerData {
name: string;
url: string;
max_level: number;
settings?: ServerSettings;
customizations: ServerSettings;
login_limit: number;
active_sessions: number;
@ -28,9 +29,16 @@ export const DemoServerData: ServerData = {
name: 'LandSandBoat Demo',
url: 'github.com/LandSandBoat/server',
max_level: 99,
settings: LsbDefaults,
customizations: {
...LsbDefaults,
'API.WEBSITE': 'https://landsandboat.github.io/server/',
'MAIN.ENABLE_TRUST_CASTING': 1,
'MAP.LEVEL_SYNC_ENABLE': true,
'LOGIN.RISE_OF_ZILART': true,
'LOGIN.CHAINS_OF_PROMATHIA': true,
'LOGIN.TREASURES_OF_AHT_URGHAN': true,
'LOGIN.WINGS_OF_THE_GODDESS': true,
'LOGIN.SEEKERS_OF_ADOULIN': true,
},
login_limit: 0,
active_sessions: 0,

View File

@ -1,6 +1,6 @@
import { Box } from '@mui/material';
import { useEffect, useState } from 'react';
import { fetchData, fetchDemo } from '../apiUtil';
import { fetchData } from '../apiUtil';
import { AlertResponse } from '../components/Alert';
import ErrorCard from '../components/ErrorCard';
import ServerCard from '../components/ServerCard';
@ -35,9 +35,7 @@ export default function Home({
setError('An unknown error occurred.');
}
}
if (data.length === 0) {
data.push(await fetchDemo());
}
setServers(data);
};
@ -161,7 +159,7 @@ export default function Home({
return (
<Box>
{error ? (
{error || filteredServers.length === 0 ? (
<ErrorCard error={error} />
) : (
filteredServers.map((server: ServerData) => (

View File

@ -0,0 +1,307 @@
import {
ContentCopy,
Launch,
Public,
PublicOff,
Warning,
} from '@mui/icons-material';
import {
Box,
Card,
CardContent,
Divider,
IconButton,
Tooltip,
Typography,
alpha,
} from '@mui/material';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { fetchData, fetchDemo } from '../apiUtil';
import { AlertResponse } from '../components/Alert';
import ErrorCard from '../components/ErrorCard';
import ExpansionBar from '../components/ExpansionsBar';
import ServerData, {
ServerSettings,
ServerSettingsInfo,
} from '../data/ServerData';
import CopyImageIcon from '../images/copy-image.png';
interface KeyValueRow {
id: number;
key: string;
name: string;
value: string | number | boolean;
}
const columns: GridColDef[] = [
{ field: 'key', headerName: 'Key', flex: 2 },
{ field: 'name', headerName: 'Name', flex: 1 },
{ field: 'value', headerName: 'Value', flex: 1 },
{ field: 'description', headerName: 'Description', flex: 3 },
];
function ServerSettingsDataGrid({
serverSettings,
}: {
serverSettings: ServerSettings;
}) {
const rows: KeyValueRow[] = Object.entries(serverSettings).map(
([key, value], index) => ({
id: index + 1,
key,
name: ServerSettingsInfo[key]?.name || 'none',
value,
description: ServerSettingsInfo[key]?.description || '',
})
);
return (
<Box className="w-full">
<DataGrid
rows={rows}
columns={columns}
checkboxSelection={false}
autoHeight
density="compact"
hideFooterSelectedRowCount
showCellVerticalBorder
/>
</Box>
);
}
export default function ServerDetails({
setAlertInfo,
}: {
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
}) {
const { id } = useParams();
const [server, setServer] = useState<ServerData>();
const [error, setError] = useState<string>('');
useEffect(() => {
let data: ServerData;
const fetchServerData = async () => {
try {
if (id === 'demo') {
data = await fetchDemo();
} else {
data = await fetchData(`server/${id}`);
}
} catch (err) {
if (err instanceof Error) {
setAlertInfo({
message: err.message,
severity: 'error',
});
} else {
setError('An unknown error occurred.');
}
}
setServer(data);
};
fetchServerData();
}, [id, 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(url: string): string {
// prepend 'https://' to the URL if it's not already there
if (!/^https?:\/\//i.test(url)) {
return `https://${url}`;
}
return url;
}
return (
<Box>
{error || !server ? (
<ErrorCard error={error} />
) : (
<Card className="mb-2">
<Box className="flex px-4">
<Box className="flex items-center justify-center">
{server.customizations['LOGIN.MAINT_MODE'] === 1 ? (
<Tooltip
arrow
disableInteractive
title="Server is undergoing maintenance."
>
<Warning color="warning" />
</Tooltip>
) : (
<div>
{server.inactivity_counter > 0 ? (
<Tooltip
arrow
disableInteractive
title={`Offline for ${server.inactivity_counter} hours.`}
>
<PublicOff color="error" />
</Tooltip>
) : (
<Tooltip arrow disableInteractive title="Server is online.">
<Public color="success" />
</Tooltip>
)}
</div>
)}
</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">
{server.settings && (
<ServerSettingsDataGrid 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>
)}
</Box>
);
}