mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-27 09:00:28 +08:00
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:
parent
54c66c7232
commit
f4aaa72f4c
@ -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"),
|
||||
|
@ -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>}/>
|
||||
|
@ -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
|
@ -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
|
@ -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
|
127
Plan/react/dashboard/src/components/table/ServersTable.js
Normal file
127
Plan/react/dashboard/src/components/table/ServersTable.js
Normal 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;
|
@ -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);
|
||||
}
|
7
Plan/react/dashboard/src/util/calculation.js
Normal file
7
Plan/react/dashboard/src/util/calculation.js
Normal 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;
|
||||
}
|
32
Plan/react/dashboard/src/views/network/NetworkServers.js
Normal file
32
Plan/react/dashboard/src/views/network/NetworkServers.js
Normal 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
|
Loading…
Reference in New Issue
Block a user