mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-09-27 14:02:38 +02:00
Geolocations to React server page
This commit is contained in:
parent
5382049dcd
commit
c7a7b60d91
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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;
|
@ -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
|
@ -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
|
@ -88,4 +88,10 @@ export const fetchWorldPie = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=worldPie&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchGeolocations = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=geolocation&server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
29
Plan/react/dashboard/src/views/layout/ErrorPage.js
Normal file
29
Plan/react/dashboard/src/views/layout/ErrorPage.js
Normal 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
|
@ -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 ? (
|
||||
<>
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
26
Plan/react/dashboard/src/views/server/ServerGeolocations.js
Normal file
26
Plan/react/dashboard/src/views/server/ServerGeolocations.js
Normal 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
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user