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
This commit is contained in:
Aurora Lahtela 2022-09-09 19:10:06 +03:00
parent 54c66c7232
commit f4aaa72f4c
9 changed files with 334 additions and 0 deletions

View File

@ -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"),

View File

@ -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() {
<Route path="/network" element={<Lazy><NetworkPage/></Lazy>}>
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
<Route path="overview" element={<Lazy><NetworkOverview/></Lazy>}/>
<Route path="serversOverview" element={<Lazy><NetworkServers/></Lazy>}/>
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>

View File

@ -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 (
<Card>
<CardHeader icon={faBookOpen} color={'light-green'} label={server.name + ' ' + t('html.label.asNumbers')}/>
<Card.Body>
<Datapoint icon={faPowerOff} color={'light-green'} name={t('html.label.currentUptime')}
value={server.current_uptime}/>
<Datapoint name={t('html.label.lastPeak') + ' (' + server.last_peak_date + ')'}
color={'blue'} icon={faChartLine}
value={server.last_peak_players} valueLabel={t('html.unit.players')} bold/>
<Datapoint name={t('html.label.bestPeak') + ' (' + server.best_peak_date + ')'}
color={'light-green'} icon={faChartLine}
value={server.best_peak_players} valueLabel={t('html.unit.players')} bold/>
<hr/>
<p><b>{t('html.label.last7days')}</b></p>
<Datapoint icon={faUsers} color={'light-blue'} name={t('html.label.uniquePlayers')}
value={server.unique_players}/>
<Datapoint icon={faUsers} color={'light-green'} name={t('html.label.newPlayers')}
value={server.new_players}/>
<Datapoint icon={faTachometerAlt} color={'orange'} name={t('html.label.averageTps')}
value={server.avg_tps}/>
<Datapoint icon={faExclamationCircle} color={'red'} name={t('html.label.lowTpsSpikes')}
value={server.low_tps_spikes}/>
<Datapoint icon={faPowerOff} color={'red'} name={t('html.label.downtime')}
value={server.downtime}/>
</Card.Body>
</Card>
)
};
export default QuickViewDataCard

View File

@ -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 (
<Card>
<CardHeader icon={faChartArea} color={'light-blue'}
label={server.name + ' ' + t('html.label.onlineActivity') + ' (' + t('html.label.thirtyDays') + ')'}/>
<PlayersOnlineGraph data={server.playersOnline}/>
</Card>
)
};
export default QuickViewGraphCard

View File

@ -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 (
<Dropdown className="float-end">
<DropdownToggle variant=''>
<Fa icon={getSortIcon()}/> {t(sortBy)}
</DropdownToggle>
<DropdownMenu>
<h6 className="dropdown-header">Sort by</h6>
{sortOptions.map((option, i) => (
<DropdownItem key={i} onClick={() => setSortBy(option)}>
{t(option)}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
)
}
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 (
<Card>
<Card.Header style={{width: "100%"}}>
<h6 className="col-black">
<Fa icon={faNetworkWired} className={"col-light-green"}/> {t('html.label.servers')}
</h6>
<SortDropDown sortBy={sortBy} setSortBy={setSort} sortReversed={sortReversed}/>
</Card.Header>
{!servers.length && <Card.Body>
<p>No servers found in the database.</p>
<p>It appears that Plan is not installed on any game servers or not connected to the same database.
See <a href="https://github.com/plan-player-analytics/Plan/wiki">wiki</a> for Network tutorial.</p>
</Card.Body>}
{servers.length && <ServersTable servers={servers}
onSelect={onSelect}
sortBy={sortBy}
sortReversed={sortReversed}/>}
</Card>
)
};
export default ServersTableCard

View File

@ -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 (
<tr>
<td>{server.name}</td>
<td className="p-1">
<NavLink to={"/server/" + encodeURIComponent(server.name)}
className={'btn bg-transparent col-light-green'}><Fa
icon={faLink}/> {t('html.label.serverAnalysis')}
</NavLink>
</td>
<td>{server.players}</td>
<td>{server.online}</td>
<td className="p-1">
<button className={'btn bg-light-blue float-right'}
title={t('html.label.quickView') + ': ' + server.name}
onClick={onQuickView}
>
<Fa icon={faCaretSquareRight}/>
</button>
</td>
</tr>
);
}
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 (
<Scrollable>
<table className={"table mb-0 table-striped" + (nightModeEnabled ? " table-dark" : '')}>
<thead>
<tr>
<th><Fa icon={faServer}/> {t('html.label.server')}</th>
<th><Fa icon={faLineChart}/> {t('html.label.serverAnalysis')}</th>
<th><Fa icon={faUsers}/> {t('html.label.registeredPlayers')}</th>
<th><Fa icon={faUser}/> {t('html.label.playersOnline')}</th>
<th></th>
</tr>
</thead>
<tbody>
{sortedServers.length ? sortedServers.map((server, i) => <ServerRow key={i} server={server}
onQuickView={() => onSelect(servers.indexOf(server))}/>) :
<tr>
<td>{t('html.generic.none')}</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>}
</tbody>
{sortedServers.length && <tfoot>
<tr>
<td><b>{t('html.label.total')}</b></td>
<td></td>
<td>{calculateSum(servers.map(s => s.players))}</td>
<td>{calculateSum(servers.map(s => s.online))}</td>
</tr>
</tfoot>}
</table>
</Scrollable>
)
};
export default ServersTable;

View File

@ -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);
}

View File

@ -0,0 +1,7 @@
export const calculateSum = array => {
let sum = 0;
for (let item of array) {
if (typeof (item) === "number") sum += item;
}
return sum;
}

View File

@ -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 <ErrorView error={loadingError}/>
}
return (
<Row>
<Col md={6}>
<ServersTableCard servers={data?.servers || []} onSelect={(index) => setSelectedServer(index)}/>
</Col>
<Col md={6}>
{data?.servers.length && <QuickViewGraphCard server={data.servers[selectedServer]}/>}
{data?.servers.length && <QuickViewDataCard server={data.servers[selectedServer]}/>}
</Col>
</Row>
)
};
export default NetworkServers