mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-23 17:47:38 +01:00
Implemented network geolocations tab in React
- Add Projection selector to geolocations visualization
This commit is contained in:
parent
7a51633690
commit
cb7749d778
@ -252,6 +252,11 @@ public enum HtmlLang implements Lang {
|
||||
LABEL_ALPHABETICAL("html.label.alphabetical", "Alphabetical"),
|
||||
LABEL_SORT_BY("html.label.sortBy", "Sort By"),
|
||||
LABEL_STACKED("html.label.stacked", "Stacked"),
|
||||
LABEL_PROJECTION("html.label.geoProjection.dropdown", "Select projection"),
|
||||
LABEL_PROJECTION_MILLER("html.label.geoProjection.miller", "Miller"),
|
||||
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"),
|
||||
|
||||
LOGIN_LOGIN("html.login.login", "Login"),
|
||||
LOGIN_LOGOUT("html.login.logout", "Logout"),
|
||||
|
@ -1866,6 +1866,7 @@ a.text-dark:hover, a.text-dark:focus {
|
||||
|
||||
#wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#wrapper #content-wrapper {
|
||||
|
@ -39,6 +39,7 @@ const NetworkOverview = React.lazy(() => import("./views/network/NetworkOverview
|
||||
const NetworkServers = React.lazy(() => import("./views/network/NetworkServers"));
|
||||
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 PlayersPage = React.lazy(() => import("./views/layout/PlayersPage"));
|
||||
const AllPlayers = React.lazy(() => import("./views/players/AllPlayers"));
|
||||
@ -124,6 +125,7 @@ function App() {
|
||||
<Route path="sessions" element={<Lazy><NetworkSessions/></Lazy>}/>
|
||||
<Route path="join-addresses" element={<Lazy><NetworkJoinAddresses/></Lazy>}/>
|
||||
<Route path="players" element={<Lazy><AllPlayers/></Lazy>}/>
|
||||
<Route path="geolocations" element={<Lazy><NetworkGeolocations/></Lazy>}/>
|
||||
<Route path="plugins-overview" element={<Lazy><ServerPluginData/></Lazy>}/>
|
||||
<Route path="plugins/:plugin" element={<Lazy><ServerWidePluginData/></Lazy>}/>
|
||||
<Route path="*" element={<ErrorView error={{
|
||||
|
@ -1,22 +1,49 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {Card, Col, Row} from "react-bootstrap-v5";
|
||||
import {Card, Col, Dropdown, Row} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import {faExclamationTriangle, faGlobe} from "@fortawesome/free-solid-svg-icons";
|
||||
import React, {useState} from "react";
|
||||
import {faExclamationTriangle, faGlobe, faLayerGroup} from "@fortawesome/free-solid-svg-icons";
|
||||
import GeolocationBarGraph from "../../graphs/GeolocationBarGraph";
|
||||
import GeolocationWorldMap from "../../graphs/GeolocationWorldMap";
|
||||
import GeolocationWorldMap, {ProjectionOptions} from "../../graphs/GeolocationWorldMap";
|
||||
import {CardLoader} from "../../navigation/Loader";
|
||||
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 ProjectionDropDown = ({projection, setProjection}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const projectionOptions = Object.values(ProjectionOptions);
|
||||
|
||||
return (
|
||||
<Dropdown className="float-end" style={{position: "absolute", right: "0.5rem"}}
|
||||
title={t('html.label.geoProjection.dropdown')}>
|
||||
<DropdownToggle variant=''>
|
||||
<Fa icon={faLayerGroup}/> {t(projection)}
|
||||
</DropdownToggle>
|
||||
|
||||
<DropdownMenu>
|
||||
<h6 className="dropdown-header">{t('html.label.geoProjection.dropdown')}</h6>
|
||||
{projectionOptions.map((option, i) => (
|
||||
<DropdownItem key={i} onClick={() => setProjection(option)}>
|
||||
{t(option)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const GeolocationsCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
const [projection, setProjection] = useState(ProjectionOptions.MILLER);
|
||||
|
||||
if (!data) return <CardLoader/>
|
||||
|
||||
if (!data?.geolocations_enabled) {
|
||||
return (
|
||||
<div className="alert alert-warning mb-0" id="geolocation-warning">
|
||||
<Fa icon={faExclamationTriangle}/>{' '}
|
||||
{t('html.description.noGeolocations')}
|
||||
<Fa icon={faExclamationTriangle}/>{' '}{t('html.description.noGeolocations')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -27,6 +54,7 @@ const GeolocationsCard = ({data}) => {
|
||||
<h6 className="col-black">
|
||||
<Fa icon={faGlobe} className="col-green"/> {t('html.label.geolocations')}
|
||||
</h6>
|
||||
<ProjectionDropDown projection={projection} setProjection={setProjection}/>
|
||||
</Card.Header>
|
||||
<Card.Body className="chart-area" style={{height: "100%"}}>
|
||||
<Row>
|
||||
@ -34,7 +62,8 @@ const GeolocationsCard = ({data}) => {
|
||||
<GeolocationBarGraph series={data.geolocation_bar_series} color={data.colors.bars}/>
|
||||
</Col>
|
||||
<Col md={9}>
|
||||
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}/>
|
||||
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}
|
||||
projection={projection}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card.Body>
|
||||
|
@ -8,7 +8,7 @@ import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faChartColumn} from "@fortawesome/free-solid-svg-icons";
|
||||
import JoinAddressGraph from "../../../graphs/JoinAddressGraph";
|
||||
import Toggle from "../../../Toggle";
|
||||
import Toggle from "../../../input/Toggle";
|
||||
|
||||
const JoinAddressGraphCard = ({identifier}) => {
|
||||
const {t} = useTranslation();
|
||||
|
@ -1,13 +1,34 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import Highcharts from 'highcharts/highmaps.js';
|
||||
import map from '@highcharts/map-collection/custom/world.geo.json';
|
||||
import topology from '@highcharts/map-collection/custom/world.topo.json';
|
||||
import Accessibility from "highcharts/modules/accessibility";
|
||||
import NoDataDisplay from "highcharts/modules/no-data-to-display";
|
||||
|
||||
const GeolocationWorldMap = ({series, colors}) => {
|
||||
export const ProjectionOptions = {
|
||||
MILLER: "html.label.geoProjection.miller",
|
||||
MERCATOR: "html.label.geoProjection.mercator",
|
||||
EQUAL_EARTH: "html.label.geoProjection.equalEarth"
|
||||
// ORTOGRAPHIC: "html.label.geoProjection.ortographic"
|
||||
}
|
||||
|
||||
const getProjection = option => {
|
||||
switch (option) {
|
||||
case ProjectionOptions.MERCATOR:
|
||||
return {name: 'WebMercator'};
|
||||
case ProjectionOptions.EQUAL_EARTH:
|
||||
return {name: 'EqualEarth'};
|
||||
// Ortographic projection stops working after a while for some reason
|
||||
// case ProjectionOptions.ORTOGRAPHIC:
|
||||
// return {name: 'Orthographic'};
|
||||
case ProjectionOptions.MILLER:
|
||||
default:
|
||||
return {name: 'Miller'};
|
||||
}
|
||||
}
|
||||
|
||||
const GeolocationWorldMap = ({series, colors, projection}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
@ -15,7 +36,6 @@ const GeolocationWorldMap = ({series, colors}) => {
|
||||
const mapSeries = {
|
||||
name: t('html.label.players'),
|
||||
type: 'map',
|
||||
mapData: map,
|
||||
data: series,
|
||||
joinBy: ['iso-a3', 'code']
|
||||
};
|
||||
@ -26,24 +46,31 @@ const GeolocationWorldMap = ({series, colors}) => {
|
||||
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}});
|
||||
Highcharts.mapChart('countryWorldMap', {
|
||||
chart: {
|
||||
map: topology,
|
||||
animation: true
|
||||
},
|
||||
title: {text: ''},
|
||||
|
||||
mapNavigation: {
|
||||
enabled: true,
|
||||
enableDoubleClickZoomTo: true
|
||||
enableDoubleClickZoomTo: true,
|
||||
enableMouseWheelZoom: true,
|
||||
enableTouchZoom: true
|
||||
},
|
||||
|
||||
mapView: {
|
||||
projection: getProjection(projection)
|
||||
},
|
||||
|
||||
colorAxis: {
|
||||
min: 1,
|
||||
type: 'logarithmic',
|
||||
minColor: nightModeEnabled ? withReducedSaturation(colors.low) : colors.low,
|
||||
maxColor: nightModeEnabled ? withReducedSaturation(colors.high) : colors.high
|
||||
minColor: colors.low,
|
||||
maxColor: colors.high
|
||||
},
|
||||
series: [mapSeries]
|
||||
})
|
||||
}, [colors, series, graphTheming, nightModeEnabled, t]);
|
||||
}, [colors, series, graphTheming, nightModeEnabled, t, projection]);
|
||||
|
||||
return (<div id="countryWorldMap"/>);
|
||||
};
|
||||
|
29
Plan/react/dashboard/src/components/input/BasicDropdown.js
Normal file
29
Plan/react/dashboard/src/components/input/BasicDropdown.js
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu";
|
||||
import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const BasicDropdown = ({selected, optionList, onChange, optionLabelMapper, icon, title}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
return (
|
||||
<BasicDropdown className="float-end" style={{position: "absolute", right: "0.5rem"}} title={t(title)}>
|
||||
<DropdownToggle variant=''>
|
||||
<Fa icon={icon}/> {t(optionLabelMapper ? optionLabelMapper(selected) : selected)}
|
||||
</DropdownToggle>
|
||||
|
||||
<DropdownMenu>
|
||||
<h6 className="dropdown-header">{t(title)}</h6>
|
||||
{optionList.map((option, i) => (
|
||||
<DropdownItem key={i} onClick={() => onChange(option)}>
|
||||
{t(optionLabelMapper ? optionLabelMapper(option) : option)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</BasicDropdown>
|
||||
)
|
||||
};
|
||||
|
||||
export default BasicDropdown
|
@ -59,7 +59,7 @@ const Header = ({page, tab}) => {
|
||||
<span className="topbar-divider"/>
|
||||
<div className="refresh-element">
|
||||
<button onClick={requestUpdate}>
|
||||
<Fa icon={faSyncAlt} spin={updating}/>
|
||||
<Fa icon={faSyncAlt} spin={Boolean(updating)}/>
|
||||
</button>
|
||||
{' '}
|
||||
<span className="refresh-time">{lastUpdate.formatted}</span>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import Scrollable from "../Scrollable";
|
||||
|
||||
const PingRow = ({country}) => {
|
||||
return (
|
||||
@ -18,24 +19,26 @@ const PingTable = ({countries}) => {
|
||||
const {nightModeEnabled} = useTheme();
|
||||
|
||||
return (
|
||||
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<thead className="bg-amber">
|
||||
<tr>
|
||||
<th>{t('html.label.country')}</th>
|
||||
<th>{t('html.label.averagePing')}</th>
|
||||
<th>{t('html.label.bestPing')}</th>
|
||||
<th>{t('html.label.worstPing')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{countries.length ? countries.map((country, i) => <PingRow key={i} country={country}/>) : <tr>
|
||||
<td>{t('generic.noData')}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
<Scrollable>
|
||||
<table className={"table mb-0" + (nightModeEnabled ? " table-dark" : '')}>
|
||||
<thead className="bg-amber" style={{position: "sticky", top: 0}}>
|
||||
<tr>
|
||||
<th>{t('html.label.country')}</th>
|
||||
<th>{t('html.label.averagePing')}</th>
|
||||
<th>{t('html.label.bestPing')}</th>
|
||||
<th>{t('html.label.worstPing')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{countries.length ? countries.map((country, i) => <PingRow key={i} country={country}/>) : <tr>
|
||||
<td>{t('generic.noData')}</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</Scrollable>
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -18,4 +18,9 @@ export const fetchServerPie = async (timestamp) => {
|
||||
export const fetchNetworkSessionsOverview = async (timestamp) => {
|
||||
const url = `/v1/network/sessionsOverview?timestamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchNetworkPingTable = async (timestamp) => {
|
||||
const url = `/v1/network/pingTable?timestamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
@ -58,7 +58,7 @@ export const fetchPlayers = async (timestamp, identifier) => {
|
||||
}
|
||||
|
||||
export const fetchPingTable = async (timestamp, identifier) => {
|
||||
const url = identifier ? `/v1/pingTable?server=${identifier}×tamp=${timestamp}` : `/v1/pingTable?timestamp=${timestamp}`;
|
||||
const url = `/v1/pingTable?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
@ -102,7 +102,8 @@ export const fetchWorldPie = async (timestamp, identifier) => {
|
||||
}
|
||||
|
||||
export const fetchGeolocations = async (timestamp, identifier) => {
|
||||
const url = `/v1/graph?type=geolocation&server=${identifier}×tamp=${timestamp}`;
|
||||
const url = identifier ? `/v1/graph?type=geolocation&server=${identifier}×tamp=${timestamp}` :
|
||||
`/v1/graph?type=geolocation×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
|
@ -1868,6 +1868,7 @@ a.text-dark:hover, a.text-dark:focus {
|
||||
|
||||
#wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#wrapper #content-wrapper {
|
||||
|
@ -1344,4 +1344,9 @@ button, input[type="submit"], input[type="reset"] {
|
||||
.login-username {
|
||||
position: relative;
|
||||
top: 0.1rem;
|
||||
}
|
||||
|
||||
.dataTables_filter input {
|
||||
/* Fixes datatables search bar going outside cards */
|
||||
width: calc(100% - 3.7rem) !important;
|
||||
}
|
24
Plan/react/dashboard/src/views/common/Geolocations.js
Normal file
24
Plan/react/dashboard/src/views/common/Geolocations.js
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import {ErrorViewCard} from "../ErrorView";
|
||||
import GeolocationsCard from "../../components/cards/common/GeolocationsCard";
|
||||
import PingTableCard from "../../components/cards/common/PingTableCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
|
||||
const Geolocations = ({className, geolocationData, pingData, geolocationError, pingError}) => {
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className={className}>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
{geolocationError ? <ErrorViewCard error={geolocationError}/> :
|
||||
<GeolocationsCard data={geolocationData}/>}
|
||||
{pingError ? <ErrorViewCard error={pingError}/> : <PingTableCard data={pingData}/>}
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
};
|
||||
|
||||
export default Geolocations
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import Geolocations from "../common/Geolocations";
|
||||
import {fetchNetworkPingTable} from "../../service/networkService";
|
||||
import {fetchGeolocations} from "../../service/serverService";
|
||||
|
||||
const NetworkGeolocations = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchGeolocations, []);
|
||||
const {data: pingData, loadingError: pingLoadingError} = useDataRequest(fetchNetworkPingTable, []);
|
||||
|
||||
return (
|
||||
<Geolocations className={"server_geolocations"}
|
||||
geolocationData={data} geolocationError={loadingError}
|
||||
pingData={pingData} pingError={pingLoadingError}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export default NetworkGeolocations
|
@ -2,11 +2,7 @@ import React from 'react';
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import {fetchGeolocations, fetchPingTable} from "../../service/serverService";
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import {ErrorViewCard} from "../ErrorView";
|
||||
import GeolocationsCard from "../../components/cards/common/GeolocationsCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import PingTableCard from "../../components/cards/common/PingTableCard";
|
||||
import Geolocations from "../common/Geolocations";
|
||||
|
||||
const ServerGeolocations = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -15,17 +11,10 @@ const ServerGeolocations = () => {
|
||||
const {pingData, pingLoadingError} = useDataRequest(fetchPingTable, [identifier]);
|
||||
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className="server_geolocations">
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
{loadingError ? <ErrorViewCard error={loadingError}/> : <GeolocationsCard data={data}/>}
|
||||
{pingLoadingError ? <ErrorViewCard error={pingLoadingError}/> :
|
||||
<PingTableCard data={pingData}/>}
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
</LoadIn>
|
||||
<Geolocations className={"server_geolocations"}
|
||||
geolocationData={data} geolocationError={loadingError}
|
||||
pingData={pingData} pingError={pingLoadingError}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user