diff --git a/Plan/react/dashboard/src/components/cards/common/PlayerRetention.jsx b/Plan/react/dashboard/src/components/cards/common/PlayerRetention.jsx
new file mode 100644
index 000000000..7f16b063d
--- /dev/null
+++ b/Plan/react/dashboard/src/components/cards/common/PlayerRetention.jsx
@@ -0,0 +1,29 @@
+import LoadIn from "../../animation/LoadIn.jsx";
+import ExtendableRow from "../../layout/extension/ExtendableRow.jsx";
+import {Col} from "react-bootstrap";
+import PlayerRetentionGraphCard from "./PlayerRetentionGraphCard.jsx";
+import React, {useState} from "react";
+import {JoinAddressListContextProvider} from "../../../hooks/context/joinAddressListContextHook.jsx";
+import AddressGroupSelectorRow from "./AddressGroupSelectorRow.jsx";
+
+const PlayerRetention = ({id, seeRetention, identifier}) => {
+ const [selectedGroupBy, setSelectedGroupBy] = useState('none');
+ return (
+
+ {seeRetention &&
+
+
+
+
+
+
+ {selectedGroupBy === 'joinAddress' && }
+
+ }
+
+ )
+};
+
+export default PlayerRetention
\ No newline at end of file
diff --git a/Plan/react/dashboard/src/components/cards/common/PlayerRetentionGraphCard.jsx b/Plan/react/dashboard/src/components/cards/common/PlayerRetentionGraphCard.jsx
index 94c853df8..002e1f161 100644
--- a/Plan/react/dashboard/src/components/cards/common/PlayerRetentionGraphCard.jsx
+++ b/Plan/react/dashboard/src/components/cards/common/PlayerRetentionGraphCard.jsx
@@ -17,6 +17,7 @@ import {useTheme} from "../../../hooks/themeHook";
import {useNavigation} from "../../../hooks/navigationHook";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faQuestionCircle} from "@fortawesome/free-regular-svg-icons";
+import {useJoinAddressListContext} from "../../../hooks/context/joinAddressListContextHook.jsx";
const dayMs = 24 * 3600000;
const getWeek = (date) => {
@@ -26,7 +27,7 @@ const getWeek = (date) => {
return Math.ceil(dayOfYear / 7)
};
-const PlayerRetentionGraphCard = ({identifier}) => {
+const PlayerRetentionGraphCard = ({identifier, selectedGroupBy, setSelectedGroupBy}) => {
const {t} = useTranslation();
const {nightModeEnabled} = useTheme();
const {setHelpModalTopic} = useNavigation();
@@ -40,6 +41,8 @@ const PlayerRetentionGraphCard = ({identifier}) => {
loadingError: joinAddressLoadingError
} = useDataRequest(fetchPlayerJoinAddresses, [identifier]);
+ const {list, playerAddresses} = useJoinAddressListContext();
+
const [selectedWindow, setSelectedWindow] = useState('days');
const windowOptions = useMemo(() => [
{name: 'hours', displayName: t('html.label.time.hours'), increment: 3600000},
@@ -57,7 +60,7 @@ const PlayerRetentionGraphCard = ({identifier}) => {
{name: 'registered-2y', displayName: t('html.label.retention.inLast730d'), start: time - 2 * 365 * dayMs},
{name: 'registered-ever', displayName: t('html.label.retention.inAnytime'), start: 0},
], [t, time]);
- const [selectedGroupBy, setSelectedGroupBy] = useState('none');
+ // State moved to higher level for join address group selection
const groupByOptions = useMemo(() => [
{name: 'none', displayName: t('html.label.retention.groupByNone')},
{name: 'days', displayName: t('html.label.time.day')},
@@ -165,8 +168,12 @@ const PlayerRetentionGraphCard = ({identifier}) => {
break;
case 'joinAddress':
const joinAddress = joinAddressData[point.playerUUID];
- if (!grouped[joinAddress]) grouped[joinAddress] = [];
- grouped[joinAddress].push(point);
+ const joinAddressGroups = list.filter(g => g.addresses.includes(joinAddress)).map(g => g.name);
+ for (const joinAddressGroup of joinAddressGroups) {
+ if (!grouped[joinAddressGroup]) grouped[joinAddressGroup] = [];
+ grouped[joinAddressGroup].push(point);
+ }
+
break;
case 'none':
default:
@@ -175,7 +182,7 @@ const PlayerRetentionGraphCard = ({identifier}) => {
}
}
return grouped;
- }, [groupByOptions, selectedGroupBy]);
+ }, [groupByOptions, selectedGroupBy, list]);
const createSeries = useCallback(async (retentionData, joinAddressData) => {
@@ -207,10 +214,10 @@ const PlayerRetentionGraphCard = ({identifier}) => {
}, [nightModeEnabled, mapToData, groupOptions, selectedGroup, selectedYAxis, group]);
useEffect(() => {
- if (!data || !joinAddressData) return;
+ if (!data || !playerAddresses) return;
- createSeries(data.player_retention, joinAddressData.joinAddressByPlayer).then(series => setSeries(series.flat()));
- }, [data, joinAddressData, createSeries, setSeries]);
+ createSeries(data.player_retention, playerAddresses).then(series => setSeries(series.flat()));
+ }, [data, playerAddresses, createSeries, setSeries]);
useEffect(() => {
const windowName = windowOptions.find(option => option.name === selectedWindow).displayName;
diff --git a/Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx b/Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx
index 42fa3d26d..626511f5f 100644
--- a/Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx
+++ b/Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx
@@ -7,7 +7,7 @@ import {useTranslation} from "react-i18next";
const JoinAddressListContext = createContext({});
-export const JoinAddressListContextProvider = ({identifier, children}) => {
+export const JoinAddressListContextProvider = ({identifier, children, loadIndividualAddresses}) => {
const {t} = useTranslation();
const {updateRequested} = useNavigation();
const {preferencesLoaded, getKeyedPreference, setSomePreferences} = usePreferences();
@@ -46,17 +46,19 @@ export const JoinAddressListContextProvider = ({identifier, children}) => {
}, [updateList, list]);
const [allAddresses, setAllAddresses] = useState([]);
+ const [playerAddresses, setPlayerAddresses] = useState([]);
const loadAddresses = useCallback(async () => {
- const {data, error} = await fetchPlayerJoinAddresses(updateRequested, identifier, true);
+ const {data, error} = await fetchPlayerJoinAddresses(updateRequested, identifier, !loadIndividualAddresses);
setAllAddresses(data?.joinAddresses || [error]);
+ setPlayerAddresses(data?.joinAddressByPlayer || {});
}, [setAllAddresses, identifier, updateRequested]);
useEffect(() => {
loadAddresses();
}, [loadAddresses]);
const sharedState = useMemo(() => {
- return {list, add, remove, replace, allAddresses};
- }, [list, add, remove, replace, allAddresses]);
+ return {list, add, remove, replace, allAddresses, playerAddresses};
+ }, [list, add, remove, replace, allAddresses, playerAddresses]);
return (
{children}
diff --git a/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.jsx b/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.jsx
index 3a79f379d..f1c6ebbf5 100644
--- a/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.jsx
+++ b/Plan/react/dashboard/src/views/network/NetworkPlayerRetention.jsx
@@ -1,24 +1,12 @@
import React from 'react';
-import ExtendableRow from "../../components/layout/extension/ExtendableRow";
-import {Col} from "react-bootstrap";
-import LoadIn from "../../components/animation/LoadIn";
-import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard";
import {useAuth} from "../../hooks/authenticationHook";
+import PlayerRetention from "../../components/cards/common/PlayerRetention.jsx";
const NetworkPlayerRetention = () => {
const {hasPermission} = useAuth();
-
const seeRetention = hasPermission('page.network.retention');
return (
-
- {seeRetention && }
-
+
)
};
diff --git a/Plan/react/dashboard/src/views/server/ServerPlayerRetention.jsx b/Plan/react/dashboard/src/views/server/ServerPlayerRetention.jsx
index 9ed35ceb6..967980edd 100644
--- a/Plan/react/dashboard/src/views/server/ServerPlayerRetention.jsx
+++ b/Plan/react/dashboard/src/views/server/ServerPlayerRetention.jsx
@@ -1,10 +1,7 @@
import React from 'react';
-import ExtendableRow from "../../components/layout/extension/ExtendableRow";
-import {Col} from "react-bootstrap";
-import LoadIn from "../../components/animation/LoadIn";
-import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard";
import {useParams} from "react-router-dom";
import {useAuth} from "../../hooks/authenticationHook";
+import PlayerRetention from "../../components/cards/common/PlayerRetention.jsx";
const ServerPlayerRetention = () => {
const {hasPermission} = useAuth();
@@ -12,15 +9,7 @@ const ServerPlayerRetention = () => {
const seeRetention = hasPermission('page.server.retention');
return (
-
-
- {seeRetention &&
-
-
-
- }
-
-
+
)
};