From f4aaa72f4c85d991625ebd01c3a1470ddae370f4 Mon Sep 17 00:00:00 2001 From: Aurora Lahtela <24460436+AuroraLS3@users.noreply.github.com> Date: Fri, 9 Sep 2022 19:10:06 +0300 Subject: [PATCH] Implemented network servers overview in React - Changed the layout to use a table instead of custom elements for more efficient look. - Added sorting options to the new table - Added a total calculator at the table footer Affects issues: - Close #1205 --- .../plan/settings/locale/lang/HtmlLang.java | 2 + Plan/react/dashboard/src/App.js | 2 + .../cards/network/QuickViewDataCard.js | 47 +++++++ .../cards/network/QuickViewGraphCard.js | 19 +++ .../cards/network/ServersTableCard.js | 93 +++++++++++++ .../src/components/table/ServersTable.js | 127 ++++++++++++++++++ .../dashboard/src/service/networkService.js | 5 + Plan/react/dashboard/src/util/calculation.js | 7 + .../src/views/network/NetworkServers.js | 32 +++++ 9 files changed, 334 insertions(+) create mode 100644 Plan/react/dashboard/src/components/cards/network/QuickViewDataCard.js create mode 100644 Plan/react/dashboard/src/components/cards/network/QuickViewGraphCard.js create mode 100644 Plan/react/dashboard/src/components/cards/network/ServersTableCard.js create mode 100644 Plan/react/dashboard/src/components/table/ServersTable.js create mode 100644 Plan/react/dashboard/src/util/calculation.js create mode 100644 Plan/react/dashboard/src/views/network/NetworkServers.js 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 080b5b997..9e8055ec7 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 @@ -243,6 +243,8 @@ public enum HtmlLang implements Lang { LABEL_MAX_FREE_DISK("html.label.maxFreeDisk", "Max Free Disk"), LABEL_MIN_FREE_DISK("html.label.minFreeDisk", "Min Free Disk"), LABEL_CURRENT_UPTIME("html.label.currentUptime", "Current Uptime"), + LABEL_TOTAL("html.label.total", "Total"), + LABEL_ALPHABETICAL("html.label.alphabetical", "Alphabetical"), 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 6e4bbf2a2..2fbf184d4 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -36,6 +36,7 @@ const ServerJoinAddresses = React.lazy(() => import("./views/server/ServerJoinAd const NetworkPage = React.lazy(() => import("./views/layout/NetworkPage")); const NetworkOverview = React.lazy(() => import("./views/network/NetworkOverview")); +const NetworkServers = React.lazy(() => import("./views/network/NetworkServers")); const PlayersPage = React.lazy(() => import("./views/layout/PlayersPage")); const AllPlayers = React.lazy(() => import("./views/players/AllPlayers")); @@ -117,6 +118,7 @@ function App() { }> }/> }/> + }/> }/> }/> }/> diff --git a/Plan/react/dashboard/src/components/cards/network/QuickViewDataCard.js b/Plan/react/dashboard/src/components/cards/network/QuickViewDataCard.js new file mode 100644 index 000000000..3aad6395f --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/network/QuickViewDataCard.js @@ -0,0 +1,47 @@ +import React from 'react'; +import {Card} from "react-bootstrap-v5"; +import CardHeader from "../CardHeader"; +import { + faBookOpen, + faChartLine, + faExclamationCircle, + faPowerOff, + faTachometerAlt, + faUsers +} from "@fortawesome/free-solid-svg-icons"; +import {useTranslation} from "react-i18next"; +import Datapoint from "../../Datapoint"; + +const QuickViewDataCard = ({server}) => { + const {t} = useTranslation() + + return ( + + + + + + +
+

{t('html.label.last7days')}

+ + + + + +
+
+ ) +}; + +export default QuickViewDataCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/network/QuickViewGraphCard.js b/Plan/react/dashboard/src/components/cards/network/QuickViewGraphCard.js new file mode 100644 index 000000000..7706c5cda --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/network/QuickViewGraphCard.js @@ -0,0 +1,19 @@ +import React from 'react'; +import CardHeader from "../CardHeader"; +import {Card} from "react-bootstrap-v5"; +import PlayersOnlineGraph from "../../graphs/PlayersOnlineGraph"; +import {faChartArea} from "@fortawesome/free-solid-svg-icons"; +import {useTranslation} from "react-i18next"; + +const QuickViewGraphCard = ({server}) => { + const {t} = useTranslation(); + return ( + + + + + ) +}; + +export default QuickViewGraphCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/network/ServersTableCard.js b/Plan/react/dashboard/src/components/cards/network/ServersTableCard.js new file mode 100644 index 000000000..432538181 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/network/ServersTableCard.js @@ -0,0 +1,93 @@ +import React, {useCallback, useState} from 'react'; +import {Card, Dropdown} from "react-bootstrap-v5"; +import ServersTable, {ServerSortOption} from "../../table/ServersTable"; +import { + faNetworkWired, + faSort, + faSortAlphaDown, + faSortAlphaUp, + faSortNumericDown, + faSortNumericUp +} from "@fortawesome/free-solid-svg-icons"; +import {useTranslation} from "react-i18next"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle"; +import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu"; +import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem"; + +const SortDropDown = ({sortBy, sortReversed, setSortBy}) => { + const {t} = useTranslation(); + + const sortOptions = Object.values(ServerSortOption); + + const getSortIcon = useCallback(() => { + switch (sortBy) { + case ServerSortOption.ALPHABETICAL: + return sortReversed ? faSortAlphaUp : faSortAlphaDown; + case ServerSortOption.PLAYERS_ONLINE: + // case ServerSortOption.DOWNTIME: + case ServerSortOption.AVERAGE_TPS: + case ServerSortOption.LOW_TPS_SPIKES: + case ServerSortOption.NEW_PLAYERS: + case ServerSortOption.UNIQUE_PLAYERS: + case ServerSortOption.REGISTERED_PLAYERS: + return sortReversed ? faSortNumericDown : faSortNumericUp; + default: + return faSort; + } + }, [sortBy, sortReversed]) + + return ( + + + {t(sortBy)} + + + +
Sort by
+ {sortOptions.map((option, i) => ( + setSortBy(option)}> + {t(option)} + + ))} +
+
+ ) +} + +const ServersTableCard = ({servers, onSelect}) => { + const {t} = useTranslation(); + const [sortBy, setSortBy] = useState(ServerSortOption.ALPHABETICAL); + const [sortReversed, setSortReversed] = useState(false); + + const setSort = option => { + if (sortBy === option) { + setSortReversed(!sortReversed); + } else { + setSortBy(option); + setSortReversed(false); + } + } + + return ( + + +
+ {t('html.label.servers')} +
+ +
+ {!servers.length && +

No servers found in the database.

+

It appears that Plan is not installed on any game servers or not connected to the same database. + See wiki for Network tutorial.

+
} + {servers.length && } +
+ ) +}; + +export default ServersTableCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/table/ServersTable.js b/Plan/react/dashboard/src/components/table/ServersTable.js new file mode 100644 index 000000000..54d900b02 --- /dev/null +++ b/Plan/react/dashboard/src/components/table/ServersTable.js @@ -0,0 +1,127 @@ +import React from "react"; +import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; +import {faCaretSquareRight, faLineChart, faLink, faServer, faUser, faUsers} from "@fortawesome/free-solid-svg-icons"; +import {useTheme} from "../../hooks/themeHook"; +import {useTranslation} from "react-i18next"; +import Scrollable from "../Scrollable"; +import {NavLink} from "react-router-dom"; +import {calculateSum} from "../../util/calculation"; + +const ServerRow = ({server, onQuickView}) => { + const {t} = useTranslation(); + return ( + + {server.name} + + {t('html.label.serverAnalysis')} + + + {server.players} + {server.online} + + + + + ); +} + +const sortBySometimesNumericProperty = (propertyName) => (a, b) => { + if (typeof (a[propertyName]) === 'number' && typeof (b[propertyName]) === 'number') return a[propertyName] - b[propertyName]; + if (typeof (a[propertyName]) === 'number') return 1; + if (typeof (b[propertyName]) === 'number') return -1; + return 0; +} +const sortByNumericProperty = (propertyName) => (a, b) => b[propertyName] - a[propertyName]; // Biggest first +const sortBeforeReverse = (servers, sortBy) => { + const sorting = [...servers]; + switch (sortBy) { + case ServerSortOption.PLAYERS_ONLINE: + return sorting.sort(sortBySometimesNumericProperty('online')); + case ServerSortOption.AVERAGE_TPS: + return sorting.sort(sortBySometimesNumericProperty('avg_tps')); + case ServerSortOption.UNIQUE_PLAYERS: + return sorting.sort(sortByNumericProperty('unique_players')); + case ServerSortOption.NEW_PLAYERS: + return sorting.sort(sortByNumericProperty('new_players')); + case ServerSortOption.REGISTERED_PLAYERS: + return sorting.sort(sortByNumericProperty('players')); + // case ServerSortOption.DOWNTIME: + // return servers.sort(sortByNumericProperty('downtime_raw')); + case ServerSortOption.ALPHABETICAL: + default: + return sorting; + } +} + +const reverse = (array) => { + const reversedArray = []; + for (let i = array.length - 1; i >= 0; i--) { + reversedArray.push(array[i]); + } + return reversedArray; +} + +const sort = (servers, sortBy, sortReversed) => { + return sortReversed ? reverse(sortBeforeReverse(servers, sortBy)) : sortBeforeReverse(servers, sortBy); +} + +export const ServerSortOption = { + ALPHABETICAL: 'html.label.alphabetical', + AVERAGE_TPS: 'html.label.averageTps', + // DOWNTIME: 'html.label.downtime', + LOW_TPS_SPIKES: 'html.label.lowTpsSpikes', + NEW_PLAYERS: 'html.label.newPlayers', + PLAYERS_ONLINE: 'html.label.playersOnline', + REGISTERED_PLAYERS: 'html.label.registeredPlayers', + UNIQUE_PLAYERS: 'html.label.uniquePlayers', +} + +const ServersTable = ({servers, onSelect, sortBy, sortReversed}) => { + const {t} = useTranslation(); + const {nightModeEnabled} = useTheme(); + + const sortedServers = sort(servers, sortBy, sortReversed); + + return ( + + + + + + + + + + + + + {sortedServers.length ? sortedServers.map((server, i) => onSelect(servers.indexOf(server))}/>) : + + + + + + } + + {sortedServers.length && + + + + + + + } +
{t('html.label.server')} {t('html.label.serverAnalysis')} {t('html.label.registeredPlayers')} {t('html.label.playersOnline')}
{t('html.generic.none')}---
{t('html.label.total')}{calculateSum(servers.map(s => s.players))}{calculateSum(servers.map(s => s.online))}
+
+ ) +}; + +export default ServersTable; \ No newline at end of file diff --git a/Plan/react/dashboard/src/service/networkService.js b/Plan/react/dashboard/src/service/networkService.js index 9110a35a5..21b4a89e1 100644 --- a/Plan/react/dashboard/src/service/networkService.js +++ b/Plan/react/dashboard/src/service/networkService.js @@ -3,4 +3,9 @@ import {doGetRequest} from "./backendConfiguration"; export const fetchNetworkOverview = async (updateRequested) => { const url = `/v1/network/overview?timestamp=${updateRequested}`; return doGetRequest(url); +} + +export const fetchServersOverview = async (updateRequested) => { + const url = `/v1/network/servers?timestamp=${updateRequested}`; + return doGetRequest(url); } \ No newline at end of file diff --git a/Plan/react/dashboard/src/util/calculation.js b/Plan/react/dashboard/src/util/calculation.js new file mode 100644 index 000000000..cd778da44 --- /dev/null +++ b/Plan/react/dashboard/src/util/calculation.js @@ -0,0 +1,7 @@ +export const calculateSum = array => { + let sum = 0; + for (let item of array) { + if (typeof (item) === "number") sum += item; + } + return sum; +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/network/NetworkServers.js b/Plan/react/dashboard/src/views/network/NetworkServers.js new file mode 100644 index 000000000..1f0202c92 --- /dev/null +++ b/Plan/react/dashboard/src/views/network/NetworkServers.js @@ -0,0 +1,32 @@ +import React, {useState} from 'react'; +import {Col, Row} from "react-bootstrap-v5"; +import {useDataRequest} from "../../hooks/dataFetchHook"; +import {fetchServersOverview} from "../../service/networkService"; +import ErrorView from "../ErrorView"; +import ServersTableCard from "../../components/cards/network/ServersTableCard"; +import QuickViewGraphCard from "../../components/cards/network/QuickViewGraphCard"; +import QuickViewDataCard from "../../components/cards/network/QuickViewDataCard"; + +const NetworkServers = () => { + const [selectedServer, setSelectedServer] = useState(0); + + const {data, loadingError} = useDataRequest(fetchServersOverview, []) + + if (loadingError) { + return + } + + return ( + + + setSelectedServer(index)}/> + + + {data?.servers.length && } + {data?.servers.length && } + + + ) +}; + +export default NetworkServers \ No newline at end of file