Geolocations to React server page

This commit is contained in:
Aurora Lahtela 2022-06-08 18:24:38 +03:00
parent 5382049dcd
commit c7a7b60d91
12 changed files with 210 additions and 56 deletions

View File

@ -13,6 +13,7 @@
"@fullcalendar/bootstrap": "^5.10.1",
"@fullcalendar/daygrid": "^5.10.1",
"@fullcalendar/react": "^5.10.1",
"@highcharts/map-collection": "^2.0.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",

View File

@ -27,6 +27,7 @@ import PlayerbaseOverview from "./views/server/PlayerbaseOverview";
import ServerPlayers from "./views/server/ServerPlayers";
import PlayersPage from "./views/layout/PlayersPage";
import AllPlayers from "./views/players/AllPlayers";
import ServerGeolocations from "./views/server/ServerGeolocations";
const OverviewRedirect = () => {
return (<Navigate to={"overview"} replace={true}/>)
@ -80,7 +81,7 @@ function App() {
<Route path="playerbase" element={<PlayerbaseOverview/>}/>
<Route path="retention" element={<></>}/>
<Route path="players" element={<ServerPlayers/>}/>
<Route path="geolocations" element={<></>}/>
<Route path="geolocations" element={<ServerGeolocations/>}/>
<Route path="performance" element={<></>}/>
<Route path="plugins-overview" element={<></>}/>
</Route>

View File

@ -0,0 +1,42 @@
import {useTranslation} from "react-i18next";
import {Card, Col, 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 GeolocationBarGraph from "../../graphs/GeolocationBarGraph";
import GeolocationWorldMap from "../../graphs/GeolocationWorldMap";
const GeolocationsCard = ({data}) => {
const {t} = useTranslation();
if (!data?.geolocations_enabled) {
return (
<div className="alert alert-warning mb-0" id="geolocation-warning">
<Fa icon={faExclamationTriangle}/>{' '}
{t('html.description.noGeolocations')}
</div>
)
}
return (
<Card>
<Card.Header>
<h6 className="col-black">
<Fa icon={faGlobe} className="col-green"/> {t('html.label.geolocations')}
</h6>
</Card.Header>
<Card.Body className="chart-area" style={{height: "100%"}}>
<Row>
<Col md={3}>
<GeolocationBarGraph series={data.geolocation_bar_series} color={data.colors.bars}/>
</Col>
<Col md={9}>
<GeolocationWorldMap series={data.geolocation_series} colors={data.colors}/>
</Col>
</Row>
</Card.Body>
</Card>
)
}
export default GeolocationsCard;

View File

@ -0,0 +1,46 @@
import React, {useEffect} from 'react';
import {useTranslation} from "react-i18next";
import {useTheme} from "../../hooks/themeHook";
import {withReducedSaturation} from "../../util/colors";
import Highcharts from "highcharts";
const GeolocationBarGraph = ({series, color}) => {
const {t} = useTranslation();
const {nightModeEnabled, graphTheming} = useTheme();
useEffect(() => {
const bars = series.map(bar => bar.value);
const categories = series.map(bar => bar.label);
const geolocationBarSeries = {
color: nightModeEnabled ? withReducedSaturation(color) : color,
name: t('html.label.players'),
data: bars
};
Highcharts.setOptions(graphTheming);
Highcharts.chart("countryBarChart", {
chart: {type: 'bar'},
title: {text: ''},
xAxis: {
categories: categories,
title: {text: ''}
},
yAxis: {
min: 0,
title: {text: t('html.label.players'), align: 'high'},
labels: {overflow: 'justify'}
},
legend: {enabled: false},
plotOptions: {
bar: {
dataLabels: {enabled: true}
}
},
series: [geolocationBarSeries]
})
}, [color, series, graphTheming, nightModeEnabled, t]);
return (<div id="countryBarChart"/>);
};
export default GeolocationBarGraph

View File

@ -0,0 +1,46 @@
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';
const GeolocationWorldMap = ({series, colors}) => {
const {t} = useTranslation();
const {nightModeEnabled, graphTheming} = useTheme();
useEffect(() => {
const mapSeries = {
name: t('html.label.players'),
type: 'map',
mapData: map,
data: series,
joinBy: ['iso-a3', 'code']
};
Highcharts.setOptions(graphTheming);
Highcharts.mapChart('countryWorldMap', {
chart: {
animation: true
},
title: {text: ''},
mapNavigation: {
enabled: true,
enableDoubleClickZoomTo: true
},
colorAxis: {
min: 1,
type: 'logarithmic',
minColor: nightModeEnabled ? withReducedSaturation(colors.low) : colors.low,
maxColor: nightModeEnabled ? withReducedSaturation(colors.high) : colors.high
},
series: [mapSeries]
})
}, [colors, series, graphTheming, nightModeEnabled, t]);
return (<div id="countryWorldMap"/>);
};
export default GeolocationWorldMap

View File

@ -88,4 +88,10 @@ export const fetchWorldPie = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/graph?type=worldPie&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
}
export const fetchGeolocations = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/graph?type=geolocation&server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import {NightModeCss} from "../../hooks/themeHook";
import Sidebar from "../../components/navigation/Sidebar";
import Header from "../../components/navigation/Header";
import ErrorView from "../ErrorView";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
const ErrorPage = ({error}) => {
return (
<>
<NightModeCss/>
<Sidebar items={[]} showBackButton={true}/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={error.title ? error.title : 'Unexpected error occurred'}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<ErrorView error={error}/>
</main>
<aside>
<ColorSelectorModal/>
</aside>
</div>
</div>
</>
)
};
export default ErrorPage

View File

@ -4,7 +4,6 @@ import {Outlet, useOutletContext, useParams} from "react-router-dom";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
import {NightModeCss} from "../../hooks/themeHook";
import {fetchPlayer} from "../../service/playerService";
import ErrorView from "../ErrorView";
import {faCampground, faCubes, faInfoCircle, faNetworkWired} from "@fortawesome/free-solid-svg-icons";
import {useAuth} from "../../hooks/authenticationHook";
import Header from "../../components/navigation/Header";
@ -12,6 +11,7 @@ import {useNavigation} from "../../hooks/navigationHook";
import {useTranslation} from "react-i18next";
import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons";
import {useDataRequest} from "../../hooks/dataFetchHook";
import ErrorPage from "./ErrorPage";
const PlayerPage = () => {
@ -51,23 +51,7 @@ const PlayerPage = () => {
const {hasPermissionOtherThan} = useAuth();
const showBackButton = hasPermissionOtherThan('page.player.self');
if (loadingError) {
return <>
<NightModeCss/>
<Sidebar items={[]} showBackButton={true}/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={loadingError.title ? loadingError.title : 'Unexpected error occurred'}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<ErrorView error={loadingError}/>
</main>
<aside>
<ColorSelectorModal/>
</aside>
</div>
</div>
</>
}
if (loadingError) return <ErrorPage error={loadingError}/>;
return player ? (
<>

View File

@ -6,9 +6,9 @@ import {faSearch} from "@fortawesome/free-solid-svg-icons";
import {NightModeCss} from "../../hooks/themeHook";
import Sidebar from "../../components/navigation/Sidebar";
import Header from "../../components/navigation/Header";
import ErrorView from "../ErrorView";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
import {useMetadata} from "../../hooks/metadataHook";
import ErrorPage from "./ErrorPage";
const PlayersPage = () => {
const {t, i18n} = useTranslation();
@ -33,23 +33,7 @@ const PlayersPage = () => {
// const {authRequired, user} = useAuth();
const showBackButton = true; // TODO
if (error) {
return <>
<NightModeCss/>
<Sidebar items={[]} showBackButton={showBackButton}/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={error.title ? error.title : 'Unexpected error occurred'}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<ErrorView error={error}/>
</main>
<aside>
<ColorSelectorModal/>
</aside>
</div>
</div>
</>
}
if (error) return <ErrorPage error={error}/>;
const displayedServerName = !isProxy && serverName && serverName.startsWith('Server') ? "Plan" : serverName;
return (

View File

@ -19,10 +19,10 @@ import {useAuth} from "../../hooks/authenticationHook";
import {NightModeCss} from "../../hooks/themeHook";
import Sidebar from "../../components/navigation/Sidebar";
import Header from "../../components/navigation/Header";
import ErrorView from "../ErrorView";
import ColorSelectorModal from "../../components/modal/ColorSelectorModal";
import {useMetadata} from "../../hooks/metadataHook";
import {faCalendarCheck} from "@fortawesome/free-regular-svg-icons";
import ErrorPage from "./ErrorPage";
const ServerPage = () => {
const {t, i18n} = useTranslation();
@ -85,23 +85,7 @@ const ServerPage = () => {
const {authRequired, user} = useAuth();
const showBackButton = isProxy && (!authRequired || user.permissions.filter(perm => perm !== 'page.network').length);
if (error) {
return <>
<NightModeCss/>
<Sidebar items={[]} showBackButton={true}/>
<div className="d-flex flex-column" id="content-wrapper">
<Header page={error.title ? error.title : 'Unexpected error occurred'}/>
<div id="content" style={{display: 'flex'}}>
<main className="container-fluid mt-4">
<ErrorView error={error}/>
</main>
<aside>
<ColorSelectorModal/>
</aside>
</div>
</div>
</>
}
if (error) return <ErrorPage error={error}/>;
const displayedServerName = !isProxy && serverName && serverName.startsWith('Server') ? "Plan" : serverName;
return (

View File

@ -0,0 +1,26 @@
import React from 'react';
import {useParams} from "react-router-dom";
import {useDataRequest} from "../../hooks/dataFetchHook";
import {fetchGeolocations} from "../../service/serverService";
import {Col, Row} from "react-bootstrap-v5";
import ErrorView from "../ErrorView";
import GeolocationsCard from "../../components/cards/common/GeolocationsCard";
const ServerGeolocations = () => {
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(fetchGeolocations, [identifier]);
if (!data) return <></>;
if (loadingError) return <ErrorView error={loadingError}/>
return (
<Row>
<Col md={12}>
<GeolocationsCard data={data}/>
</Col>
</Row>
)
};
export default ServerGeolocations

View File

@ -1224,6 +1224,11 @@
"@fullcalendar/common" "~5.11.0"
tslib "^2.1.0"
"@highcharts/map-collection@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@highcharts/map-collection/-/map-collection-2.0.1.tgz#c5eac79b9711c75248d4219c53be9b6a07704c9d"
integrity sha512-eC5sEMC8LoCVMCz4BKuREcS3wVBBSFaRClJ7/vVZxd4nJe0VEykSFfQJwh8lpY4oJDy46iYrFzy3fwsTo9hmqg==
"@humanwhocodes/config-array@^0.9.2":
version "0.9.5"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"