diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java index 48a620958..2f5034317 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/NetworkPerformanceJSONResolver.java @@ -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())); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index 34755475b..609a7bb4b 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -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"), diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index 66107f0eb..699f7f45b 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -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() { }/> }/> }/> + }/> }/> }/> }/> diff --git a/Plan/react/dashboard/src/components/cards/network/PerformanceGraphsCard.js b/Plan/react/dashboard/src/components/cards/network/PerformanceGraphsCard.js new file mode 100644 index 000000000..e28774a0b --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/network/PerformanceGraphsCard.js @@ -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 ( + + ) +} + +const PingTab = ({identifier}) => { + const {data, loadingError} = useDataRequest(fetchPingGraph, [identifier]); + + if (loadingError) return + if (!data) return ; + + return ; +} + +const PerformanceGraphsCard = ({data}) => { + const {t} = useTranslation(); + const {networkMetadata} = useMetadata(); + + if (!data || !Object.values(data).length) return + + 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 + } + + return ( + + + }, { + name: t('html.label.tps'), icon: faTachometerAlt, color: 'red', href: 'tps', + element: + }, { + name: t('html.label.cpu'), icon: faTachometerAlt, color: 'amber', href: 'cpu', + element: + }, { + name: t('html.label.ram'), icon: faMicrochip, color: 'light-green', href: 'ram', + element: + }, { + name: t('html.label.entities'), icon: faDragon, color: 'purple', href: 'entities', + element: + }, { + name: t('html.label.loadedChunks'), icon: faMap, color: 'blue-grey', href: 'chunks', + element: + }, { + name: t('html.label.diskSpace'), icon: faHdd, color: 'green', href: 'disk', + element: + }, { + name: t('html.label.ping'), icon: faSignal, color: 'amber', href: 'ping', + element: networkMetadata ? : + + }, + ]}/> + + ) +}; + +export default PerformanceGraphsCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js b/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js index baa0e6ed2..d094ff15d 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js +++ b/Plan/react/dashboard/src/components/cards/server/graphs/PerformanceGraphsCard.js @@ -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 @@ -58,43 +59,6 @@ const PingGraphTab = ({identifier}) => { return ; } -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]); diff --git a/Plan/react/dashboard/src/components/graphs/LineGraph.js b/Plan/react/dashboard/src/components/graphs/LineGraph.js index 6d05db467..3a8e0b2e5 100644 --- a/Plan/react/dashboard/src/components/graphs/LineGraph.js +++ b/Plan/react/dashboard/src/components/graphs/LineGraph.js @@ -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 ( -
+
) diff --git a/Plan/react/dashboard/src/components/input/MultiSelect.js b/Plan/react/dashboard/src/components/input/MultiSelect.js new file mode 100644 index 000000000..a483453e4 --- /dev/null +++ b/Plan/react/dashboard/src/components/input/MultiSelect.js @@ -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 ( + + ) +}; + +export default MultiSelect \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/table/PerformanceAsNumbersTable.js b/Plan/react/dashboard/src/components/table/PerformanceAsNumbersTable.js index ef4fc8dc4..82bd065da 100644 --- a/Plan/react/dashboard/src/components/table/PerformanceAsNumbersTable.js +++ b/Plan/react/dashboard/src/components/table/PerformanceAsNumbersTable.js @@ -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 ; + if (!data) return ; return ( { data.low_tps_spikes_24h ]}/> + { 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}) } diff --git a/Plan/react/dashboard/src/service/networkService.js b/Plan/react/dashboard/src/service/networkService.js index 6545b721d..b37b546ec 100644 --- a/Plan/react/dashboard/src/service/networkService.js +++ b/Plan/react/dashboard/src/service/networkService.js @@ -28,4 +28,9 @@ export const fetchNetworkPlayerbaseOverview = async (timestamp) => { 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))}×tamp=${timestamp}`; + return doGetRequest(url); } \ No newline at end of file diff --git a/Plan/react/dashboard/src/service/serverService.js b/Plan/react/dashboard/src/service/serverService.js index 293815e30..eeb4a6e39 100644 --- a/Plan/react/dashboard/src/service/serverService.js +++ b/Plan/react/dashboard/src/service/serverService.js @@ -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}×tamp=${timestamp}`; +export const fetchOptimizedPerformance = async (timestamp, identifier, after) => { + const url = `/v1/graph?type=optimizedPerformance&server=${identifier}×tamp=${timestamp}&after=${after}`; return doGetRequest(url); } diff --git a/Plan/react/dashboard/src/util/graphs.js b/Plan/react/dashboard/src/util/graphs.js index 2510bc7ab..da5e83b07 100644 --- a/Plan/react/dashboard/src/util/graphs.js +++ b/Plan/react/dashboard/src/util/graphs.js @@ -22,4 +22,99 @@ export const linegraphButtons = [{ 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 + } } \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/layout/NetworkPage.js b/Plan/react/dashboard/src/views/layout/NetworkPage.js index 97401ac8b..8a7a85bb5 100644 --- a/Plan/react/dashboard/src/views/layout/NetworkPage.js +++ b/Plan/react/dashboard/src/views/layout/NetworkPage.js @@ -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 = [ diff --git a/Plan/react/dashboard/src/views/network/NetworkPerformance.js b/Plan/react/dashboard/src/views/network/NetworkPerformance.js new file mode 100644 index 000000000..8c1ec8ca9 --- /dev/null +++ b/Plan/react/dashboard/src/views/network/NetworkPerformance.js @@ -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 ( + +
+ + + + + + + + + + + + + server.serverName)} + selectedIndexes={selectedOptions} + setSelectedIndexes={setSelectedOptions}/> + + + + +
+
+ ) +}; + +export default NetworkPerformance \ No newline at end of file