diff --git a/Plan/react/dashboard/package.json b/Plan/react/dashboard/package.json index e9f6cf121..1afb023f8 100644 --- a/Plan/react/dashboard/package.json +++ b/Plan/react/dashboard/package.json @@ -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", diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index 167ff040a..fdea7e33f 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -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 () @@ -80,7 +81,7 @@ function App() { }/> }/> }/> - }/> + }/> }/> }/> diff --git a/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js b/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js new file mode 100644 index 000000000..2bd01541a --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/GeolocationsCard.js @@ -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 ( +
+ {' '} + {t('html.description.noGeolocations')} +
+ ) + } + + return ( + + +
+ {t('html.label.geolocations')} +
+
+ + + + + + + + + + +
+ ) +} + +export default GeolocationsCard; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/graphs/GeolocationBarGraph.js b/Plan/react/dashboard/src/components/graphs/GeolocationBarGraph.js new file mode 100644 index 000000000..8f76ccea1 --- /dev/null +++ b/Plan/react/dashboard/src/components/graphs/GeolocationBarGraph.js @@ -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 (
); +}; + +export default GeolocationBarGraph \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/graphs/GeolocationWorldMap.js b/Plan/react/dashboard/src/components/graphs/GeolocationWorldMap.js new file mode 100644 index 000000000..794be48b9 --- /dev/null +++ b/Plan/react/dashboard/src/components/graphs/GeolocationWorldMap.js @@ -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 (
); +}; + +export default GeolocationWorldMap \ No newline at end of file diff --git a/Plan/react/dashboard/src/service/serverService.js b/Plan/react/dashboard/src/service/serverService.js index 65540139b..955822362 100644 --- a/Plan/react/dashboard/src/service/serverService.js +++ b/Plan/react/dashboard/src/service/serverService.js @@ -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); -} \ No newline at end of file +} + +export const fetchGeolocations = async (identifier) => { + const timestamp = Date.now(); + const url = `/v1/graph?type=geolocation&server=${identifier}×tamp=${timestamp}`; + return doGetRequest(url); +} diff --git a/Plan/react/dashboard/src/views/layout/ErrorPage.js b/Plan/react/dashboard/src/views/layout/ErrorPage.js new file mode 100644 index 000000000..7bb606c26 --- /dev/null +++ b/Plan/react/dashboard/src/views/layout/ErrorPage.js @@ -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 ( + <> + + +
+
+
+
+ +
+ +
+
+ + ) +}; + +export default ErrorPage \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/layout/PlayerPage.js b/Plan/react/dashboard/src/views/layout/PlayerPage.js index 33970e092..5c9fd86b5 100644 --- a/Plan/react/dashboard/src/views/layout/PlayerPage.js +++ b/Plan/react/dashboard/src/views/layout/PlayerPage.js @@ -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 <> - - -
-
-
-
- -
- -
-
- - } + if (loadingError) return ; return player ? ( <> diff --git a/Plan/react/dashboard/src/views/layout/PlayersPage.js b/Plan/react/dashboard/src/views/layout/PlayersPage.js index 8c8530ab4..0ae166345 100644 --- a/Plan/react/dashboard/src/views/layout/PlayersPage.js +++ b/Plan/react/dashboard/src/views/layout/PlayersPage.js @@ -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 <> - - -
-
-
-
- -
- -
-
- - } + if (error) return ; const displayedServerName = !isProxy && serverName && serverName.startsWith('Server') ? "Plan" : serverName; return ( diff --git a/Plan/react/dashboard/src/views/layout/ServerPage.js b/Plan/react/dashboard/src/views/layout/ServerPage.js index 7880ddd7e..6d412050e 100644 --- a/Plan/react/dashboard/src/views/layout/ServerPage.js +++ b/Plan/react/dashboard/src/views/layout/ServerPage.js @@ -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 <> - - -
-
-
-
- -
- -
-
- - } + if (error) return ; const displayedServerName = !isProxy && serverName && serverName.startsWith('Server') ? "Plan" : serverName; return ( diff --git a/Plan/react/dashboard/src/views/server/ServerGeolocations.js b/Plan/react/dashboard/src/views/server/ServerGeolocations.js new file mode 100644 index 000000000..e9a9b88a7 --- /dev/null +++ b/Plan/react/dashboard/src/views/server/ServerGeolocations.js @@ -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 + + return ( + + + + + + ) +}; + +export default ServerGeolocations \ No newline at end of file diff --git a/Plan/react/dashboard/yarn.lock b/Plan/react/dashboard/yarn.lock index 0715024c5..507f3aaf6 100644 --- a/Plan/react/dashboard/yarn.lock +++ b/Plan/react/dashboard/yarn.lock @@ -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"