This commit is contained in:
AuroraLS3 2022-09-04 09:24:21 +00:00
parent 44b9780057
commit b029dc6c7a
118 changed files with 12320 additions and 9691 deletions

View File

@ -96,6 +96,8 @@ Data_gathering:
Accept_GeoLite2_EULA: false
Ping: true
Disk_space: true
# Does not affect already gathered data
Preserve_join_address_case: false
# -----------------------------------------------------
# Supported time units: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS
# -----------------------------------------------------

View File

@ -101,6 +101,8 @@ Data_gathering:
Commands:
Log_unknown: false
Log_aliases_as_main_command: true
# Does not affect already gathered data
Preserve_join_address_case: false
# -----------------------------------------------------
# Supported time units: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS
# -----------------------------------------------------

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +0,0 @@
const errorLogs = document.getElementById('error-logs');
function renderErrorLog(i, errorLog) {
return createErrorAccordionTitle(i, errorLog) + createErrorAccordionBody(i, errorLog);
}
function createErrorAccordionTitle(i, errorLog) {
let style = 'bg-amber-outline';
const tagLine = errorLog.contents[0];
return `<tr id="error_h_${i}" aria-controls="error_t_${i}" aria-expanded="false"
class="clickable collapsed ${style}" data-bs-target="#error_t_${i}" data-bs-toggle="collapse">
<td>${errorLog.fileName}</td>
<td>${tagLine}</td>
</tr>`
}
function createErrorAccordionBody(i, errorLog) {
return `<tr class="collapse" data-bs-parent="#tableAccordion" id="error_t_${i}">
<td colspan="2">
<pre class="pre-scrollable" style="overflow-x: scroll">${errorLog.contents.join('\n')}</pre>
</td>
</tr>`;
}
jsonRequest("./v1/errors", (json, error) => {
if (error) {
return errorLogs.innerText = `Failed to load /v1/errors: ${error}`;
}
let html = ``;
for (let i = 0; i < json.length; i++) {
html += renderErrorLog(i, json[i]);
}
errorLogs.innerHTML = html;
})

View File

@ -46,6 +46,7 @@ function drawSine(canvasId) {
function draw() {
const canvas = document.getElementById(canvasId);
if (canvas == null) return;
const context = canvas.getContext("2d");
context.clearRect(0, 0, 1000, 150);
@ -60,6 +61,7 @@ function drawSine(canvasId) {
function fix_dpi() {
const canvas = document.getElementById(canvasId);
if (canvas == null) return;
let dpi = window.devicePixelRatio;
canvas.getContext('2d');
const style_width = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2);

View File

@ -1,43 +1,44 @@
{
"license": "LGPL-3.0-or-later",
"name": "dashboard",
"version": "0.1.0",
"private": true,
"proxy": "https://localhost:8804",
"proxy": "http://localhost:8800",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.1.2",
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.1.2",
"@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@fortawesome/fontawesome-free": "^6.2.0",
"@fortawesome/fontawesome-svg-core": "^6.2.0",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@fullcalendar/bootstrap": "^5.11.2",
"@fullcalendar/daygrid": "^5.11.2",
"@fullcalendar/bootstrap": "^5.11.3",
"@fullcalendar/daygrid": "^5.11.3",
"@fullcalendar/react": "^5.11.2",
"@highcharts/map-collection": "^2.0.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^14.3.0",
"@testing-library/user-event": "^14.4.3",
"axios": "^0.27.2",
"bootstrap": "^5.2.0",
"datatables.net": "^1.12.1",
"datatables.net-bs5": "^1.12.1",
"datatables.net-responsive-bs5": "^2.3.0",
"highcharts": "^9.3.2",
"i18next": "^21.8.16",
"i18next-chained-backend": "^3.0.2",
"highcharts": "^10.2.1",
"i18next": "^21.9.1",
"i18next-chained-backend": "^3.1.0",
"i18next-http-backend": "^1.4.1",
"i18next-localstorage-backend": "^3.1.3",
"masonry-layout": "^4.2.2",
"react": "^17.0.2",
"react-bootstrap-v5": "^1.4.0",
"react-dom": "^17.0.2",
"react-i18next": "^11.18.3",
"react-i18next": "^11.18.5",
"react-router-dom": "6",
"react-scripts": "5.0.1",
"sass": "^1.54.2",
"sass": "^1.54.8",
"source-map-explorer": "^2.5.2",
"swagger-ui": "^4.13.2",
"web-vitals": "^2.1.0"
"swagger-ui": "^4.14.0",
"web-vitals": "^3.0.1"
},
"scripts": {
"start": "react-scripts start",

View File

@ -4,12 +4,6 @@ import './style/style.css';
import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
import React from "react";
import PlayerPage from "./views/layout/PlayerPage";
import PlayerOverview from "./views/player/PlayerOverview";
import PlayerSessions from "./views/player/PlayerSessions";
import PlayerPvpPve from "./views/player/PlayerPvpPve";
import PlayerServers from "./views/player/PlayerServers";
import PlayerPluginData from "./views/player/PlayerPluginData";
import {ThemeContextProvider} from "./hooks/themeHook";
import axios from "axios";
import ErrorView from "./views/ErrorView";
@ -17,18 +11,35 @@ import {faMapSigns} from "@fortawesome/free-solid-svg-icons";
import {MetadataContextProvider} from "./hooks/metadataHook";
import {AuthenticationContextProvider} from "./hooks/authenticationHook";
import {NavigationContextProvider} from "./hooks/navigationHook";
import ServerPage from "./views/layout/ServerPage";
import ServerOverview from "./views/server/ServerOverview";
import MainPageRedirect from "./components/navigation/MainPageRedirect";
import OnlineActivity from "./views/server/OnlineActivity";
import ServerSessions from "./views/server/ServerSessions";
import ServerPvpPve from "./views/server/ServerPvpPve";
import PlayerbaseOverview from "./views/server/PlayerbaseOverview";
import ServerPlayers from "./views/server/ServerPlayers";
import PlayersPage from "./views/layout/PlayersPage";
import AllPlayers from "./views/players/AllPlayers";
import ServerGeolocations from "./views/server/ServerGeolocations";
const PlayerPage = React.lazy(() => import("./views/layout/PlayerPage"));
const PlayerOverview = React.lazy(() => import("./views/player/PlayerOverview"));
const PlayerSessions = React.lazy(() => import("./views/player/PlayerSessions"));
const PlayerPvpPve = React.lazy(() => import("./views/player/PlayerPvpPve"));
const PlayerServers = React.lazy(() => import("./views/player/PlayerServers"));
const PlayerPluginData = React.lazy(() => import("./views/player/PlayerPluginData"));
const ServerPage = React.lazy(() => import("./views/layout/ServerPage"));
const ServerOverview = React.lazy(() => import("./views/server/ServerOverview"));
const OnlineActivity = React.lazy(() => import("./views/server/OnlineActivity"));
const ServerSessions = React.lazy(() => import("./views/server/ServerSessions"));
const ServerPvpPve = React.lazy(() => import("./views/server/ServerPvpPve"));
const PlayerbaseOverview = React.lazy(() => import("./views/server/PlayerbaseOverview"));
const ServerPlayers = React.lazy(() => import("./views/server/ServerPlayers"));
const ServerGeolocations = React.lazy(() => import("./views/server/ServerGeolocations"));
const LoginPage = React.lazy(() => import("./views/layout/LoginPage"));
const ServerPerformance = React.lazy(() => import("./views/server/ServerPerformance"));
const ServerPluginData = React.lazy(() => import("./views/server/ServerPluginData"));
const ServerWidePluginData = React.lazy(() => import("./views/server/ServerWidePluginData"));
const NetworkPage = React.lazy(() => import("./views/layout/NetworkPage"));
const NetworkOverview = React.lazy(() => import("./views/network/NetworkOverview"));
const PlayersPage = React.lazy(() => import("./views/layout/PlayersPage"));
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
const ErrorsPage = React.lazy(() => import("./views/layout/ErrorsPage"));
const SwaggerView = React.lazy(() => import("./views/SwaggerView"));
const OverviewRedirect = () => {
@ -47,6 +58,12 @@ const ContextProviders = ({children}) => (
</AuthenticationContextProvider>
)
const Lazy = ({children}) => (
<React.Suspense fallback={<></>}>
{children}
</React.Suspense>
)
function App() {
axios.defaults.withCredentials = true;
@ -57,39 +74,58 @@ function App() {
<BrowserRouter>
<Routes>
<Route path="" element={<MainPageRedirect/>}/>
<Route path="/player/:identifier" element={<PlayerPage/>}>
<Route path="" element={<OverviewRedirect/>}/>
<Route path="overview" element={<PlayerOverview/>}/>
<Route path="sessions" element={<PlayerSessions/>}/>
<Route path="pvppve" element={<PlayerPvpPve/>}/>
<Route path="servers" element={<PlayerServers/>}/>
<Route path="plugins/:serverName" element={<PlayerPluginData/>}/>
<Route path="/" element={<MainPageRedirect/>}/>
<Route path="/login" element={<Lazy><LoginPage/></Lazy>}/>
<Route path="/player/:identifier" element={<Lazy><PlayerPage/></Lazy>}>
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
<Route path="overview" element={<Lazy><PlayerOverview/></Lazy>}/>
<Route path="sessions" element={<Lazy><PlayerSessions/></Lazy>}/>
<Route path="pvppve" element={<Lazy><PlayerPvpPve/></Lazy>}/>
<Route path="servers" element={<Lazy><PlayerServers/></Lazy>}/>
<Route path="plugins/:serverName" element={<Lazy><PlayerPluginData/></Lazy>}/>
<Route path="*" element={<ErrorView error={{
message: 'Unknown tab address, please correct the address',
title: 'No such tab',
icon: faMapSigns
}}/>}/>
</Route>
<Route path="/players" element={<PlayersPage/>}>
<Route path="" element={<AllPlayers/>}/>
<Route path="*" element={<AllPlayers/>}/>
<Route path="/players" element={<Lazy><PlayersPage/></Lazy>}>
<Route path="" element={<Lazy><AllPlayers/></Lazy>}/>
<Route path="*" element={<Lazy><AllPlayers/></Lazy>}/>
</Route>
<Route path="/server/:identifier" element={<ServerPage/>}>
<Route path="" element={<OverviewRedirect/>}/>
<Route path="overview" element={<ServerOverview/>}/>
<Route path="online-activity" element={<OnlineActivity/>}/>
<Route path="sessions" element={<ServerSessions/>}/>
<Route path="pvppve" element={<ServerPvpPve/>}/>
<Route path="playerbase" element={<PlayerbaseOverview/>}/>
<Route path="/server/:identifier" element={<Lazy><ServerPage/></Lazy>}>
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
<Route path="overview" element={<Lazy><ServerOverview/></Lazy>}/>
<Route path="online-activity" element={<Lazy><OnlineActivity/></Lazy>}/>
<Route path="sessions" element={<Lazy><ServerSessions/></Lazy>}/>
<Route path="pvppve" element={<Lazy><ServerPvpPve/></Lazy>}/>
<Route path="playerbase" element={<Lazy><PlayerbaseOverview/></Lazy>}/>
<Route path="retention" element={<></>}/>
<Route path="players" element={<ServerPlayers/>}/>
<Route path="geolocations" element={<ServerGeolocations/>}/>
<Route path="performance" element={<></>}/>
<Route path="plugins-overview" element={<></>}/>
<Route path="players" element={<Lazy><ServerPlayers/></Lazy>}/>
<Route path="geolocations" element={<Lazy><ServerGeolocations/></Lazy>}/>
<Route path="performance" element={<Lazy><ServerPerformance/></Lazy>}/>
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
<Route path="*" element={<ErrorView error={{
message: 'Unknown tab address, please correct the address',
title: 'No such tab',
icon: faMapSigns
}}/>}/>
</Route>
<Route path="docs" element={<React.Suspense fallback={<></>}>
<SwaggerView/>
</React.Suspense>}/>
<Route path="/network" element={<Lazy><NetworkPage/></Lazy>}>
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
<Route path="overview" element={<Lazy><NetworkOverview/></Lazy>}/>
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
<Route path="*" element={<ErrorView error={{
message: 'Unknown tab address, please correct the address',
title: 'No such tab',
icon: faMapSigns
}}/>}/>
</Route>
<Route path="/errors" element={<Lazy><ErrorsPage/></Lazy>}/>
<Route path="/docs" element={<Lazy><SwaggerView/></Lazy>}/>
</Routes>
</BrowserRouter>
</div>

View File

@ -3,6 +3,8 @@ import React from "react";
import End from "./layout/End";
const Datapoint = ({icon, color, name, value, valueLabel, bold, boldTitle, title, trend}) => {
if (value === undefined && valueLabel === undefined) return <></>;
const displayedValue = bold ? <b>{value}</b> : value;
const extraLabel = typeof valueLabel === 'string' ? ` (${valueLabel})` : '';
const colorClass = color && color.startsWith("col-") ? color : "col-" + color;

View File

@ -40,7 +40,7 @@ const NoDataRow = ({width}) => {
</tr>);
}
const Accordion = ({headers, slices, open}) => {
const Accordion = ({headers, slices, open, style}) => {
const [openSlice, setOpenSlice] = useState(open ? 0 : -1);
const {nightModeEnabled} = useTheme();
@ -51,7 +51,8 @@ const Accordion = ({headers, slices, open}) => {
const width = headers.length;
return (
<table className={"table accordion-striped" + (nightModeEnabled ? " table-dark" : '')} id="tableAccordion">
<table className={"table accordion-striped" + (nightModeEnabled ? " table-dark" : '')} id="tableAccordion"
style={style}>
<thead>
<tr>
{headers.map((header, i) => <th key={i}>{header}</th>)}

View File

@ -0,0 +1,39 @@
import React from 'react';
import Accordion from "./Accordion";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBug} from "@fortawesome/free-solid-svg-icons";
import {faClock} from "@fortawesome/free-regular-svg-icons";
const ErrorBody = ({error}) => {
return (<pre className="pre-scrollable" style={{overflowX: "scroll"}}>
{error.contents.map((line) => <>{line}<br/></>)}
</pre>)
}
const ErrorHeader = ({error}) => {
return <>
<td>{error.fileName}</td>
<td>{error.contents.length ? error.contents[0] : '{Empty file}'}</td>
</>
}
const ErrorsAccordion = ({errors}) => {
const headers = [
<><FontAwesomeIcon icon={faBug}/> Logfile</>,
<><FontAwesomeIcon icon={faClock}/> Occurrences</>
];
const slices = errors ? errors.map(error => {
return {
body: <ErrorBody error={error}/>,
header: <ErrorHeader error={error}/>,
color: 'orange',
outline: true
}
}) : [];
return (
<Accordion headers={headers} slices={slices} open={false} style={{tableLayout: "fixed"}}/>
)
};
export default ErrorsAccordion

View File

@ -9,6 +9,7 @@ import KillsTable from "../table/KillsTable";
import Accordion from "./Accordion";
import {useTranslation} from "react-i18next";
import {baseAddress} from "../../service/backendConfiguration";
import {ChartLoader} from "../navigation/Loader";
const SessionHeader = ({session}) => {
return (
@ -88,6 +89,8 @@ const SessionAccordion = (
) => {
const {t} = useTranslation();
if (!sessions) return <ChartLoader/>
const firstColumn = isPlayer ? (<><Fa icon={faUser}/> {t('html.label.player')}</>)
: (<><Fa icon={faServer}/> {t('html.label.server')}</>)

View File

@ -0,0 +1,74 @@
import React, {useEffect, useState} from 'react';
import {Transition} from 'react-transition-group';
const reduceAnimations = window.matchMedia(`(prefers-reduced-motion: reduce)`) === true;
const defaultDuration = 250;
const LoadIn = ({children, duration}) => {
if (!duration) duration = defaultDuration;
const defaultStyle = reduceAnimations ? {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0
} : {
transition: `opacity ${duration}ms ease-in-out, transform ${duration}ms ease-in-out`,
opacity: 0,
transform: "scale(0.99)"
}
const transitionStyles = reduceAnimations ? {
entering: {
opacity: 1,
},
entered: {
opacity: 1,
},
exited: {
opacity: 0,
},
exiting: {
opacity: 0,
}
} : {
entering: {
opacity: 1,
transform: "scale(1)"
},
entered: {
opacity: 1,
transform: "scale(1)"
},
exiting: {
opacity: 0,
transform: "scale(0.99)"
},
exited: {
opacity: 0,
transform: "scale(0.99)"
},
};
const [visible, setVisible] = useState(false);
useEffect(() => {
setTimeout(() => setVisible(true), 0);
return () => {
setVisible(false);
}
}, [setVisible])
return (
<Transition in={visible} timeout={duration}>
{state => (
<div style={{
...defaultStyle,
...transitionStyles[state]
}}>
{children}
</div>
)}
</Transition>
);
}
export default LoadIn

View File

@ -0,0 +1,18 @@
import React from 'react';
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {Card} from "react-bootstrap-v5";
import {useTranslation} from "react-i18next";
const CardHeader = ({icon, color, label}) => {
const {t} = useTranslation();
return (
<Card.Header>
<h6 className="col-black">
<Fa icon={icon} className={"col-" + color}/> {t(label)}
</h6>
</Card.Header>
)
};
export default CardHeader

View File

@ -5,10 +5,13 @@ import React from "react";
import {faExclamationTriangle, faGlobe} from "@fortawesome/free-solid-svg-icons";
import GeolocationBarGraph from "../../graphs/GeolocationBarGraph";
import GeolocationWorldMap from "../../graphs/GeolocationWorldMap";
import {CardLoader} from "../../navigation/Loader";
const GeolocationsCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <CardLoader/>
if (!data?.geolocations_enabled) {
return (
<div className="alert alert-warning mb-0" id="geolocation-warning">

View File

@ -0,0 +1,16 @@
import React from 'react';
import {Card} from "react-bootstrap-v5";
import CardHeader from "../CardHeader";
import {faWifi} from "@fortawesome/free-solid-svg-icons";
import PingTable from "../../table/PingTable";
const PingTableCard = ({data}) => {
return (
<Card>
<CardHeader icon={faWifi} color="green" label={'html.label.connectionInfo'}/>
<PingTable countries={data?.table || []}/>
</Card>
)
};
export default PingTableCard

View File

@ -11,6 +11,7 @@ const PlayerListCard = ({data}) => {
const [options, setOptions] = useState(undefined);
useEffect(() => {
if (!data) return;
for (const row of data.data) {
row.name = row.name.replace('../player/', '../../player/');
}

View File

@ -4,11 +4,12 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faCrosshairs} from "@fortawesome/free-solid-svg-icons";
import KillsTable from "../../table/KillsTable";
import React from "react";
import {CardLoader} from "../../navigation/Loader";
const PvpKillsTableCard = ({player_kills}) => {
const {t} = useTranslation();
if (!player_kills) return <></>;
if (!player_kills) return <CardLoader/>;
return (
<Card>

View File

@ -4,9 +4,13 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faClock} from "@fortawesome/free-regular-svg-icons";
import WorldPie from "../../graphs/WorldPie";
import React from "react";
import {CardLoader} from "../../navigation/Loader";
const WorldPieCard = ({worldSeries, gmSeries}) => {
const {t} = useTranslation();
if (!worldSeries || !gmSeries) return <CardLoader/>;
return (
<Card>
<Card.Header>

View File

@ -2,12 +2,13 @@ import React from "react";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchPlayerbaseDevelopmentGraph} from "../../../../service/serverService";
import {ErrorViewBody} from "../../../../views/ErrorView";
import {ErrorViewCard} from "../../../../views/ErrorView";
import {useTranslation} from "react-i18next";
import {Card} from "react-bootstrap-v5";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faUsers} from "@fortawesome/free-solid-svg-icons";
import PlayerbasePie from "../../../graphs/PlayerbasePie";
import {CardLoader} from "../../../navigation/Loader";
const CurrentPlayerbaseCard = () => {
const {t} = useTranslation();
@ -15,8 +16,8 @@ const CurrentPlayerbaseCard = () => {
const {data, loadingError} = useDataRequest(fetchPlayerbaseDevelopmentGraph, [identifier]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <></>;
if (loadingError) return <ErrorViewCard error={loadingError}/>
if (!data) return <CardLoader/>;
return (
<Card>

View File

@ -0,0 +1,60 @@
import React from 'react';
import {useTranslation} from "react-i18next";
import {Card} from "react-bootstrap-v5";
import CardTabs from "../../../CardTabs";
import {faChartArea} from "@fortawesome/free-solid-svg-icons";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchDayByDayGraph, fetchHourByHourGraph, fetchPlayersOnlineGraph} from "../../../../service/serverService";
import {ErrorViewBody} from "../../../../views/ErrorView";
import {ChartLoader} from "../../../navigation/Loader";
import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph";
import PlayersOnlineGraph from "../../../graphs/PlayersOnlineGraph";
import {useMetadata} from "../../../../hooks/metadataHook";
const PlayersOnlineTab = () => {
const {serverUUID} = useMetadata();
const {data, loadingError} = useDataRequest(fetchPlayersOnlineGraph, [serverUUID]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader/>;
return <PlayersOnlineGraph data={data}/>
}
const DayByDayTab = () => {
const {data, loadingError} = useDataRequest(fetchDayByDayGraph, [])
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader/>;
return <TimeByTimeGraph data={data}/>
}
const HourByHourTab = () => {
const {data, loadingError} = useDataRequest(fetchHourByHourGraph, [])
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader/>;
return <TimeByTimeGraph data={data}/>
}
const NetworkOnlineActivityGraphsCard = () => {
const {t} = useTranslation();
return <Card>
<CardTabs tabs={[
{
name: t('html.label.networkOnlineActivity'), icon: faChartArea, color: 'blue', href: 'online-activity',
element: <PlayersOnlineTab/>
}, {
name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day',
element: <DayByDayTab/>
}, {
name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour',
element: <HourByHourTab/>
}
]}/>
</Card>
};
export default NetworkOnlineActivityGraphsCard

View File

@ -0,0 +1,136 @@
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchOptimizedPerformance, fetchPingGraph} from "../../../../service/serverService";
import {ErrorViewBody} from "../../../../views/ErrorView";
import {useTranslation} from "react-i18next";
import {Card} from "react-bootstrap-v5";
import CardTabs from "../../../CardTabs";
import {faGears, faHdd, faMap, faMicrochip, faSignal, faTachometerAlt} from "@fortawesome/free-solid-svg-icons";
import React, {useEffect, useState} from "react";
import {ChartLoader} from "../../../navigation/Loader";
import AllPerformanceGraph from "../../../graphs/performance/AllPerformanceGraph";
import TpsPerformanceGraph from "../../../graphs/performance/TpsPerformanceGraph";
import CpuRamPerformanceGraph from "../../../graphs/performance/CpuRamPerformanceGraph";
import WorldPerformanceGraph from "../../../graphs/performance/WorldPerformanceGraph";
import DiskPerformanceGraph from "../../../graphs/performance/DiskPerformanceGraph";
import PingGraph from "../../../graphs/performance/PingGraph";
const AllGraphTab = ({data, dataSeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <AllPerformanceGraph id="server-performance-all-chart" data={data} dataSeries={dataSeries}/>
}
const TpsGraphTab = ({data, dataSeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <TpsPerformanceGraph id="server-performance-tps-chart" data={data} dataSeries={dataSeries}/>
}
const CpuRamGraphTab = ({data, dataSeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <CpuRamPerformanceGraph id="server-performance-cpuram-chart" data={data} dataSeries={dataSeries}/>
}
const WorldGraphTab = ({data, dataSeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <WorldPerformanceGraph id="server-performance-world-chart" data={data} dataSeries={dataSeries}/>
}
const DiskGraphTab = ({data, dataSeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!dataSeries) return <ChartLoader style={{height: "450px"}}/>;
return <DiskPerformanceGraph id="server-performance-disk-chart" data={data} dataSeries={dataSeries}/>
}
const PingGraphTab = ({identifier}) => {
const {data, loadingError} = useDataRequest(fetchPingGraph, [identifier]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader style={{height: "450px"}}/>;
return <PingGraph id="server-performance-ping-chart" data={data}/>;
}
function mapToDataSeries(performanceData) {
const playersOnline = [];
const tps = [];
const cpu = [];
const ram = [];
const entities = [];
const chunks = [];
const disk = [];
return new Promise((resolve => {
let i = 0;
const length = performanceData.length;
function processNextThousand() {
const to = Math.min(i + 1000, length);
for (i; i < to; i++) {
const entry = performanceData[i];
const date = entry[0];
playersOnline[i] = [date, entry[1]];
tps[i] = [date, entry[2]];
cpu[i] = [date, entry[3]];
ram[i] = [date, entry[4]];
entities[i] = [date, entry[5]];
chunks[i] = [date, entry[6]];
disk[i] = [date, entry[7]];
}
if (i >= length) {
resolve({playersOnline, tps, cpu, ram, entities, chunks, disk})
} else {
setTimeout(processNextThousand, 10);
}
}
processNextThousand();
}))
}
const PerformanceGraphsCard = () => {
const {t} = useTranslation();
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(fetchOptimizedPerformance, [identifier]);
const [parsedData, setParsedData] = useState(undefined)
useEffect(() => {
if (data) {
mapToDataSeries(data.values).then(parsed => setParsedData(parsed))
}
}, [data, setParsedData]);
return <Card>
<CardTabs tabs={[
{
name: t('html.label.all'), icon: faGears, color: 'blue-grey', href: 'all',
element: <AllGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
}, {
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
element: <TpsGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
}, {
name: t('html.label.cpuRam'), icon: faMicrochip, color: 'light-green', href: 'cpu-ram',
element: <CpuRamGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
}, {
name: t('html.label.world'), icon: faMap, color: 'purple', href: 'world-load',
element: <WorldGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
}, {
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
element: <PingGraphTab identifier={identifier}/>
}, {
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
element: <DiskGraphTab data={data} dataSeries={parsedData} loadingError={loadingError}/>
},
]}/>
</Card>
}
export default PerformanceGraphsCard;

View File

@ -8,6 +8,7 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faChartLine} from "@fortawesome/free-solid-svg-icons";
import React from "react";
import PlayerbaseGraph from "../../../graphs/PlayerbaseGraph";
import {CardLoader} from "../../../navigation/Loader";
const PlayerbaseDevelopmentCard = () => {
const {t} = useTranslation();
@ -17,8 +18,8 @@ const PlayerbaseDevelopmentCard = () => {
fetchPlayerbaseDevelopmentGraph,
[identifier])
if (!data) return <></>;
if (loadingError) return <ErrorViewCard error={loadingError}/>
if (!data) return <CardLoader/>;
return (
<Card>

View File

@ -3,21 +3,19 @@ import WorldPieCard from "../../common/WorldPieCard";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchWorldPie} from "../../../../service/serverService";
import {ErrorViewBody} from "../../../../views/ErrorView";
import {CardLoader} from "../../../navigation/Loader";
import {ErrorViewCard} from "../../../../views/ErrorView";
const ServerWorldPieCard = () => {
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(fetchWorldPie, [identifier]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <CardLoader/>;
if (loadingError) return <ErrorViewCard error={loadingError}/>
return (
<WorldPieCard
worldSeries={data.world_series}
gmSeries={data.gm_series}
worldSeries={data?.world_series}
gmSeries={data?.gm_series}
/>
)
}

View File

@ -7,10 +7,11 @@ import SmallTrend from "../../../trend/SmallTrend";
import {faCalendar, faCalendarPlus} from "@fortawesome/free-regular-svg-icons";
import ComparingLabel from "../../../trend/ComparingLabel";
import End from "../../../layout/End";
import {CardLoader} from "../../../navigation/Loader";
const OnlineActivityInsightsCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <></>
if (!data) return <CardLoader/>;
return (
<InsightsFor30DaysCard>

View File

@ -0,0 +1,29 @@
import React from "react";
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
import {useTranslation} from "react-i18next";
import Datapoint from "../../../Datapoint";
import {faDragon, faMap, faTachometerAlt, faUsers} from "@fortawesome/free-solid-svg-icons";
import {CardLoader} from "../../../navigation/Loader";
const PerformanceInsightsCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <CardLoader/>;
return (
<InsightsFor30DaysCard>
<p>{t('html.label.duringLowTps')}</p>
<Datapoint name={t('html.label.average') + ' ' + t('html.label.players')} icon={faUsers} color="red"
value={data.low_tps_players} bold/>
<Datapoint name={t('html.label.averageEntities')} icon={faDragon} color="red"
value={data.low_tps_entities}/>
<Datapoint name={t('html.label.averageChunks')} icon={faMap} color="red"
value={data.low_tps_chunks}/>
<Datapoint name={t('html.label.averageCpuUsage')} icon={faTachometerAlt} color="red"
value={data.low_tps_entities}/>
<Datapoint name={t('html.label.averageTps')} icon={faTachometerAlt} color="red"
value={data.low_tps_tps}/>
</InsightsFor30DaysCard>
)
}
export default PerformanceInsightsCard;

View File

@ -7,6 +7,7 @@ import {faLongArrowAltRight, faUser} from "@fortawesome/free-solid-svg-icons";
import SmallTrend from "../../../trend/SmallTrend";
import End from "../../../layout/End";
import ComparingLabel from "../../../trend/ComparingLabel";
import {CardLoader} from "../../../navigation/Loader";
const TwoPlayerChange = ({colorBefore, labelBefore, colorAfter, labelAfter}) => {
return (
@ -20,7 +21,7 @@ const TwoPlayerChange = ({colorBefore, labelBefore, colorAfter, labelAfter}) =>
const PlayerbaseInsightsCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <></>;
if (!data) return <CardLoader/>;
return (
<InsightsFor30DaysCard>
<Datapoint name={<TwoPlayerChange colorBefore={'light-green'}

View File

@ -3,10 +3,13 @@ import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
import Datapoint from "../../../Datapoint";
import {useTranslation} from "react-i18next";
import {faKhanda} from "@fortawesome/free-solid-svg-icons";
import {CardLoader} from "../../../navigation/Loader";
const PvpPveInsightsCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <CardLoader/>;
return (
<InsightsFor30DaysCard>
<Datapoint name={t('html.label.deadliestWeapon')} icon={faKhanda} color="amber"

View File

@ -3,7 +3,7 @@ import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchSessionOverview} from "../../../../service/serverService";
import ErrorView from "../../../../views/ErrorView";
import {ErrorViewCard} from "../../../../views/ErrorView";
import Datapoint from "../../../Datapoint";
import {useTranslation} from "react-i18next";
import {faGamepad, faUsers} from "@fortawesome/free-solid-svg-icons";
@ -15,22 +15,22 @@ const SessionInsightsCard = () => {
const {data, loadingError} = useDataRequest(fetchSessionOverview, [identifier]);
if (!data) return <></>;
if (loadingError) return <ErrorView error={loadingError}/>
if (loadingError) return <ErrorViewCard error={loadingError}/>
return (
<InsightsFor30DaysCard>
<Datapoint name={t('html.label.mostActiveGamemode')} icon={faGamepad} color="teal" bold
value={data.insights.most_active_gamemode} valueLabel={data.insights.most_active_gamemode_perc}
value={data?.insights.most_active_gamemode}
valueLabel={data?.insights.most_active_gamemode_perc}
/>
<Datapoint name={t('html.label.serverOccupied')} icon={faUsers} color="teal"
value={'~' + data.insights.server_occupied} valueLabel={data.insights.server_occupied_perc}
value={'~' + data?.insights.server_occupied} valueLabel={data?.insights.server_occupied_perc}
/>
<Datapoint name={t('html.label.playtime')} icon={faClock} color="green"
value={data.insights.total_playtime}
value={data?.insights.total_playtime}
/>
<Datapoint name={t('html.label.afkTime')} icon={faClock} color="grey"
value={data.insights.afk_time} valueLabel={data.insights.afk_time_perc}
value={data?.insights.afk_time} valueLabel={data?.insights.afk_time_perc}
/>
</InsightsFor30DaysCard>
)

View File

@ -4,10 +4,11 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faBookOpen} from "@fortawesome/free-solid-svg-icons";
import React from "react";
import OnlineActivityAsNumbersTable from "../../../table/OnlineActivityAsNumbersTable";
import {CardLoader} from "../../../navigation/Loader";
const OnlineActivityAsNumbersCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <></>
if (!data) return <CardLoader/>;
return (
<Card>

View File

@ -0,0 +1,16 @@
import React from 'react';
import PerformanceAsNumbersTable from "../../../table/PerformanceAsNumbersTable";
import CardHeader from "../../CardHeader";
import {faBookOpen} from "@fortawesome/free-solid-svg-icons";
import {Card} from "react-bootstrap-v5";
const PerformanceAsNumbersCard = ({data}) => {
return (
<Card>
<CardHeader icon={faBookOpen} color="blue-grey" label={'html.label.performanceAsNumbers'}/>
<PerformanceAsNumbersTable data={data}/>
</Card>
)
};
export default PerformanceAsNumbersCard

View File

@ -7,10 +7,11 @@ import BigTrend from "../../../trend/BigTrend";
import React from "react";
import {faClock} from "@fortawesome/free-regular-svg-icons";
import {TableRow} from "../../../table/TableRow";
import {CardLoader} from "../../../navigation/Loader";
const PlayerbaseTrendsCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <></>;
if (!data) return <CardLoader/>;
return (
<Card>
<Card.Header>

View File

@ -2,7 +2,7 @@ import React from "react";
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchSessions} from "../../../../service/serverService";
import {ErrorViewBody} from "../../../../views/ErrorView";
import {ErrorViewCard} from "../../../../views/ErrorView";
import RecentSessionsCard from "../../common/RecentSessionsCard";
const ServerRecentSessionsCard = () => {
@ -11,11 +11,10 @@ const ServerRecentSessionsCard = () => {
const {data, loadingError} = useDataRequest(fetchSessions, [identifier])
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <></>;
if (loadingError) return <ErrorViewCard error={loadingError}/>
return (
<RecentSessionsCard sessions={data.sessions} isPlayer={true}/>
<RecentSessionsCard sessions={data?.sessions} isPlayer={true}/>
)
}

View File

@ -7,10 +7,11 @@ import BigTrend from "../../../trend/BigTrend";
import {faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
import React from "react";
import {TableRow} from "../../../table/TableRow";
import {CardLoader} from "../../../navigation/Loader";
const ServerWeekComparisonCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <></>;
if (!data) return <CardLoader/>;
return (
<Card>
<Card.Header>
@ -30,6 +31,10 @@ const ServerWeekComparisonCard = ({data}) => {
text={t('html.label.averagePlaytime') + ' ' + t('html.label.perPlayer')}
values={[data.average_playtime_before, data.average_playtime_after,
<BigTrend trend={data.average_playtime_trend}/>]}/>
<TableRow icon={faClock} color="teal"
text={t('html.label.averageSessionLength')}
values={[data.session_length_average_before, data.session_length_average_after,
<BigTrend trend={data.session_length_average_trend}/>]}/>
<TableRow icon={faCalendarCheck} color="teal" text={t('html.label.sessions')}
values={[data.sessions_before, data.sessions_after,
<BigTrend trend={data.sessions_trend}/>]}/>

View File

@ -13,17 +13,18 @@ import {
import Datapoint from "../../../Datapoint";
import {faCalendarCheck, faClock} from "@fortawesome/free-regular-svg-icons";
import React from "react";
import {CardLoader} from "../../../navigation/Loader";
const ServerAsNumbersCard = ({data}) => {
const {t} = useTranslation();
if (!data) return <></>;
if (!data) return <CardLoader/>;
return (
<Card>
<Card.Header>
<h6 className="col-black">
<Fa icon={faBookOpen}/> {t('html.label.serverAsNumberse')}
<Fa icon={faBookOpen}/> {data.player_kills ? t('html.label.serverAsNumberse') : t('html.label.networkAsNumbers')}
</h6>
</Card.Header>
<Card.Body>
@ -54,10 +55,13 @@ const ServerAsNumbersCard = ({data}) => {
<Datapoint name={t('html.label.averagePlaytime') + ' ' + t('html.label.perPlayer')}
color={'green'} icon={faClock}
value={data.player_playtime}/>
<Datapoint name={t('html.label.averageSessionLength')}
color={'teal'} icon={faClock}
value={data.session_length_avg}/>
<Datapoint name={t('html.label.sessions')}
color={'teal'} icon={faCalendarCheck}
value={data.sessions} bold/>
<hr/>
{data.player_kills && <hr/>}
<Datapoint name={t('html.label.playerKills')}
color={'red'} icon={faCrosshairs}
value={data.player_kills} bold/>

View File

@ -3,7 +3,7 @@ import {Card, Col} from "react-bootstrap-v5";
import ExtensionIcon from "./ExtensionIcon";
import Datapoint from "../Datapoint";
import Masonry from 'masonry-layout'
import {useTheme} from "../../hooks/themeHook";
import ExtensionTable from "./ExtensionTable";
export const ExtensionCardWrapper = ({extension, children}) => {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
@ -51,32 +51,17 @@ const ExtensionValue = ({data}) => {
/>);
}
const ExtensionValues = ({tab}) => (
<Card.Body>
{tab.values.map((data, i) => {
return (<ExtensionValue key={i} data={data}/>);
}
)}
</Card.Body>
)
const ExtensionTable = ({table}) => {
const {nightModeEnabled} = useTheme();
const ExtensionValues = ({tab}) => {
return (
<table className={"table table-striped" + (nightModeEnabled ? " table-dark" : '')}>
<thead className={table.tableColorClass}>
<tr>
{table.table.columns.map((column, i) => <th key={i}><ExtensionIcon
icon={table.table.icons[i]}/> {column}
</th>)}
</tr>
</thead>
<tbody>
{table.table.rows.map((row, i) => <tr key={i}>{row.map((value, j) => <td
key={i + '' + j}>{value}</td>)}</tr>)}
</tbody>
</table>
);
<>
{Boolean(tab.values.length) && <Card.Body>
{tab.values.map((data, i) => {
return (<ExtensionValue key={i} data={data}/>);
}
)}
</Card.Body>}
</>
)
}
const ExtensionTables = ({tab}) => {
@ -113,7 +98,7 @@ const ExtensionCard = ({extension}) => {
{extension.onlyGenericTab ? '' :
extension.tabs.map((tab, i) => <li key={i} role="presentation" className="nav-item col-black">
<button className={"nav-link col-black"
+ (openTabIndex === i ? ' active' : '')} onClick={() => toggleTabIndex(i)}>
+ (openTabIndex === i ? ' active' : '')} onClick={() => toggleTabIndex(i)}>
<ExtensionIcon icon={tab.tabInformation.icon}/> {tab.tabInformation.tabName}
</button>
</li>)

View File

@ -2,13 +2,19 @@ import {FontAwesomeIcon as Fa} from '@fortawesome/react-fontawesome'
import {iconTypeToFontAwesomeClass} from "../../util/icons";
import React from "react";
const ExtensionIcon = ({icon}) => (
<Fa icon={[
iconTypeToFontAwesomeClass(icon.family),
icon.iconName
]}
className={icon.colorClass}
/>
)
const ExtensionIcon = ({icon}) => {
return (
<Fa icon={[
iconTypeToFontAwesomeClass(icon.family),
icon.iconName
]}
className={icon.colorClass}
/>
)
}
export const toExtensionIconHtmlString = ({icon}) => {
return icon ? `<i class="${iconTypeToFontAwesomeClass(icon.family)} ${icon.iconName} ${icon.colorClass}"></i>` : '';
}
export default ExtensionIcon;

View File

@ -0,0 +1,77 @@
import React, {useState} from 'react';
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
import ExtensionIcon, {toExtensionIconHtmlString} from "./ExtensionIcon";
import DataTablesTable from "../table/DataTablesTable";
const ExtensionDataTable = ({table}) => {
const [id] = useState("extension-table-" + new Date().getTime() + "-" + (Math.floor(Math.random() * 100000)));
const data = {
columns: table.table.columns.map((column, i) => {
return {
title: toExtensionIconHtmlString(table.table.icons[i]) + ' ' + column,
data: {
"_": `col${i}.v`,
display: `col${i}.d`
},
};
}),
data: table.table.rows.map((row) => {
const dataRow = {};
row.forEach((cell, j) => dataRow[`col${j}`] = {
v: cell['valueUnformatted'] || cell.value || cell,
d: cell.value || cell
});
return dataRow;
})
};
const options = {
responsive: true,
deferRender: true,
columns: data.columns,
data: data.data,
order: [[1, "desc"]]
}
return (
<DataTablesTable id={id} options={options}/>
)
}
const ExtensionColoredTable = ({table}) => {
const {nightModeEnabled} = useTheme();
const {t} = useTranslation();
const rows = table.table.rows.length ? table.table.rows.map((row, i) => <tr key={i}>{row.map((value, j) => <td
key={i + '' + j}>{value.value || String(value)}</td>)}</tr>) :
<tr>{table.table.columns.map((column, i) =>
<td key={i}>{i === 0 ? t('generic.noData') : '-'}</td>)}
</tr>
return (
<table className={"table table-striped" + (nightModeEnabled ? " table-dark" : '')}>
<thead className={table.tableColorClass}>
<tr>
{table.table.columns.map((column, i) => <th key={i}><ExtensionIcon
icon={table.table.icons[i]}/> {column}
</th>)}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
)
}
const ExtensionTable = ({table}) => {
const tableLength = table.table.rows.length;
if (tableLength > 10) {
return <ExtensionDataTable table={table}/>
} else {
return <ExtensionColoredTable table={table}/>
}
}
export default ExtensionTable

View File

@ -3,6 +3,7 @@ import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import Highcharts from "highcharts";
import Accessibility from "highcharts/modules/accessibility";
const GeolocationBarGraph = ({series, color}) => {
const {t} = useTranslation();
@ -17,6 +18,7 @@ const GeolocationBarGraph = ({series, color}) => {
data: bars
};
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.chart("countryBarChart", {
chart: {type: 'bar'},

View File

@ -4,6 +4,7 @@ import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import Highcharts from 'highcharts/highmaps.js';
import map from '@highcharts/map-collection/custom/world.geo.json';
import Accessibility from "highcharts/modules/accessibility";
const GeolocationWorldMap = ({series, colors}) => {
const {t} = useTranslation();
@ -18,6 +19,7 @@ const GeolocationWorldMap = ({series, colors}) => {
joinBy: ['iso-a3', 'code']
};
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.mapChart('countryWorldMap', {
chart: {

View File

@ -3,14 +3,16 @@ import React, {useEffect} from "react";
import {linegraphButtons} from "../../util/graphs";
import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display"
import Accessibility from "highcharts/modules/accessibility"
import {useTranslation} from "react-i18next";
const LineGraph = ({id, series}) => {
const {t} = useTranslation()
const {graphTheming} = useTheme();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, {
@ -25,12 +27,12 @@ const LineGraph = ({id, series}) => {
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: 0.4
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
series: series
})
}, [series, graphTheming, id, t])
}, [series, graphTheming, id, t, nightModeEnabled])
return (
<div className="chart-area" id={id}>

View File

@ -2,10 +2,13 @@ import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {tooltip} from "../../util/graphs";
import LineGraph from "./LineGraph";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
const PingGraph = ({data}) => {
const PlayerPingGraph = ({data}) => {
const {t} = useTranslation();
const [series, setSeries] = useState([]);
const {nightModeEnabled} = useTheme();
useEffect(() => {
const avgPingSeries = {
@ -13,28 +16,28 @@ const PingGraph = ({data}) => {
type: 'spline',
tooltip: tooltip.twoDecimals,
data: data.avg_ping_series,
color: data.colors.avg
color: nightModeEnabled ? withReducedSaturation(data.colors.avg) : data.colors.avg
}
const maxPingSeries = {
name: t('html.label.worstPing'),
type: 'spline',
tooltip: tooltip.twoDecimals,
data: data.max_ping_series,
color: data.colors.max
color: nightModeEnabled ? withReducedSaturation(data.colors.max) : data.colors.max
}
const minPingSeries = {
name: t('html.label.bestPing'),
type: 'spline',
tooltip: tooltip.twoDecimals,
data: data.min_ping_series,
color: data.colors.min
color: nightModeEnabled ? withReducedSaturation(data.colors.min) : data.colors.min
}
setSeries([avgPingSeries, maxPingSeries, minPingSeries]);
}, [data, t])
}, [data, t, nightModeEnabled])
return (
<LineGraph id="ping-graph" series={series}/>
)
}
export default PingGraph
export default PlayerPingGraph

View File

@ -4,6 +4,7 @@ import {useTheme} from "../../hooks/themeHook";
import NoDataDisplay from "highcharts/modules/no-data-to-display";
import Highcharts from "highcharts/highstock";
import {withReducedSaturation} from "../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
const PlayerbaseGraph = ({data}) => {
const {t} = useTranslation()
@ -17,6 +18,7 @@ const PlayerbaseGraph = ({data}) => {
});
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);

View File

@ -3,6 +3,7 @@ import Highcharts from 'highcharts';
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import {useTranslation} from "react-i18next";
import Accessibility from "highcharts/modules/accessibility";
const PlayerbasePie = ({series}) => {
const {t} = useTranslation();
@ -19,6 +20,7 @@ const PlayerbasePie = ({series}) => {
data: nightModeEnabled ? reduceColors(series) : series
};
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.chart('playerbase-pie', {
chart: {

View File

@ -2,16 +2,19 @@ import React, {useEffect} from "react";
import Highcharts from 'highcharts';
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
import Accessibility from "highcharts/modules/accessibility";
import {withReducedSaturation} from "../../util/colors";
const PunchCard = ({series}) => {
const {t} = useTranslation();
const {graphTheming} = useTheme();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
const punchCard = {
name: t('html.label.relativeJoinActivity'),
color: '#222',
color: nightModeEnabled ? withReducedSaturation('#222') : '#222',
data: series
};
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
setTimeout(() => Highcharts.chart('punchcard', {
chart: {
@ -44,7 +47,7 @@ const PunchCard = ({series}) => {
},
series: [punchCard]
}), 25)
}, [series, graphTheming, t])
}, [series, graphTheming, t, nightModeEnabled])
return (
<div className="chart-area" id="punchcard">

View File

@ -5,6 +5,7 @@ import {formatTimeAmount} from '../../util/formatters'
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import {useTranslation} from "react-i18next";
import Accessibility from "highcharts/modules/accessibility";
const ServerPie = ({colors, series}) => {
const {t} = useTranslation();
@ -20,6 +21,7 @@ const ServerPie = ({colors, series}) => {
data: series
};
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
Highcharts.chart('server-pie', {
chart: {

View File

@ -2,10 +2,13 @@ import {useTranslation} from "react-i18next";
import React, {useEffect, useState} from "react";
import {tooltip} from "../../util/graphs";
import LineGraph from "./LineGraph";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
const TimeByTimeGraph = ({data}) => {
const {t} = useTranslation();
const [series, setSeries] = useState([]);
const {nightModeEnabled} = useTheme();
useEffect(() => {
const uniquePlayers = {
@ -13,17 +16,17 @@ const TimeByTimeGraph = ({data}) => {
type: 'spline',
tooltip: tooltip.zeroDecimals,
data: data.uniquePlayers,
color: data.colors.playersOnline
color: nightModeEnabled ? withReducedSaturation(data.colors.playersOnline) : data.colors.playersOnline
};
const newPlayers = {
name: t('html.label.newPlayers'),
type: 'spline',
tooltip: tooltip.zeroDecimals,
data: data.newPlayers,
color: data.colors.newPlayers
color: nightModeEnabled ? withReducedSaturation(data.colors.newPlayers) : data.colors.newPlayers
};
setSeries([uniquePlayers, newPlayers]);
}, [data, t])
}, [data, t, nightModeEnabled])
return (
<LineGraph id="day-by-day-graph" series={series}/>

View File

@ -7,6 +7,7 @@ import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import {useMetadata} from "../../hooks/metadataHook";
import {useTranslation} from "react-i18next";
import Accessibility from "highcharts/modules/accessibility";
const WorldPie = ({id, worldSeries, gmSeries}) => {
const {t} = useTranslation();
@ -33,6 +34,7 @@ const WorldPie = ({id, worldSeries, gmSeries}) => {
const defaultTitle = '';
const defaultSubtitle = t('html.text.clickToExpand');
Accessibility(Highcharts);
Highcharts.setOptions(graphTheming);
setTimeout(() => {
const chart = Highcharts.chart(id, {

View File

@ -0,0 +1,184 @@
import React, {useCallback, useEffect} from 'react';
import {linegraphButtons, tooltip} from "../../../util/graphs";
import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display"
import {useTranslation} from "react-i18next";
import {useTheme} from "../../../hooks/themeHook";
import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
const yAxis = [
{
labels: {
formatter: function () {
return this.value + ' P';
}
},
softMin: 0,
softMax: 2
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + ' TPS';
}
},
softMin: 0,
softMax: 20
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + '%';
}
},
softMin: 0,
softMax: 100
}, {
labels: {
formatter: function () {
return this.value + ' MB';
}
},
softMin: 0
}, {
opposite: true,
labels: {
formatter: function () {
return this.value + ' E';
}
},
softMin: 0,
softMax: 2
}, {
labels: {
formatter: function () {
return this.value + ' C';
}
},
softMin: 0
}
]
const AllPerformanceGraph = ({id, data, dataSeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
const onResize = useCallback(() => {
let chartElement = document.getElementById(id);
let chartId = chartElement?.getAttribute('data-highcharts-chart');
const chart = chartId !== undefined ? Highcharts.charts[chartId] : undefined;
if (chart && chart.yAxis && chart.yAxis.length) {
const newWidth = window.innerWidth
chart.yAxis[0].update({labels: {enabled: newWidth >= 900}});
chart.yAxis[1].update({labels: {enabled: newWidth >= 900}});
chart.yAxis[2].update({labels: {enabled: newWidth >= 1000}});
chart.yAxis[3].update({labels: {enabled: newWidth >= 1000}});
chart.yAxis[4].update({labels: {enabled: newWidth >= 1400}});
chart.yAxis[5].update({labels: {enabled: newWidth >= 1400}});
}
}, [id])
useEffect(() => {
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
}
}, [onResize])
useEffect(() => {
const zones = {
tps: [{
value: data.zones.tpsThresholdMed,
color: nightModeEnabled ? withReducedSaturation(data.colors.low) : data.colors.low
}, {
value: data.zones.tpsThresholdHigh,
color: nightModeEnabled ? withReducedSaturation(data.colors.med) : data.colors.med
}, {
value: 30,
color: nightModeEnabled ? withReducedSaturation(data.colors.high) : data.colors.high
}]
};
const spline = 'spline'
const series = {
playersOnline: {
name: t('html.label.playersOnline'),
type: 'areaspline',
tooltip: tooltip.zeroDecimals,
data: dataSeries.playersOnline,
color: data.colors.playersOnline,
yAxis: 0
}, tps: {
name: t('html.label.tps'),
type: spline,
color: nightModeEnabled ? withReducedSaturation(data.colors.high) : data.colors.high,
zones: zones.tps,
tooltip: tooltip.twoDecimals,
data: dataSeries.tps,
yAxis: 1
}, cpu: {
name: t('html.label.cpu'),
type: spline,
tooltip: tooltip.twoDecimals,
data: dataSeries.cpu,
color: nightModeEnabled ? withReducedSaturation(data.colors.cpu) : data.colors.cpu,
yAxis: 2
}, ram: {
name: t('html.label.ram'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: dataSeries.ram,
color: nightModeEnabled ? withReducedSaturation(data.colors.ram) : data.colors.ram,
yAxis: 3
}, entities: {
name: t('html.label.loadedEntities'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: dataSeries.entities,
color: nightModeEnabled ? withReducedSaturation(data.colors.entities) : data.colors.entities,
yAxis: 4
}, chunks: {
name: t('html.label.loadedChunks'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: dataSeries.chunks,
color: nightModeEnabled ? withReducedSaturation(data.colors.chunks) : data.colors.chunks,
yAxis: 5
}
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
yAxis,
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: true
},
series: [series.playersOnline, series.tps, series.cpu, series.ram, series.entities, series.chunks]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>
<span className="loader"/>
</div>
)
};
export default AllPerformanceGraph

View File

@ -0,0 +1,96 @@
import React, {useEffect} from 'react';
import {linegraphButtons, tooltip} from "../../../util/graphs";
import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display"
import {useTranslation} from "react-i18next";
import {useTheme} from "../../../hooks/themeHook";
import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
const CpuRamPerformanceGraph = ({id, data, dataSeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
const spline = 'spline'
const series = {
playersOnline: {
name: t('html.label.playersOnline'),
type: 'areaspline',
tooltip: tooltip.zeroDecimals,
data: dataSeries.playersOnline,
color: data.colors.playersOnline,
yAxis: 0
}, cpu: {
name: t('html.label.cpu'),
type: spline,
tooltip: tooltip.twoDecimals,
data: dataSeries.cpu,
color: nightModeEnabled ? withReducedSaturation(data.colors.cpu) : data.colors.cpu,
yAxis: 1
}, ram: {
name: t('html.label.ram'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: dataSeries.ram,
color: nightModeEnabled ? withReducedSaturation(data.colors.ram) : data.colors.ram,
yAxis: 2
}
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, {
rangeSelector: {
selected: 1, // TODO Sync range selectors state
buttons: linegraphButtons
},
yAxis: [{
labels: {
formatter: function () {
return this.value + ' ' + t('html.unit.players')
}
},
softMin: 0,
softMax: 2
}, {
labels: {
formatter: function () {
return this.value + ' %'
}
},
softMin: 0,
softMax: 100
}, {
labels: {
formatter: function () {
return this.value + ' MB'
}
},
softMin: 0
}],
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: true
},
series: [series.playersOnline, series.cpu, series.ram]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>
<span className="loader"/>
</div>
)
};
export default CpuRamPerformanceGraph

View File

@ -0,0 +1,77 @@
import React, {useEffect} from 'react';
import {linegraphButtons, tooltip} from "../../../util/graphs";
import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display"
import {useTranslation} from "react-i18next";
import {useTheme} from "../../../hooks/themeHook";
import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
const DiskPerformanceGraph = ({id, data, dataSeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
const zones = {
disk: [{
value: data.zones.diskThresholdMed,
color: data.colors.low
}, {
value: data.zones.diskThresholdHigh,
color: data.colors.med
}, {
value: Number.MAX_VALUE,
color: data.colors.high
}]
};
const series = {
disk: {
name: t('html.label.disk'),
type: 'areaspline',
color: nightModeEnabled ? withReducedSaturation(data.colors.high) : data.colors.high,
zones: zones.disk,
tooltip: tooltip.zeroDecimals,
data: dataSeries.disk
}
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
yAxis: {
labels: {
formatter: function () {
return this.value + ' MB';
}
},
softMin: 0
},
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: true
},
series: [series.disk]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>
<span className="loader"/>
</div>
)
};
export default DiskPerformanceGraph

View File

@ -0,0 +1,74 @@
import React, {useEffect} from 'react';
import {linegraphButtons, tooltip} from "../../../util/graphs";
import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display"
import {useTranslation} from "react-i18next";
import {useTheme} from "../../../hooks/themeHook";
import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
const PingGraph = ({id, data}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
const spline = 'spline'
const series = {
avgPing: {
name: t('html.label.averagePing'),
type: spline,
tooltip: tooltip.twoDecimals,
data: data.avg_ping_series,
color: nightModeEnabled ? withReducedSaturation(data.colors.avg) : data.colors.avg,
},
maxPing: {
name: t('html.label.worstPing'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: data.max_ping_series,
color: nightModeEnabled ? withReducedSaturation(data.colors.max) : data.colors.max,
},
minPing: {
name: t('html.label.bestPing'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: data.min_ping_series,
color: nightModeEnabled ? withReducedSaturation(data.colors.min) : data.colors.min,
}
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
yAxis: {
labels: {
formatter: function () {
return this.value + ' ms'
}
},
softMin: 0
},
title: {text: ''},
legend: {
enabled: true
},
series: [series.avgPing, series.maxPing, series.minPing]
});
}, [data, graphTheming, nightModeEnabled, id, t])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>
<span className="loader"/>
</div>
)
};
export default PingGraph

View File

@ -0,0 +1,93 @@
import React, {useEffect} from 'react';
import {linegraphButtons, tooltip} from "../../../util/graphs";
import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display"
import {useTranslation} from "react-i18next";
import {useTheme} from "../../../hooks/themeHook";
import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
const TpsPerformanceGraph = ({id, data, dataSeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
const zones = {
tps: [{
value: data.zones.tpsThresholdMed,
color: nightModeEnabled ? withReducedSaturation(data.colors.low) : data.colors.low
}, {
value: data.zones.tpsThresholdHigh,
color: nightModeEnabled ? withReducedSaturation(data.colors.med) : data.colors.med
}, {
value: 30,
color: nightModeEnabled ? withReducedSaturation(data.colors.high) : data.colors.high
}]
};
const spline = 'spline'
const series = {
playersOnline: {
name: t('html.label.playersOnline'),
type: 'areaspline',
tooltip: tooltip.zeroDecimals,
data: dataSeries.playersOnline,
color: data.colors.playersOnline,
yAxis: 0
}, tps: {
name: t('html.label.tps'),
type: spline,
color: nightModeEnabled ? withReducedSaturation(data.colors.high) : data.colors.high,
zones: zones.tps,
tooltip: tooltip.twoDecimals,
data: dataSeries.tps,
yAxis: 1
}
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, {
rangeSelector: {
selected: 1,
buttons: linegraphButtons
},
yAxis: [{
labels: {
formatter: function () {
return this.value + ' ' + t('html.unit.players')
}
},
softMin: 0,
softMax: 2
}, {
labels: {
formatter: function () {
return this.value + ' ' + t('html.label.tps')
}
}
}],
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: true
},
series: [series.playersOnline, series.tps]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>
<span className="loader"/>
</div>
)
};
export default TpsPerformanceGraph

View File

@ -0,0 +1,95 @@
import React, {useEffect} from 'react';
import {linegraphButtons, tooltip} from "../../../util/graphs";
import Highcharts from "highcharts/highstock";
import NoDataDisplay from "highcharts/modules/no-data-to-display"
import {useTranslation} from "react-i18next";
import {useTheme} from "../../../hooks/themeHook";
import {withReducedSaturation} from "../../../util/colors";
import Accessibility from "highcharts/modules/accessibility";
const WorldPerformanceGraph = ({id, data, dataSeries}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
const spline = 'spline'
const series = {
playersOnline: {
name: t('html.label.playersOnline'),
type: 'areaspline',
tooltip: tooltip.zeroDecimals,
data: dataSeries.playersOnline,
color: data.colors.playersOnline,
yAxis: 0
}, entities: {
name: t('html.label.loadedEntities'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: dataSeries.entities,
color: nightModeEnabled ? withReducedSaturation(data.colors.entities) : data.colors.entities,
yAxis: 1
}, chunks: {
name: t('html.label.loadedChunks'),
type: spline,
tooltip: tooltip.zeroDecimals,
data: dataSeries.chunks,
color: nightModeEnabled ? withReducedSaturation(data.colors.chunks) : data.colors.chunks,
yAxis: 2
}
};
NoDataDisplay(Highcharts);
Accessibility(Highcharts);
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
Highcharts.setOptions(graphTheming);
Highcharts.stockChart(id, {
rangeSelector: {
selected: 2,
buttons: linegraphButtons
},
yAxis: [{
labels: {
formatter: function () {
return this.value + ' ' + t('html.unit.players')
}
},
softMin: 0,
softMax: 2
}, {
labels: {
formatter: function () {
return this.value + ' ' + t('html.label.entities')
}
},
softMin: 0,
}, {
labels: {
formatter: function () {
return this.value + ' ' + t('html.unit.chunks')
}
},
softMin: 0,
}],
title: {text: ''},
plotOptions: {
areaspline: {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: true
},
series: [series.playersOnline, series.entities, series.chunks]
});
}, [data, dataSeries, graphTheming, nightModeEnabled, id, t])
return (
<div className="chart-area" style={{height: "450px"}} id={id}>
<span className="loader"/>
</div>
)
};
export default WorldPerformanceGraph

View File

@ -7,7 +7,7 @@ import {Modal} from "react-bootstrap-v5";
import {useTranslation} from "react-i18next";
const ColorSelectorButton = ({color, setColor, disabled}) =>
<button className={"btn color-chooser " + colorEnumToBgClass(color)}
<button className={"btn color-chooser " + colorEnumToBgClass(color) + (disabled ? " disabled" : '')}
id={"choose-" + color}
disabled={disabled}
onClick={() => setColor(color)}
@ -24,7 +24,7 @@ const ColorSelectorModal = () => {
aria-labelledby="colorChooserModalLabel"
show={theme.colorChooserOpen}
onHide={theme.toggleColorChooser}>
<Modal.Header>
<Modal.Header className="bg-white">
<Modal.Title id="colorChooserModalLabel">
<Fa icon={faPalette}/> {t('html.label.themeSelect')}
</Modal.Title>

View File

@ -0,0 +1,37 @@
import React from 'react';
import {Modal} from "react-bootstrap-v5";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faHandPointRight} from "@fortawesome/free-regular-svg-icons";
import {useTranslation} from "react-i18next";
import {useMetadata} from "../../hooks/metadataHook";
import {Link} from "react-router-dom";
const ForgotPasswordModal = ({show, toggle}) => {
const {t} = useTranslation();
const {mainCommand} = useMetadata();
return (
<Modal id="forgotPasswordModal"
aria-labelledby="forgotModalLabel"
show={show}
onHide={toggle}
>
<Modal.Header className="bg-white">
<Modal.Title id="forgotModalLabel">
<Fa icon={faHandPointRight}/> {t('html.login.forgotPassword1')}
</Modal.Title>
<button aria-label="Close" className="btn-close" onClick={toggle}/>
</Modal.Header>
<Modal.Body className="bg-white">
<p>{t('html.login.forgotPassword2')}</p>
<p><code>/{mainCommand || 'plan'} unregister</code></p>
<p>{t('html.login.forgotPassword3')}</p>
<p><code>/{mainCommand || 'plan'} unregister [username]</code></p>
<p>{t('html.login.forgotPassword4')} <Link to="/register"
className="col-plan">{t('html.login.register')}</Link></p>
</Modal.Body>
</Modal>
)
};
export default ForgotPasswordModal

View File

@ -10,6 +10,7 @@ import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
import {localeService} from "../../service/localeService";
import {useTranslation} from "react-i18next";
import {useNavigation} from "../../hooks/navigationHook";
import {baseAddress} from "../../service/backendConfiguration";
const LanguageSelector = () => {
const languages = localeService.getLanguages();
@ -84,7 +85,7 @@ const Header = ({page, tab}) => {
<DropdownItem onClick={toggleColorChooser}>
<Fa icon={faPalette}/> {t('html.label.themeSelect')}
</DropdownItem>
{authRequired ? <DropdownItem href="./auth/logout">
{authRequired ? <DropdownItem href={baseAddress + "/auth/logout"}>
<Fa icon={faDoorOpen}/> {t('html.login.logout')}
</DropdownItem> : ''}
</DropdownMenu>

View File

@ -14,8 +14,8 @@ export const CardLoader = () => {
)
}
export const ChartLoader = () => {
return <div className="chart-area loading">
export const ChartLoader = ({style}) => {
return <div className="chart-area loading" style={style}>
<Loader/>
</div>
}

View File

@ -49,26 +49,25 @@ const MainPageRedirect = () => {
const {authLoaded, authRequired, loggedIn, user} = useAuth();
const {isProxy, serverName} = useMetadata();
console.log(authLoaded, authRequired, loggedIn, user)
if (!authLoaded || !serverName) {
return <RedirectPlaceholder/>
}
if (authRequired && !loggedIn) {
return (<Navigate to={"login"} replace={true}/>)
return (<Navigate to="/login" replace={true}/>)
} else if (authRequired && loggedIn) {
if (isProxy && user.permissions.includes('page.network')) {
return (<Navigate to={"network/overview"} replace={true}/>)
return (<Navigate to={"/network/overview"} replace={true}/>)
} else if (user.permissions.includes('page.server')) {
return (<Navigate to={"server/overview"} replace={true}/>)
return (<Navigate to={"/server/" + encodeURIComponent(serverName) + "/overview"} replace={true}/>)
} else if (user.permissions.includes('page.player.other')) {
return (<Navigate to={"players"} replace={true}/>)
return (<Navigate to={"/players"} replace={true}/>)
} else if (user.permissions.includes('page.player.self')) {
return (<Navigate to={"player/" + user.linkedToUuid} replace={true}/>)
return (<Navigate to={"/player/" + user.linkedToUuid} replace={true}/>)
}
} else {
return (<Navigate to={isProxy ? "network/overview" : "server/" + encodeURIComponent(serverName) + "/overview"}
return (<Navigate to={isProxy ? "/network/overview" : "/server/" + encodeURIComponent(serverName) + "/overview"}
replace={true}/>)
}
}

View File

@ -23,11 +23,16 @@ const Divider = ({showMargin}) => (
<hr className={"sidebar-divider" + (showMargin ? '' : " my-0")}/>
)
const InnerItem = ({href, icon, name, nameShort}) => {
const InnerItem = ({href, icon, name, nameShort, color}) => {
if (!href) {
return (<hr className={"nav-servers dropdown-divider mx-3 my-2"}/>)
}
if (href.startsWith('/')) {
return (
<a href={href} className="collapse-item nav-button">
<Fa icon={icon}/> <span>{nameShort ? nameShort : name}</span>
<Fa icon={icon} className={color ? "col-" + color : undefined}/>
<span>{nameShort ? nameShort : name}</span>
</a>
)
}
@ -35,11 +40,11 @@ const InnerItem = ({href, icon, name, nameShort}) => {
return <NavLink to={href} className={({isActive}) => {
return isActive ? "collapse-item nav-button active" : "collapse-item nav-button"
}}>
<Fa icon={icon}/> <span>{nameShort ? nameShort : name}</span>
<Fa icon={icon} className={color ? "col-" + color : undefined}/> <span>{nameShort ? nameShort : name}</span>
</NavLink>
}
const Item = ({href, icon, name, nameShort, inner}) => {
const Item = ({href, icon, name, nameShort, color, inner}) => {
const {setCurrentTab} = useNavigation();
const {pathname} = useLocation();
const {t} = useTranslation();
@ -49,14 +54,15 @@ const Item = ({href, icon, name, nameShort, inner}) => {
}, [pathname, href, setCurrentTab, name])
if (inner) {
return (<InnerItem href={href} icon={icon} name={t(name)} nameShort={t(nameShort)}/>)
return (<InnerItem href={href} icon={icon} name={t(name)} nameShort={t(nameShort)} color={color}/>)
}
if (href.startsWith('/')) {
return (
<li className={"nav-item nav-button"}>
<a href={baseAddress + href} className="nav-link">
<Fa icon={icon}/> <span>{t(nameShort ? nameShort : name)}</span>
<Fa icon={icon} className={color ? "col-" + color : undefined}/>
<span>{t(nameShort ? nameShort : name)}</span>
</a>
</li>
)
@ -67,7 +73,7 @@ const Item = ({href, icon, name, nameShort, inner}) => {
<NavLink to={href} className={({isActive}) => {
return isActive ? "nav-link active" : "nav-link"
}}>
<Fa icon={icon}/> <span>{t(name)}</span>
<Fa icon={icon} className={color ? "col-" + color : undefined}/> <span>{t(name)}</span>
</NavLink>
</li>
);
@ -151,7 +157,8 @@ const SidebarCollapse = ({item, open, setOpen}) => {
aria-expanded={open}
data-bs-toggle="collapse"
>
<Fa icon={item.icon}/> <span>{t(item.name)}</span>
<Fa icon={item.icon} className={item?.color ? "col-" + item?.color : undefined}/>
<span>{t(item.name)}</span>
</button>
<Collapse in={open}>
<div id={item.name + "-collapse"}>
@ -164,6 +171,7 @@ const SidebarCollapse = ({item, open, setOpen}) => {
icon={content.icon}
name={content.name}
nameShort={content.nameShort}
color={content.color}
/>)}
</div>
</div>
@ -186,6 +194,7 @@ const renderItem = (item, i, openCollapse, setOpenCollapse, t) => {
href={item.href}
icon={item.icon}
name={item.name}
color={item.color}
nameShort={item.nameShort}
/>
}
@ -217,22 +226,20 @@ const Sidebar = ({items, showBackButton}) => {
const collapseSidebar = () => setSidebarExpanded(windowWidth > 1350);
useEffect(collapseSidebar, [windowWidth, currentTab, setSidebarExpanded]);
if (!items.length) return <></>
return (
<>
{sidebarExpanded &&
<ul className={"navbar-nav sidebar sidebar-dark accordion bg-" + color} id="accordionSidebar">
<Logo/>
<Divider/>
{showBackButton && <>
<Item active={false} href="/" icon={faArrowLeft} name={t('html.label.toMainPage')}/>
<Divider showMargin={!items[0].contents && items[0].href === undefined}/>
</>}
{items.map((item, i) => renderItem(item, i, openCollapse, toggleCollapse, t))}
<Divider/>
<FooterButtons/>
</ul>}
<ul className={"navbar-nav sidebar sidebar-dark accordion bg-" + color} id="accordionSidebar">
<Logo/>
<Divider/>
{showBackButton && <>
<Item active={false} href="/" icon={faArrowLeft} name={t('html.label.toMainPage')}/>
<Divider showMargin={items.length && !items[0].contents && items[0].href === undefined}/>
</>}
{items.length ? items.map((item, i) => renderItem(item, i, openCollapse, toggleCollapse, t)) : ''}
<Divider/>
<FooterButtons/>
</ul>}
</>
)
}

View File

@ -4,9 +4,11 @@ import 'datatables.net-bs5'
import 'datatables.net-responsive-bs5'
import 'datatables.net-bs5/css/dataTables.bootstrap5.min.css';
import 'datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css';
import {useTheme} from "../../hooks/themeHook";
const DataTablesTable = ({id, options}) => {
const dataTableRef = useRef(null);
const {nightModeEnabled} = useTheme();
useEffect(() => {
const idSelector = `#${id}`;
@ -23,8 +25,13 @@ const DataTablesTable = ({id, options}) => {
};
}, [id, options, dataTableRef]);
// The surrounding div is required: jquery Datatables changes the DOM around table and React needs to have a node
// that wasn't changed.
return (
<table id={id} className="table table-bordered table-striped" style={{width: "100%"}}/>
<div>
<table id={id} className={"table table-bordered table-striped" + (nightModeEnabled ? " table-dark" : '')}
style={{width: "100%"}}/>
</div>
)
};

View File

@ -3,6 +3,7 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faAngleRight, faSkullCrossbones} from "@fortawesome/free-solid-svg-icons";
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
import Scrollable from "../Scrollable";
const KillRow = ({kill}) => {
const killSeparator = <Fa
@ -23,16 +24,18 @@ const KillsTable = ({kills}) => {
const {nightModeEnabled} = useTheme();
return (
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<tbody>
{kills.length ? kills.map((kill, i) => <KillRow key={i} kill={kill}/>) : <tr>
<td>{t('html.generic.none')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
<Scrollable>
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<tbody>
{kills.length ? kills.map((kill, i) => <KillRow key={i} kill={kill}/>) : <tr>
<td>{t('html.generic.none')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
</Scrollable>
)
};

View File

@ -6,10 +6,11 @@ import ComparisonTable from "./ComparisonTable";
import SmallTrend from "../trend/SmallTrend";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faCalendarCheck, faClock, faEye} from "@fortawesome/free-regular-svg-icons";
import {CardLoader} from "../navigation/Loader";
const OnlineActivityAsNumbersTable = ({data}) => {
const {t} = useTranslation();
if (!data) return <></>;
if (!data) return <CardLoader/>;
return (
<ComparisonTable

View File

@ -0,0 +1,96 @@
import {useTranslation} from "react-i18next";
import {
faDragon,
faExclamationCircle,
faHdd,
faMap,
faMicrochip,
faPowerOff,
faTachometerAlt,
faUser
} from "@fortawesome/free-solid-svg-icons";
import React from "react";
import {TableRow} from "./TableRow";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faEye} from "@fortawesome/free-regular-svg-icons";
import AsNumbersTable from "./AsNumbersTable";
import {CardLoader} from "../navigation/Loader";
const PerformanceAsNumbersTable = ({data}) => {
const {t} = useTranslation();
if (!data) return <CardLoader/>;
return (
<AsNumbersTable
headers={[t('html.label.last30days'), t('html.label.last7days'), t('html.label.last24hours')]}
>
<TableRow icon={faExclamationCircle} color="red" text={t('html.label.lowTpsSpikes')}
values={[
data.low_tps_spikes_30d,
data.low_tps_spikes_7d,
data.low_tps_spikes_24h
]}/>
<TableRow icon={faPowerOff} color="red"
text={t('html.label.serverDowntime') + ' (' + t('generic.noData') + ')'}
values={[
data.server_downtime_30d,
data.server_downtime_7d,
data.server_downtime_24h
]}/>
<TableRow icon={faUser} color="light-blue" text={t('html.label.averagePlayers')}
values={[
data.players_30d,
data.players_7d,
data.players_24h
]}/>
<TableRow icon={faTachometerAlt} color="orange" text={t('html.label.averageTps')}
values={[
data.tps_30d,
data.tps_7d,
data.tps_24h
]}/>
<TableRow icon={faTachometerAlt} color="amber" text={t('html.label.averageCpuUsage')}
values={[
data.cpu_30d,
data.cpu_7d,
data.cpu_24h
]}/>
<TableRow icon={faMicrochip} color="light-green" text={t('html.label.averageRamUsage')}
values={[
data.ram_30d,
data.ram_7d,
data.ram_24h
]}/>
<TableRow icon={faDragon} color="purple" text={t('html.label.averageEntities')}
values={[
data.entities_30d,
data.entities_7d,
data.entities_24h
]}/>
<TableRow icon={faMap} color="blue-grey"
text={<>{t('html.label.averageChunks')}{' '}{data.chunks_30d === 'Unavailable' ?
<Fa icon={faEye} title={t('html.description.noSpongeChunks')}/> : ''}</>}
values={[
data.chunks_30d,
data.chunks_7d,
data.chunks_24h
]}/>
<TableRow icon={faHdd} color="green"
text={t('html.label.maxFreeDisk')}
values={[
data.max_disk_30d,
data.max_disk_7d,
data.max_disk_24h
]}/>
<TableRow icon={faHdd} color="green"
text={t('html.label.minFreeDisk')}
values={[
data.min_disk_30d,
data.min_disk_7d,
data.min_disk_24h
]}/>
</AsNumbersTable>
)
}
export default PerformanceAsNumbersTable;

View File

@ -0,0 +1,42 @@
import React from "react";
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
const PingRow = ({country}) => {
return (
<tr>
<td>{country.country}</td>
<td>{country.avg_ping}</td>
<td>{country.min_ping}</td>
<td>{country.max_ping}</td>
</tr>
);
}
const PingTable = ({countries}) => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
return (
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
<thead className="bg-amber">
<tr>
<th>{t('html.label.country')}</th>
<th>{t('html.label.averagePing')}</th>
<th>{t('html.label.bestPing')}</th>
<th>{t('html.label.worstPing')}</th>
</tr>
</thead>
<tbody>
{countries.length ? countries.map((country, i) => <PingRow key={i} country={country}/>) : <tr>
<td>{t('generic.noData')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
</table>
)
};
export default PingTable;

View File

@ -6,6 +6,9 @@ import {TableRow} from "./TableRow";
const ServerPvpPveAsNumbersTable = ({killData}) => {
const {t} = useTranslation();
if (!killData) return <></>
return (
<AsNumbersTable
headers={[t('html.label.allTime'), t('html.label.last30days'), t('html.label.last7days')]}

View File

@ -2,6 +2,8 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import React from "react";
export const TableRow = ({icon, text, color, values, bold}) => {
if (!values || values.filter(value => value !== undefined).length < values.length) return <></>;
const label = (<><Fa icon={icon} className={'col-' + color}/> {text}</>);
return (
<tr>

View File

@ -23,15 +23,6 @@ export const AuthenticationContextProvider = ({children}) => {
}
}, [])
const login = useCallback(async (username, password) => {
// TODO implement later when login page is done with React
await updateLoginDetails();
}, [updateLoginDetails]);
const logout = useCallback(() => {
// TODO implement later when login page is done with React
}, []);
const hasPermission = useCallback(permission => {
return !authRequired || (loggedIn && user && user.permissions.filter(perm => perm === permission).length);
}, [authRequired, loggedIn, user]);
@ -49,11 +40,10 @@ export const AuthenticationContextProvider = ({children}) => {
authRequired,
loggedIn,
user,
login,
logout,
loginError,
hasPermission,
hasPermissionOtherThan
hasPermissionOtherThan,
updateLoginDetails
}
return (<AuthenticationContext.Provider value={sharedState}>
{children}

View File

@ -8,11 +8,12 @@ export const useDataRequest = (fetchMethod, parameters) => {
/*eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
fetchMethod(...parameters, updateRequested).then(({data: json, error}) => {
fetchMethod(updateRequested, ...parameters).then(({data: json, error}) => {
if (json) {
setData(json);
finishUpdate(json.timestamp, json.timestamp_f);
} else if (error) {
console.warn(error);
setLoadingError(error);
}
});

View File

@ -8,8 +8,22 @@ export const NavigationContextProvider = ({children}) => {
const [updating, setUpdating] = useState(false);
const [lastUpdate, setLastUpdate] = useState({date: 0, formatted: ""});
const [items, setItems] = useState([]);
const [sidebarExpanded, setSidebarExpanded] = useState(window.innerWidth > 1350);
const setSidebarItems = useCallback((items) => {
const pathname = window.location.href;
setItems(items);
for (const item of items) {
if ('/' !== item.href && pathname.includes(item.href)) setCurrentTab(item.name);
if (item.contents) {
for (const subItem of item.contents) {
if ('/' !== subItem.href && pathname.includes(subItem.href)) setCurrentTab(subItem.name);
}
}
}
}, [setItems]);
const requestUpdate = useCallback(() => {
if (!updating) {
setUpdateRequested(Date.now());
@ -19,8 +33,10 @@ export const NavigationContextProvider = ({children}) => {
const finishUpdate = useCallback((date, formatted) => {
// TODO Logic to retry if received data is too old
setLastUpdate({date, formatted});
setUpdating(false);
if (date) {
setLastUpdate({date, formatted});
setUpdating(false);
}
}, [setLastUpdate, setUpdating]);
const toggleSidebar = useCallback(() => {
@ -30,7 +46,7 @@ export const NavigationContextProvider = ({children}) => {
const sharedState = {
currentTab, setCurrentTab,
lastUpdate, updateRequested, updating, requestUpdate, finishUpdate,
sidebarExpanded, setSidebarExpanded, toggleSidebar
sidebarExpanded, setSidebarExpanded, toggleSidebar, sidebarItems: items, setSidebarItems
}
return (<NavigationContext.Provider value={sharedState}>
{children}

View File

@ -0,0 +1,27 @@
import {createContext, useContext, useEffect, useState} from "react";
import {useDataRequest} from "./dataFetchHook";
import {fetchExtensionData} from "../service/serverService";
const ServerExtensionContext = createContext({});
export const ServerExtensionContextProvider = ({identifier, children}) => {
const [extensionData, setExtensionData] = useState(undefined);
const [extensionDataLoadingError, setExtensionDataLoadingError] = useState(undefined);
const {data, loadingError} = useDataRequest(fetchExtensionData, [identifier]);
useEffect(() => {
setExtensionData(data);
setExtensionDataLoadingError(loadingError);
}, [data, loadingError, setExtensionData, setExtensionDataLoadingError])
const sharedState = {extensionData, extensionDataLoadingError}
return (<ServerExtensionContext.Provider value={sharedState}>
{children}
</ServerExtensionContext.Provider>
)
}
export const useServerExtensionContext = () => {
return useContext(ServerExtensionContext);
}

View File

@ -1,6 +1,11 @@
import {doGetRequest} from "./backendConfiguration";
import {doGetRequest, doSomePostRequest, standard200option} from "./backendConfiguration";
export const fetchWhoAmI = async () => {
const url = '/v1/whoami';
return doGetRequest(url);
}
export const fetchLogin = async (username, password) => {
const url = '/auth/login';
return doSomePostRequest(url, [standard200option], `user=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`);
}

View File

@ -11,13 +11,22 @@ const isCurrentAddress = (address) => {
export const baseAddress = "PLAN_BASE_ADDRESS" === toBeReplaced || !isCurrentAddress(toBeReplaced) ? "" : toBeReplaced;
export const doSomeGetRequest = async (url, statusOptions) => {
return doSomeRequest(url, statusOptions, async () => axios.get(url));
}
export const doSomePostRequest = async (url, statusOptions, body) => {
return doSomeRequest(url, statusOptions, async () => axios.post(url, body));
}
export const doSomeRequest = async (url, statusOptions, axiosFunction) => {
let response = undefined;
try {
response = await axios.get(baseAddress + url);
response = await axiosFunction.call();
for (const statusOption of statusOptions) {
if (response.status === statusOption.status) {
return {
status: response.status,
data: statusOption.get(response),
error: undefined
};
@ -29,6 +38,7 @@ export const doSomeGetRequest = async (url, statusOptions) => {
for (const statusOption of statusOptions) {
if (e.response.status === statusOption.status) {
return {
status: e.response.status,
data: undefined,
error: statusOption.get(response, e)
};
@ -37,6 +47,7 @@ export const doSomeGetRequest = async (url, statusOptions) => {
return {
data: undefined,
error: {
status: e.response.status,
message: e.message,
url,
data: e.response.data
@ -46,6 +57,7 @@ export const doSomeGetRequest = async (url, statusOptions) => {
return {
data: undefined,
error: {
status: undefined,
message: e.message,
url
}

View File

@ -13,4 +13,14 @@ export const fetchPlanVersion = async () => {
export const fetchAvailableLocales = async () => {
const url = '/v1/locale';
return doGetRequest(url);
}
export const fetchErrorLogs = async () => {
const url = '/v1/errors';
return doGetRequest(url);
}
export const fetchNetworkMetadata = async () => {
const url = '/v1/networkMetadata';
return doGetRequest(url);
}

View File

@ -0,0 +1,6 @@
import {doGetRequest} from "./backendConfiguration";
export const fetchNetworkOverview = async (updateRequested) => {
const url = `/v1/network/overview?timestamp=${updateRequested}`;
return doGetRequest(url);
}

View File

@ -1,7 +1,7 @@
import {faMapSigns} from "@fortawesome/free-solid-svg-icons";
import {doSomeGetRequest, standard200option} from "./backendConfiguration";
export const fetchPlayer = async (uuid, timestamp) => {
export const fetchPlayer = async (timestamp, uuid) => {
const url = `/v1/player?player=${uuid}&timestamp=${timestamp}`;
return doSomeGetRequest(url, [
standard200option,

View File

@ -1,97 +1,116 @@
import {doGetRequest} from "./backendConfiguration";
export const fetchServerOverview = async (identifier) => {
const timestamp = Date.now();
export const fetchServerIdentity = async (timestamp, identifier) => {
const url = `/v1/serverIdentity?server=${identifier}`;
return doGetRequest(url);
}
export const fetchServerOverview = async (timestamp, identifier) => {
const url = `/v1/serverOverview?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchOnlineActivityOverview = async (identifier) => {
const timestamp = Date.now();
export const fetchOnlineActivityOverview = async (timestamp, identifier) => {
const url = `/v1/onlineOverview?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPlayerbaseOverview = async (identifier) => {
const timestamp = Date.now();
export const fetchPlayerbaseOverview = async (timestamp, identifier) => {
const url = `/v1/playerbaseOverview?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchSessionOverview = async (identifier) => {
const timestamp = Date.now();
export const fetchSessionOverview = async (timestamp, identifier) => {
const url = `/v1/sessionsOverview?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPvpPve = async (identifier) => {
const timestamp = Date.now();
export const fetchPvpPve = async (timestamp, identifier) => {
const url = `/v1/playerVersus?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchSessions = async (identifier) => {
const timestamp = Date.now();
export const fetchPerformanceOverview = async (timestamp, identifier) => {
const url = `/v1/performanceOverview?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchExtensionData = async (timestamp, identifier) => {
const url = `/v1/extensionData?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchSessions = async (timestamp, identifier) => {
const url = `/v1/sessions?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchKills = async (identifier) => {
const timestamp = Date.now();
export const fetchKills = async (timestamp, identifier) => {
const url = `/v1/kills?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPlayers = async (identifier) => {
const timestamp = Date.now();
export const fetchPlayers = async (timestamp, identifier) => {
const url = identifier ? `/v1/players?server=${identifier}&timestamp=${timestamp}` : `/v1/players?timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPlayersOnlineGraph = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/graph?type=playersOnline&server=${identifier}&timestamp=${timestamp}`;
export const fetchPingTable = async (timestamp, identifier) => {
const url = identifier ? `/v1/pingTable?server=${identifier}&timestamp=${timestamp}` : `/v1/pingTable?timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPlayerbaseDevelopmentGraph = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/graph?type=activity&server=${identifier}&timestamp=${timestamp}`;
export const fetchPlayersOnlineGraph = async (timestamp, identifier) => {
const url = identifier ? `/v1/graph?type=playersOnline&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=playersOnline&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchDayByDayGraph = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/graph?type=uniqueAndNew&server=${identifier}&timestamp=${timestamp}`;
export const fetchPlayerbaseDevelopmentGraph = async (timestamp, identifier) => {
const url = identifier ? `/v1/graph?type=activity&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=activity&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchHourByHourGraph = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}&timestamp=${timestamp}`;
export const fetchDayByDayGraph = async (timestamp, identifier) => {
const url = identifier ? `/v1/graph?type=uniqueAndNew&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=uniqueAndNew&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchServerCalendarGraph = async (identifier) => {
const timestamp = Date.now();
export const fetchHourByHourGraph = async (timestamp, identifier) => {
const url = identifier ? `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}&timestamp=${timestamp}` :
`/v1/graph?type=hourlyUniqueAndNew&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchServerCalendarGraph = async (timestamp, identifier) => {
const url = `/v1/graph?type=serverCalendar&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPunchCardGraph = async (identifier) => {
const timestamp = Date.now();
export const fetchPunchCardGraph = async (timestamp, identifier) => {
const url = `/v1/graph?type=punchCard&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchWorldPie = async (identifier) => {
const timestamp = Date.now();
export const fetchWorldPie = async (timestamp, identifier) => {
const url = `/v1/graph?type=worldPie&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchGeolocations = async (identifier) => {
const timestamp = Date.now();
export const fetchGeolocations = async (timestamp, identifier) => {
const url = `/v1/graph?type=geolocation&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchOptimizedPerformance = async (timestamp, identifier) => {
const url = `/v1/graph?type=optimizedPerformance&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPingGraph = async (timestamp, identifier) => {
const url = `/v1/graph?type=aggregatedPing&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}

View File

@ -181,6 +181,7 @@ div.scrollbar {
.color-chooser {
margin-right: 0.15rem;
margin-bottom: 0.2rem;
border-color: transparent !important;
}
/* Navbar ====================================== */
@ -767,111 +768,133 @@ div#navSrvContainer::-webkit-scrollbar-thumb {
.bg-red, body.theme-red .fc-toolbar-chunk .btn.btn-primary {
background-color: #F44336;
--bs-btn-disabled-bg: #F44336;
color: #fff;
}
.bg-pink, body.theme-pink .fc-toolbar-chunk .btn.btn-primary {
background-color: #E91E63;
--bs-btn-disabled-bg: #E91E63;
color: #fff;
}
.bg-purple, body.theme-purple .fc-toolbar-chunk .btn.btn-primary {
background-color: #9C27B0;
--bs-btn-disabled-bg: #9C27B0;
color: #fff;
}
.bg-deep-purple, body.theme-deep-purple .fc-toolbar-chunk .btn.btn-primary {
background-color: #673AB7;
--bs-btn-disabled-bg: #673AB7;
color: #fff;
}
.bg-indigo, body.theme-indigo .fc-toolbar-chunk .btn.btn-primary {
background-color: #3F51B5;
--bs-btn-disabled-bg: #3F51B5;
color: #fff;
}
.bg-blue, body.theme-blue .fc-toolbar-chunk .btn.btn-primary {
background-color: #2196F3;
--bs-btn-disabled-bg: #2196F3;
color: #fff;
}
.bg-light-blue, body.theme-light-blue .fc-toolbar-chunk .btn.btn-primary {
background-color: #03A9F4;
--bs-btn-disabled-bg: #03A9F4;
color: #fff;
}
.bg-cyan, body.theme-cyan .fc-toolbar-chunk .btn.btn-primary {
background-color: #00BCD4;
--bs-btn-disabled-bg: #00BCD4;
color: #fff;
}
.bg-teal, body.theme-teal .fc-toolbar-chunk .btn.btn-primary {
background-color: #009688;
--bs-btn-disabled-bg: #009688;
color: #fff;
}
.bg-green, body.theme-green .fc-toolbar-chunk .btn.btn-primary {
background-color: #4CAF50;
--bs-btn-disabled-bg: #4CAF50;
color: #fff;
}
.bg-light-green, body.theme-light-green .fc-toolbar-chunk .btn.btn-primary {
background-color: #8BC34A;
--bs-btn-disabled-bg: #8BC34A;
color: #fff;
}
.bg-lime, body.theme-lime .fc-toolbar-chunk .btn.btn-primary {
background-color: #CDDC39;
--bs-btn-disabled-bg: #CDDC39;
color: #fff;
}
.bg-yellow, body.theme-yellow .fc-toolbar-chunk .btn.btn-primary {
background-color: #ffe821;
--bs-btn-disabled-bg: #ffe821;
color: #fff;
}
.bg-amber, body.theme-amber .fc-toolbar-chunk .btn.btn-primary {
background-color: #FFC107;
--bs-btn-disabled-bg: #FFC107;
color: #fff;
}
.bg-orange, body.theme-orange .fc-toolbar-chunk .btn.btn-primary {
background-color: #FF9800;
--bs-btn-disabled-bg: #FF9800;
color: #fff;
}
.bg-deep-orange, body.theme-deep-orange .fc-toolbar-chunk .btn.btn-primary {
background-color: #FF5722;
--bs-btn-disabled-bg: #FF5722;
color: #fff;
}
.bg-brown, body.theme-brown .fc-toolbar-chunk .btn.btn-primary {
background-color: #795548;
--bs-btn-disabled-bg: #795548;
color: #fff;
}
.bg-grey, body.theme-grey .fc-toolbar-chunk .btn.btn-primary {
background-color: #9E9E9E;
--bs-btn-disabled-bg: #9E9E9E;
color: #fff;
}
.bg-blue-grey, body.theme-blue-grey .fc-toolbar-chunk .btn.btn-primary {
background-color: #607D8B;
--bs-btn-disabled-bg: #607D8B;
color: #fff;
}
.bg-black, body.theme-black .fc-toolbar-chunk .btn.btn-primary {
background-color: #555555;
--bs-btn-disabled-bg: #555555;
color: #fff;
}
.bg-white, body.theme-white .fc-toolbar-chunk .btn.btn-primary {
background-color: #ffffff;
--bs-btn-disabled-bg: #ffffff;
color: #333;
}
.bg-plan, body.theme-plan .fc-toolbar-chunk .btn.btn-primary {
background-color: #368F17;
--bs-btn-disabled-bg: #368F17;
color: #fff;
}
@ -902,6 +925,7 @@ div#navSrvContainer::-webkit-scrollbar-thumb {
.bg-night, body.theme-night .fc-toolbar-chunk .btn.btn-primary {
background-color: #44475a;
--bs-btn-disabled-bg: #44475a;
color: #eee8d5;
}

View File

@ -181,7 +181,7 @@ const createNightModeColorCss = () => {
return `.bg-${color.name}{background-color: ${desaturatedColor} !important;color: ${nightColors.yellow};}` +
`.bg-${color.name}-outline{outline-color: ${desaturatedColor};border-color: ${desaturatedColor};}` +
`.col-${color.name}{color: ${desaturatedColor} !important;}`
}).join();
}).join('');
}
export const createNightModeCss = () => {

View File

@ -0,0 +1,75 @@
// https://gist.github.com/gkhays/e264009c0832c73d5345847e673a64ab
export default function drawSine(canvasId) {
let step;
function drawPoint(ctx, x, y) {
const radius = 2;
ctx.beginPath();
// Hold x constant at 4 so the point only moves up and down.
ctx.arc(x - 5, y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.lineWidth = 1;
ctx.stroke();
}
function plotSine(ctx, xOffset) {
const width = ctx.canvas.width;
const height = ctx.canvas.height;
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = "#fff";
// Drawing point
let x = -2;
let y = 0;
const amplitude = 50;
const frequency = 50;
ctx.moveTo(x, 50);
while (x <= width) {
y = height / 2 + amplitude * Math.sin((x + xOffset) / frequency) * Math.cos((x + xOffset) / (frequency * 0.54515978463));
ctx.lineTo(x, y);
x += 5;
}
ctx.stroke();
ctx.save();
drawPoint(ctx, x, y);
ctx.stroke();
ctx.restore();
}
function draw() {
const canvas = document.getElementById(canvasId);
if (canvas == null) return;
const context = canvas.getContext("2d");
context.clearRect(0, 0, 1000, 150);
context.save();
plotSine(context, step);
context.restore();
step += 0.5;
window.requestAnimationFrame(draw);
}
function fix_dpi() {
const canvas = document.getElementById(canvasId);
if (canvas == null) return;
let dpi = window.devicePixelRatio;
canvas.getContext('2d');
const style_width = getComputedStyle(canvas).getPropertyValue("width").slice(0, -2);
// Scale the canvas
canvas.setAttribute('width', `${style_width * dpi}`);
}
fix_dpi();
step = -1;
window.requestAnimationFrame(draw);
}

View File

@ -2,13 +2,14 @@ import {Card, Col, Row} from "react-bootstrap-v5";
import React from "react";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faBug} from "@fortawesome/free-solid-svg-icons";
import LoadIn from "../components/animation/LoadIn";
export const ErrorViewText = ({error}) => {
return (
<>
<p>{error.message} {error.url && <a href={error.url}>{error.url}</a>}</p>
{error.data && <><br/>
<pre>{error.data}</pre>
<pre>{JSON.stringify(error.data)}</pre>
</>}
</>
)
@ -24,26 +25,30 @@ export const ErrorViewBody = ({error}) => {
export const ErrorViewCard = ({error}) => {
return (
<Card>
<Card.Header>
<h6 className="col-black">
<Fa icon={error.icon ? error.icon : faBug}/> Error information
</h6>
</Card.Header>
<ErrorViewBody error={error}/>
</Card>
<LoadIn>
<Card>
<Card.Header>
<h6 className="col-black">
<Fa icon={error.icon ? error.icon : faBug}/> Error information
</h6>
</Card.Header>
<ErrorViewBody error={error}/>
</Card>
</LoadIn>
)
}
const ErrorView = ({error}) => {
return (
<section className="error_view">
<Row>
<Col lg={12}>
<ErrorViewCard error={error}/>
</Col>
</Row>
</section>
<LoadIn>
<section className="error_view">
<Row>
<Col lg={12}>
<ErrorViewCard error={error}/>
</Col>
</Row>
</section>
</LoadIn>
)
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import {NightModeCss} from "../../hooks/themeHook";
import Sidebar from "../../components/navigation/Sidebar";
import Header from "../../components/navigation/Header";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBug} from "@fortawesome/free-solid-svg-icons";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchErrorLogs} from "../../service/metadataService";
import ErrorPage from "./ErrorPage";
import ErrorsAccordion from "../../components/accordion/ErrorsAccordion";
import {Card} from "react-bootstrap-v5";
const ErrorsPage = () => {
const {data, loadingError} = useDataRequest(fetchErrorLogs, []);
if (loadingError) return <ErrorPage error={loadingError}/>;
return (
<>
<NightModeCss/>
<Sidebar items={[]} showBackButton={true}/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={<><FontAwesomeIcon icon={faBug}/> Error Logs</>}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<Card>
<ErrorsAccordion errors={data}/>
</Card>
</main>
<aside>
<ColorSelectorModal/>
</aside>
</div>
</div>
</>
)
};
export default ErrorsPage

View File

@ -0,0 +1,208 @@
import React, {useCallback, useEffect, useState} from 'react';
import logo from '../../Flaticon_circle.png'
import {Alert, Card, Col, Row} from "react-bootstrap-v5";
import {Link, useNavigate} from "react-router-dom";
import {useTranslation} from "react-i18next";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faPalette} from "@fortawesome/free-solid-svg-icons";
import {useTheme} from "../../hooks/themeHook";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
import drawSine from "../../util/loginSineRenderer";
import {fetchLogin} from "../../service/authenticationService";
import ForgotPasswordModal from "../../components/modal/ForgotPasswordModal";
import {useAuth} from "../../hooks/authenticationHook";
import {baseAddress} from "../../service/backendConfiguration";
const Logo = () => {
return (
<Col md={12} className='mt-5 text-center'>
<img alt="logo" className="w-15" src={logo}/>
</Col>
)
};
const LoginCard = ({children}) => {
return (
<Row className="justify-content-center container-fluid">
<Col xl={6} lg={7} md={9}>
<Card className='o-hidden border-0 shadow-lg my-5'>
<Card.Body className='p-0'>
<Row>
<Col lg={12}>
<div className='p-5'>
{children}
</div>
</Col>
</Row>
</Card.Body>
</Card>
</Col>
</Row>
)
}
const LoginForm = ({login}) => {
const {t} = useTranslation();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const onLogin = useCallback(event => {
event.preventDefault();
login(username, password).then(() => setPassword(''));
}, [username, password, setPassword, login]);
return (
<form className="user">
<div className="mb-3">
<input autoComplete="username" className="form-control form-control-user"
id="inputUser"
placeholder={t('html.login.username')} type="text"
value={username} onChange={event => setUsername(event.target.value)}/>
</div>
<div className="mb-3">
<input autoComplete="current-password" className="form-control form-control-user"
id="inputPassword" placeholder={t('html.login.password')} type="password"
value={password} onChange={event => setPassword(event.target.value)}/>
</div>
<button className="btn bg-plan btn-user w-100" id="login-button" onClick={onLogin}>
{t('html.login.login')}
</button>
</form>
);
}
const ColorChooserButton = () => {
const {t} = useTranslation();
const {toggleColorChooser} = useTheme();
return (
<div className='text-center'>
<button className="btn col-plan" onClick={toggleColorChooser}
title={t('html.label.themeSelect')}>
<Fa icon={faPalette}/>
</button>
</div>
)
}
const ForgotPasswordButton = ({onClick}) => {
const {t} = useTranslation();
return (
<div className='text-center'>
<button className='col-plan small' onClick={onClick}>{t('html.login.forgotPassword')}</button>
</div>
)
}
const CreateAccountLink = () => {
const {t} = useTranslation();
return (
<div className='text-center'>
<Link to='/register' className='col-plan small'>{t('html.login.register')}</Link>
</div>
)
}
const Decoration = () => {
useEffect(() => {
drawSine('decoration');
})
return (
<Row className='justify-content-center'>
<canvas className="col-xl-3 col-lg-3 col-md-5" id="decoration" style={{height: "100px"}}/>
</Row>
);
}
const LoginPage = () => {
const {t} = useTranslation();
const navigate = useNavigate();
const {authLoaded, authRequired, loggedIn, updateLoginDetails} = useAuth();
const [forgotPasswordModalOpen, setForgotPasswordModalOpen] = useState(false);
const [failMessage, setFailMessage] = useState('');
const [redirectTo, setRedirectTo] = useState(undefined);
const togglePasswordModal = useCallback(() => setForgotPasswordModalOpen(!forgotPasswordModalOpen),
[setForgotPasswordModalOpen, forgotPasswordModalOpen])
useEffect(() => {
document.body.classList.add("bg-plan", "plan-bg-gradient");
const urlParams = new URLSearchParams(window.location.search);
const cameFrom = urlParams.get('from');
if (cameFrom) setRedirectTo(cameFrom);
return () => {
document.body.classList.remove("bg-plan", "plan-bg-gradient");
}
}, [setRedirectTo])
const login = async (username, password) => {
if (!username || username.length < 1) {
return setFailMessage(t('html.register.error.noUsername'));
}
if (username.length > 50) {
return setFailMessage(t('html.register.error.usernameLength') + username.length);
}
if (!password || password.length < 1) {
return setFailMessage(t('html.register.error.noPassword'));
}
const {data, error} = await fetchLogin(username, password);
if (error) {
if (error.message === 'Request failed with status code 403') {
// Too many logins, reload browser to show forbidden page
window.location.reload();
} else {
setFailMessage(t('html.login.failed') + (error.data && error.data.error ? error.data.error : error.message));
}
} else if (data && data.success) {
await updateLoginDetails();
if (redirectTo && !redirectTo.startsWith('http') && !redirectTo.startsWith('file') && !redirectTo.startsWith('javascript')) {
navigate(baseAddress + redirectTo.substring(redirectTo.indexOf('/')) + (window.location.hash ? window.location.hash : ''));
} else {
navigate(baseAddress + '/');
}
} else {
setFailMessage(t('html.login.failed') + data ? data.error : t('generic.noData'));
}
}
if (!authLoaded) {
return <></>
}
if (!authRequired || loggedIn) {
navigate('../');
}
return (
<>
<main className="container">
<Logo/>
<LoginCard>
{failMessage && <Alert className='alert-danger'>{failMessage}</Alert>}
<LoginForm login={login}/>
<hr className="bg-secondary"/>
<ForgotPasswordButton onClick={togglePasswordModal}/>
<CreateAccountLink/>
<ColorChooserButton/>
</LoginCard>
<Decoration/>
</main>
<aside>
<ColorSelectorModal/>
<ForgotPasswordModal show={forgotPasswordModalOpen} toggle={togglePasswordModal}/>
</aside>
</>
)
};
export default LoginPage

View File

@ -0,0 +1,146 @@
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import {Outlet} from "react-router-dom";
import {useNavigation} from "../../hooks/navigationHook";
import {
faChartLine,
faCogs,
faCubes,
faGlobe,
faInfoCircle,
faNetworkWired,
faSearch,
faServer,
faUserGroup,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import {useAuth} from "../../hooks/authenticationHook";
import {NightModeCss} from "../../hooks/themeHook";
import Sidebar from "../../components/navigation/Sidebar";
import Header from "../../components/navigation/Header";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
import {useMetadata} from "../../hooks/metadataHook";
import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons";
import {SwitchTransition} from "react-transition-group";
import MainPageRedirect from "../../components/navigation/MainPageRedirect";
import ExtensionIcon from "../../components/extensions/ExtensionIcon";
import {ServerExtensionContextProvider, useServerExtensionContext} from "../../hooks/serverExtensionDataContext";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchNetworkMetadata} from "../../service/metadataService";
const NetworkSidebar = () => {
const {t, i18n} = useTranslation();
const {sidebarItems, setSidebarItems} = useNavigation();
const {extensionData} = useServerExtensionContext();
const {data: networkMetadata} = useDataRequest(fetchNetworkMetadata, [])
useEffect(() => {
const servers = networkMetadata?.servers || [];
const items = [
{name: 'html.label.networkOverview', icon: faInfoCircle, href: "overview"},
{},
{
name: 'html.label.servers',
icon: faServer,
contents: [
{
nameShort: 'html.label.overview',
name: 'html.label.servers',
icon: faNetworkWired,
href: "serversOverview"
},
{name: 'html.label.sessions', icon: faCalendarCheck, href: "sessions"},
{name: 'html.label.performance', icon: faCogs, href: "performance"},
{},
...servers.map(server => {
return {
name: server.serverName,
icon: faServer,
href: "/server/" + server.serverUUID,
color: 'light-green'
}
})
]
},
{
name: 'html.label.playerbase',
icon: faUsers,
contents: [
{
nameShort: 'html.label.overview',
name: 'html.label.playerbaseOverview',
icon: faChartLine,
href: "playerbase"
},
// {name: 'html.label.playerRetention', icon: faUsersViewfinder, href: "retention"},
{name: 'html.label.playerList', icon: faUserGroup, href: "players"},
{name: 'html.label.geolocations', icon: faGlobe, href: "geolocations"},
]
},
{},
{name: 'html.label.plugins'},
{name: 'html.label.pluginsOverview', icon: faCubes, href: "plugins-overview"}
]
if (extensionData) {
extensionData.extensions.filter(extension => extension.wide)
.map(extension => extension.extensionInformation)
.map(info => {
return {
name: info.pluginName,
icon: <ExtensionIcon icon={info.icon}/>,
href: `plugins/${encodeURIComponent(info.pluginName)}`
}
}).forEach(item => items.push(item))
}
items.push(
{},
{name: 'html.label.links'},
{name: 'html.label.query', icon: faSearch, href: "/query"}
);
setSidebarItems(items);
window.document.title = `Plan | Network`;
}, [t, i18n, extensionData, setSidebarItems, networkMetadata])
return (
<Sidebar items={sidebarItems} showBackButton={false}/>
)
}
const ServerPage = () => {
const {networkName, serverUUID} = useMetadata();
const {currentTab} = useNavigation();
const {authRequired, loggedIn} = useAuth();
if (authRequired && !loggedIn) return <MainPageRedirect/>
if (!serverUUID) return <></>
return (
<>
<NightModeCss/>
<ServerExtensionContextProvider identifier={serverUUID}>
<NetworkSidebar/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={networkName} tab={currentTab}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<SwitchTransition>
<Outlet/>
</SwitchTransition>
</main>
<aside>
<ColorSelectorModal/>
</aside>
</div>
</div>
</ServerExtensionContextProvider>
</>
)
}
export default ServerPage;

View File

@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react";
import React, {useEffect} from "react";
import Sidebar from "../../components/navigation/Sidebar";
import {Outlet, useOutletContext, useParams} from "react-router-dom";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
@ -17,12 +17,12 @@ import ErrorPage from "./ErrorPage";
const PlayerPage = () => {
const {t, i18n} = useTranslation();
const [sidebarItems, setSidebarItems] = useState([]);
const {sidebarItems, setSidebarItems} = useNavigation();
const {identifier} = useParams();
const {currentTab, updateRequested, finishUpdate} = useNavigation();
const {currentTab, finishUpdate} = useNavigation();
const {data: player, loadingError} = useDataRequest(fetchPlayer, [identifier, updateRequested])
const {data: player, loadingError} = useDataRequest(fetchPlayer, [identifier])
useEffect(() => {
if (!player) return;
@ -46,7 +46,7 @@ const PlayerPage = () => {
window.document.title = `Plan | ${player.info.name}`;
finishUpdate(player.timestamp, player.timestamp_f);
}, [player, t, i18n, finishUpdate])
}, [player, t, i18n, finishUpdate, setSidebarItems])
const {hasPermissionOtherThan} = useAuth();
const showBackButton = hasPermissionOtherThan('page.player.self');

View File

@ -15,7 +15,7 @@ const PlayersPage = () => {
const {isProxy, serverName} = useMetadata();
const [error] = useState(undefined);
const [sidebarItems, setSidebarItems] = useState([]);
const {sidebarItems, setSidebarItems} = useNavigation();
const {currentTab, setCurrentTab} = useNavigation();
@ -28,7 +28,7 @@ const PlayersPage = () => {
setSidebarItems(items);
window.document.title = `Plan | Player list`;
setCurrentTab('html.label.players')
}, [t, i18n, setCurrentTab])
}, [t, i18n, setCurrentTab, setSidebarItems])
// const {authRequired, user} = useAuth();
const showBackButton = true; // TODO

View File

@ -1,6 +1,6 @@
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {Outlet} from "react-router-dom";
import {Outlet, useParams} from "react-router-dom";
import {useNavigation} from "../../hooks/navigationHook";
import {
faCampground,
@ -12,8 +12,7 @@ import {
faInfoCircle,
faSearch,
faUserGroup,
faUsers,
faUsersViewfinder
faUsers
} from "@fortawesome/free-solid-svg-icons";
import {useAuth} from "../../hooks/authenticationHook";
import {NightModeCss} from "../../hooks/themeHook";
@ -23,15 +22,22 @@ import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
import {useMetadata} from "../../hooks/metadataHook";
import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons";
import ErrorPage from "./ErrorPage";
import {SwitchTransition} from "react-transition-group";
import MainPageRedirect from "../../components/navigation/MainPageRedirect";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchServerIdentity} from "../../service/serverService";
import ExtensionIcon from "../../components/extensions/ExtensionIcon";
import {ServerExtensionContextProvider, useServerExtensionContext} from "../../hooks/serverExtensionDataContext";
const ServerPage = () => {
const ServerSidebar = () => {
const {t, i18n} = useTranslation();
const {isProxy, serverName} = useMetadata();
const {sidebarItems, setSidebarItems} = useNavigation();
const {extensionData} = useServerExtensionContext();
const {authRequired, loggedIn, user} = useAuth();
const [error] = useState(undefined);
const [sidebarItems, setSidebarItems] = useState([]);
const {currentTab} = useNavigation();
const {isProxy} = useMetadata();
const showBackButton = isProxy
&& (!authRequired || (loggedIn && user.permissions.filter(perm => perm !== 'page.network').length));
useEffect(() => {
const items = [
@ -62,7 +68,7 @@ const ServerPage = () => {
icon: faChartLine,
href: "playerbase"
},
{name: 'html.label.playerRetention', icon: faUsersViewfinder, href: "retention"},
// {name: 'html.label.playerRetention', icon: faUsersViewfinder, href: "retention"},
{name: 'html.label.playerList', icon: faUserGroup, href: "players"},
{name: 'html.label.geolocations', icon: faGlobe, href: "geolocations"},
]
@ -70,39 +76,91 @@ const ServerPage = () => {
{name: 'html.label.performance', icon: faCogs, href: "performance"},
{},
{name: 'html.label.plugins'},
{name: 'html.label.pluginsOverview', icon: faCubes, href: "plugins-overview"},
{},
{name: 'html.label.links'},
{name: 'html.label.query', icon: faSearch, href: "/query"},
{name: 'html.label.pluginsOverview', icon: faCubes, href: "plugins-overview"}
]
// TODO Extensions
if (extensionData) {
extensionData.extensions.filter(extension => extension.wide)
.map(extension => extension.extensionInformation)
.map(info => {
return {
name: info.pluginName,
icon: <ExtensionIcon icon={info.icon}/>,
href: `plugins/${encodeURIComponent(info.pluginName)}`
}
}).forEach(item => items.push(item))
}
items.push(
{},
{name: 'html.label.links'},
{name: 'html.label.query', icon: faSearch, href: "/query"}
);
setSidebarItems(items);
window.document.title = `Plan | Server Analysis`;
}, [t, i18n])
}, [t, i18n, extensionData, setSidebarItems])
const {authRequired, user} = useAuth();
const showBackButton = isProxy && (!authRequired || user.permissions.filter(perm => perm !== 'page.network').length);
return (
<Sidebar items={sidebarItems} showBackButton={showBackButton}/>
)
}
const ServerPage = () => {
const {t} = useTranslation();
const {identifier} = useParams();
const {isProxy, serverName} = useMetadata();
const {
data: serverIdentity,
loadingError: identityLoadingError
} = useDataRequest(fetchServerIdentity, [identifier]);
const [error] = useState(undefined);
const {currentTab} = useNavigation();
const {authRequired, loggedIn} = useAuth();
if (authRequired && !loggedIn) return <MainPageRedirect/>
const getDisplayedServerName = () => {
if (serverIdentity) {
return serverIdentity.serverName;
}
if (isProxy) {
return identifier;
} else {
return serverName && serverName.startsWith('Server') ? "Plan" : serverName
}
}
const displayedServerName = getDisplayedServerName();
if (error) return <ErrorPage error={error}/>;
if (identityLoadingError) {
if (identityLoadingError.status === 404) return <ErrorPage
error={{title: t('html.error.404NotFound'), message: t('html.error.serverNotSeen')}}/>
return <ErrorPage error={identityLoadingError}/>
}
const displayedServerName = !isProxy && serverName && serverName.startsWith('Server') ? "Plan" : serverName;
return (
<>
<NightModeCss/>
<Sidebar items={sidebarItems} showBackButton={showBackButton}/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={displayedServerName} tab={currentTab}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<Outlet context={{}}/>
</main>
<aside>
<ColorSelectorModal/>
</aside>
<ServerExtensionContextProvider identifier={identifier}>
<ServerSidebar/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={displayedServerName} tab={currentTab}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<SwitchTransition>
<Outlet/>
</SwitchTransition>
</main>
<aside>
<ColorSelectorModal/>
</aside>
</div>
</div>
</div>
</ServerExtensionContextProvider>
</>
)
}

Some files were not shown because too many files have changed in this diff Show More