Implemented Network performance tab in React

Affects issues:
- Implemented #2469
This commit is contained in:
Aurora Lahtela 2022-09-11 19:30:57 +03:00
parent 3d64e36159
commit 8cdbebf191
14 changed files with 440 additions and 52 deletions

View File

@ -179,6 +179,9 @@ public class NetworkPerformanceJSONResolver implements Resolver {
numbers.put("avg_server_downtime_24h", "-");
}
numbers.put("players_30d", format(tpsDataMonth.averagePlayers()));
numbers.put("players_7d", format(tpsDataWeek.averagePlayers()));
numbers.put("players_24h", format(tpsDataDay.averagePlayers()));
numbers.put("tps_30d", format(tpsDataMonth.averageTPS()));
numbers.put("tps_7d", format(tpsDataWeek.averageTPS()));
numbers.put("tps_24h", format(tpsDataDay.averageTPS()));

View File

@ -180,6 +180,8 @@ public enum HtmlLang implements Lang {
LABEL_AVG("html.label.average", "Average"),
TITLE_PERFORMANCE_AS_NUMBERS("html.label.performanceAsNumbers", "Performance as Numbers"),
LABEL_SERVER_DOWNTIME("html.label.serverDowntime", "Server Downtime"),
LABEL_TOTAL_SERVER_DOWNTIME("html.label.totalServerDowntime", "Total Server Downtime"),
LABEL_AVERAGE_SERVER_DOWNTIME("html.label.averageServerDowntime", "Average Downtime / Server"),
LABEL_DURING_LOW_TPS("html.label.duringLowTps", "During Low TPS Spikes:"),
TEXT_NO_LOW_TPS("html.text.noLowTps", "No low tps spikes"),
// Player Page
@ -257,6 +259,8 @@ public enum HtmlLang implements Lang {
LABEL_PROJECTION_MERCATOR("html.label.geoProjection.mercator", "Mercator"),
LABEL_PROJECTION_EQUAL_EARTH("html.label.geoProjection.equalEarth", "Equal Earth"),
LABEL_PROJECTION_ORTOGRAPHIC("html.label.geoProjection.ortographic", "Ortographic"),
LABEL_SERVER_SELECTOR("html.label.serverSelector", "Server selector"),
LABEL_APPLY("html.label.apply", "Apply"),
LOGIN_LOGIN("html.login.login", "Login"),
LOGIN_LOGOUT("html.login.logout", "Logout"),

View File

@ -41,6 +41,7 @@ const NetworkSessions = React.lazy(() => import("./views/network/NetworkSessions
const NetworkJoinAddresses = React.lazy(() => import("./views/network/NetworkJoinAddresses"));
const NetworkGeolocations = React.lazy(() => import("./views/network/NetworkGeolocations"));
const NetworkPlayerbaseOverview = React.lazy(() => import("./views/network/NetworkPlayerbaseOverview"));
const NetworkPerformance = React.lazy(() => import("./views/network/NetworkPerformance"));
const PlayersPage = React.lazy(() => import("./views/layout/PlayersPage"));
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
@ -124,6 +125,7 @@ function App() {
<Route path="overview" element={<Lazy><NetworkOverview/></Lazy>}/>
<Route path="serversOverview" element={<Lazy><NetworkServers/></Lazy>}/>
<Route path="sessions" element={<Lazy><NetworkSessions/></Lazy>}/>
<Route path="performance" element={<Lazy><NetworkPerformance/></Lazy>}/>
<Route path="playerbase" element={<Lazy><NetworkPlayerbaseOverview/></Lazy>}/>
<Route path="join-addresses" element={<Lazy><NetworkJoinAddresses/></Lazy>}/>
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>

View File

@ -0,0 +1,157 @@
import React from 'react';
import CardTabs from "../../CardTabs";
import {
faDragon,
faHdd,
faMap,
faMicrochip,
faSignal,
faTachometerAlt,
faUser
} from "@fortawesome/free-solid-svg-icons";
import {Card} from "react-bootstrap-v5";
import {useDataRequest} from "../../../hooks/dataFetchHook";
import {fetchPingGraph} from "../../../service/serverService";
import {tooltip, yAxisConfigurations} from "../../../util/graphs";
import {useTranslation} from "react-i18next";
import {CardLoader, ChartLoader} from "../../navigation/Loader";
import LineGraph from "../../graphs/LineGraph";
import {ErrorViewBody, ErrorViewCard} from "../../../views/ErrorView";
import PingGraph from "../../graphs/performance/PingGraph";
import {useMetadata} from "../../../hooks/metadataHook";
const Tab = ({data, yAxis}) => {
return (
<LineGraph id={'performance-' + new Date().getTime()} series={data} legendEnabled tall yAxis={yAxis}/>
)
}
const PingTab = ({identifier}) => {
const {data, loadingError} = useDataRequest(fetchPingGraph, [identifier]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader style={{height: "450px"}}/>;
return <PingGraph id="network-performance-ping-chart" data={data}/>;
}
const PerformanceGraphsCard = ({data}) => {
const {t} = useTranslation();
const {networkMetadata} = useMetadata();
if (!data || !Object.values(data).length) return <CardLoader/>
const zones = {
tps: [{
value: data.zones.tpsThresholdMed,
color: data.colors.low
}, {
value: data.zones.tpsThresholdHigh,
color: data.colors.med
}, {
value: 30,
color: data.colors.high
}],
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 serverData = [];
for (let i = 0; i < data.servers.length; i++) {
const server = data.servers[i];
const values = data.values[i];
serverData.push({
serverName: server.serverName,
values
});
}
const series = {
players: [],
tps: [],
cpu: [],
ram: [],
entities: [],
chunks: [],
disk: []
}
const spline = 'spline';
for (const server of serverData) {
series.players.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.playersOnline, color: data.colors.playersOnline, yAxis: 0
});
series.tps.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.tps, color: data.colors.high, zones: zones.tps, yAxis: 0
});
series.cpu.push({
name: server.serverName, type: spline, tooltip: tooltip.twoDecimals,
data: server.values.cpu, color: data.colors.cpu, yAxis: 0
});
series.ram.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.ram, color: data.colors.ram, yAxis: 0
});
series.entities.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.entities, color: data.colors.entities, yAxis: 0
});
series.chunks.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.chunks, color: data.colors.chunks, yAxis: 0
});
series.disk.push({
name: server.serverName, type: spline, tooltip: tooltip.zeroDecimals,
data: server.values.disk, color: data.colors.high, zones: zones.disk, yAxis: 0
});
}
if (data.errors.length) {
return <ErrorViewCard error={data.errors[0]}/>
}
return (
<Card>
<CardTabs tabs={[
{
name: t('html.label.playersOnline'), icon: faUser, color: 'light-blue', href: 'players-online',
element: <Tab data={series.players} yAxis={yAxisConfigurations.PLAYERS_ONLINE}/>
}, {
name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps',
element: <Tab data={series.tps} yAxis={yAxisConfigurations.TPS}/>
}, {
name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu',
element: <Tab data={series.cpu} yAxis={yAxisConfigurations.CPU}/>
}, {
name: t('html.label.ram'), icon: faMicrochip, color: 'light-green', href: 'ram',
element: <Tab data={series.ram} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities',
element: <Tab data={series.entities} yAxis={yAxisConfigurations.ENTITIES}/>
}, {
name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks',
element: <Tab data={series.chunks} yAxis={yAxisConfigurations.CHUNKS}/>
}, {
name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk',
element: <Tab data={series.disk} yAxis={yAxisConfigurations.RAM_OR_DISK}/>
}, {
name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping',
element: networkMetadata ? <PingTab identifier={networkMetadata.currentServer.serverUUID}/> :
<ChartLoader/>
},
]}/>
</Card>
)
};
export default PerformanceGraphsCard

View File

@ -14,6 +14,7 @@ import CpuRamPerformanceGraph from "../../../graphs/performance/CpuRamPerformanc
import WorldPerformanceGraph from "../../../graphs/performance/WorldPerformanceGraph";
import DiskPerformanceGraph from "../../../graphs/performance/DiskPerformanceGraph";
import PingGraph from "../../../graphs/performance/PingGraph";
import {mapPerformanceDataToSeries} from "../../../../util/graphs";
const AllGraphTab = ({data, dataSeries, loadingError}) => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
@ -58,43 +59,6 @@ const PingGraphTab = ({identifier}) => {
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();
@ -104,7 +68,7 @@ const PerformanceGraphsCard = () => {
useEffect(() => {
if (data) {
mapToDataSeries(data.values).then(parsed => setParsedData(parsed))
mapPerformanceDataToSeries(data.values).then(parsed => setParsedData(parsed))
}
}, [data, setParsedData]);

View File

@ -6,7 +6,7 @@ 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 LineGraph = ({id, series, legendEnabled, tall, yAxis}) => {
const {t} = useTranslation()
const {graphTheming, nightModeEnabled} = useTheme();
@ -20,7 +20,7 @@ const LineGraph = ({id, series}) => {
selected: 2,
buttons: linegraphButtons
},
yAxis: {
yAxis: yAxis || {
softMax: 2,
softMin: 0
},
@ -30,12 +30,17 @@ const LineGraph = ({id, series}) => {
fillOpacity: nightModeEnabled ? 0.2 : 0.4
}
},
legend: {
enabled: legendEnabled
},
series: series
})
}, [series, graphTheming, id, t, nightModeEnabled])
}, [series, graphTheming, id, t, nightModeEnabled, legendEnabled, yAxis])
const style = tall ? {height: "450px"} : undefined;
return (
<div className="chart-area" id={id}>
<div className="chart-area" style={style} id={id}>
<span className="loader"/>
</div>
)

View File

@ -0,0 +1,24 @@
import React from 'react';
const MultiSelect = ({options, selectedIndexes, setSelectedIndexes}) => {
const handleChange = (event) => {
const renderedOptions = Object.values(event.target.selectedOptions)
.map(htmlElement => htmlElement.text)
.map(option => options.indexOf(option));
setSelectedIndexes(renderedOptions);
}
return (
<select className="form-control" multiple
onChange={handleChange}>
{options.map((option, i) => {
return (
<option key={i} value={selectedIndexes.includes(i)}
selected={selectedIndexes.includes(i)}>{option}</option>
)
})}
</select>
)
};
export default MultiSelect

View File

@ -14,11 +14,11 @@ 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";
import {ChartLoader} from "../navigation/Loader";
const PerformanceAsNumbersTable = ({data}) => {
const {t} = useTranslation();
if (!data) return <CardLoader/>;
if (!data) return <ChartLoader/>;
return (
<AsNumbersTable
@ -31,12 +31,19 @@ const PerformanceAsNumbersTable = ({data}) => {
data.low_tps_spikes_24h
]}/>
<TableRow icon={faPowerOff} color="red"
text={t('html.label.serverDowntime') + ' (' + t('generic.noData') + ')'}
text={t(data.avg_server_downtime_30d ? 'html.label.serverDowntime' : 'html.label.totalServerDowntime') + ' (' + t('generic.noData') + ')'}
values={[
data.server_downtime_30d,
data.server_downtime_7d,
data.server_downtime_24h
]}/>
<TableRow icon={faPowerOff} color="red"
text={t('html.label.averageServerDowntime')}
values={[
data.avg_server_downtime_30d,
data.avg_server_downtime_7d,
data.avg_server_downtime_24h
]}/>
<TableRow icon={faUser} color="light-blue" text={t('html.label.averagePlayers')}
values={[
data.players_30d,

View File

@ -1,5 +1,5 @@
import {createContext, useCallback, useContext, useEffect, useState} from "react";
import {fetchPlanMetadata} from "../service/metadataService";
import {fetchNetworkMetadata, fetchPlanMetadata} from "../service/metadataService";
import terminal from '../Terminal-icon.png'
@ -13,6 +13,12 @@ export const MetadataContextProvider = ({children}) => {
const {data, error} = await fetchPlanMetadata();
if (data) {
setMetadata(data);
if (data.isProxy) {
const {data: networkMetadata} = await fetchNetworkMetadata(); // error ignored
if (networkMetadata) {
setMetadata({...data, networkMetadata})
}
}
} else if (error) {
setMetadata({metadataError: error})
}

View File

@ -29,3 +29,8 @@ export const fetchNetworkPingTable = async (timestamp) => {
const url = `/v1/network/pingTable?timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchNetworkPerformanceOverview = async (timestamp, serverUUIDs) => {
const url = `/v1/network/performanceOverview?servers=${encodeURIComponent(JSON.stringify(serverUUIDs))}&timestamp=${timestamp}`;
return doGetRequest(url);
}

View File

@ -107,8 +107,8 @@ export const fetchGeolocations = async (timestamp, identifier) => {
return doGetRequest(url);
}
export const fetchOptimizedPerformance = async (timestamp, identifier) => {
const url = `/v1/graph?type=optimizedPerformance&server=${identifier}&timestamp=${timestamp}`;
export const fetchOptimizedPerformance = async (timestamp, identifier, after) => {
const url = `/v1/graph?type=optimizedPerformance&server=${identifier}&timestamp=${timestamp}&after=${after}`;
return doGetRequest(url);
}

View File

@ -23,3 +23,98 @@ export const tooltip = {
twoDecimals: {valueDecimals: 2},
zeroDecimals: {valueDecimals: 0}
}
export const mapPerformanceDataToSeries = 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();
}))
};
export const yAxisConfigurations = {
PLAYERS_ONLINE: {
labels: {
formatter: function () {
return this.value + ' P';
}
},
softMin: 0,
softMax: 2
},
TPS: {
opposite: true,
labels: {
formatter: function () {
return this.value + ' TPS';
}
},
softMin: 0,
softMax: 20
},
CPU: {
opposite: true,
labels: {
formatter: function () {
return this.value + '%';
}
},
softMin: 0,
softMax: 100
},
RAM_OR_DISK: {
labels: {
formatter: function () {
return this.value + ' MB';
}
},
softMin: 0
},
ENTITIES: {
opposite: true,
labels: {
formatter: function () {
return this.value + ' E';
}
},
softMin: 0,
softMax: 2
},
CHUNKS: {
labels: {
formatter: function () {
return this.value + ' C';
}
},
softMin: 0
}
}

View File

@ -25,17 +25,14 @@ import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons";
import {SwitchTransition} from "react-transition-group";
import MainPageRedirect from "../../components/navigation/MainPageRedirect";
import {ServerExtensionContextProvider, useServerExtensionContext} from "../../hooks/serverExtensionDataContext";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchNetworkMetadata} from "../../service/metadataService";
import {iconTypeToFontAwesomeClass} from "../../util/icons";
const NetworkSidebar = () => {
const {t, i18n} = useTranslation();
const {sidebarItems, setSidebarItems} = useNavigation();
const {networkMetadata} = useMetadata();
const {extensionData} = useServerExtensionContext();
const {data: networkMetadata} = useDataRequest(fetchNetworkMetadata, [])
useEffect(() => {
const servers = networkMetadata?.servers || [];
const items = [

View File

@ -0,0 +1,119 @@
import React, {useCallback, useEffect, useState} from 'react';
import LoadIn from "../../components/animation/LoadIn";
import {Card, Col, Row} from "react-bootstrap-v5";
import {useMetadata} from "../../hooks/metadataHook";
import CardHeader from "../../components/cards/CardHeader";
import {faServer} from "@fortawesome/free-solid-svg-icons";
import MultiSelect from "../../components/input/MultiSelect";
import {useTranslation} from "react-i18next";
import {fetchOptimizedPerformance} from "../../service/serverService";
import {fetchNetworkPerformanceOverview} from "../../service/networkService";
import PerformanceAsNumbersCard from "../../components/cards/server/tables/PerformanceAsNumbersCard";
import {useNavigation} from "../../hooks/navigationHook";
import {mapPerformanceDataToSeries} from "../../util/graphs";
import PerformanceGraphsCard from "../../components/cards/network/PerformanceGraphsCard";
const NetworkPerformance = () => {
const {t} = useTranslation();
const {networkMetadata} = useMetadata();
const {updateRequested} = useNavigation();
const [serverOptions, setServerOptions] = useState([]);
const [selectedOptions, setSelectedOptions] = useState([]);
const [visualizedServers, setVisualizedServers] = useState([]);
const initializeServerOptions = () => {
if (networkMetadata) {
const options = networkMetadata.servers;
setServerOptions(options);
const indexOfProxy = options
.findIndex(option => option.serverName === networkMetadata.currentServer.serverName);
setSelectedOptions([indexOfProxy]);
setVisualizedServers([indexOfProxy]);
}
};
useEffect(initializeServerOptions, [networkMetadata, setVisualizedServers]);
const applySelected = () => {
setVisualizedServers(selectedOptions);
}
const [performanceData, setPerformanceData] = useState({});
const loadPerformanceData = useCallback(async () => {
const loaded = {
servers: [],
data: [],
values: [],
errors: [],
zones: {},
colors: {},
timestamp_f: ''
}
const time = new Date().getTime();
const monthMs = 2592000000;
const after = time - monthMs;
for (const index of visualizedServers) {
const server = serverOptions[index];
const {data, error} = await fetchOptimizedPerformance(time, encodeURIComponent(server.serverUUID), after);
if (data) {
loaded.servers.push(server);
const values = data.values;
delete data.values;
loaded.data.push(data);
loaded.values.push(await mapPerformanceDataToSeries(values));
loaded.zones = data.zones;
loaded.colors = data.colors;
loaded.timestamp_f = data.timestamp_f;
} else if (error) {
loaded.errors.push(error);
}
}
const selectedUUIDs = visualizedServers
.map(index => serverOptions[index])
.map(server => server.serverUUID);
const {data, error} = await fetchNetworkPerformanceOverview(time, selectedUUIDs);
if (error) loaded.errors.push(error);
setPerformanceData({...loaded, overview: data});
}, [visualizedServers, serverOptions, setPerformanceData])
useEffect(() => {
loadPerformanceData();
}, [loadPerformanceData, visualizedServers, updateRequested]);
const isUpToDate = visualizedServers.every((s, i) => s === selectedOptions[i]);
return (
<LoadIn>
<section className={"network_performance"}>
<Row>
<Col>
<PerformanceGraphsCard data={performanceData}/>
</Col>
</Row>
<Row>
<Col md={8}>
<PerformanceAsNumbersCard data={performanceData?.overview?.numbers}/>
</Col>
<Col md={4}>
<Card>
<CardHeader icon={faServer} color={'light-green'} label={t('html.label.serverSelector')}/>
<MultiSelect options={serverOptions.map(server => server.serverName)}
selectedIndexes={selectedOptions}
setSelectedIndexes={setSelectedOptions}/>
<button className={'btn bg-transparent'} onClick={applySelected} disabled={isUpToDate}>
{t('html.label.apply')}
</button>
</Card>
</Col>
</Row>
</section>
</LoadIn>
)
};
export default NetworkPerformance