From 3c83cf6baa03dfa9757bdb9d6dec42b471a76190 Mon Sep 17 00:00:00 2001 From: Aurora Lahtela <24460436+AuroraLS3@users.noreply.github.com> Date: Sun, 24 Mar 2024 11:12:10 +0200 Subject: [PATCH] Implement most of the requirements for this feature --- .../datatransfer/PlayerJoinAddresses.java | 67 ++++++++++ .../delivery/rendering/json/JSONFactory.java | 20 ++- .../json/PlayerJoinAddressJSONResolver.java | 9 +- .../queries/objects/JoinAddressQueries.java | 32 +++++ .../cards/common/AddressListCard.jsx | 53 ++++++++ .../components/cards/common/JoinAddresses.jsx | 48 +++++++ .../cards/common/PlayerRetentionGraphCard.jsx | 2 +- .../server/graphs/JoinAddressGraphCard.jsx | 70 +++++++++-- .../context/joinAddressListContextHook.jsx | 44 +++++++ .../dashboard/src/service/serverService.js | 14 +-- .../views/network/NetworkJoinAddresses.jsx | 118 ++---------------- .../src/views/server/ServerJoinAddresses.jsx | 28 +---- 12 files changed, 348 insertions(+), 157 deletions(-) create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PlayerJoinAddresses.java create mode 100644 Plan/react/dashboard/src/components/cards/common/AddressListCard.jsx create mode 100644 Plan/react/dashboard/src/components/cards/common/JoinAddresses.jsx create mode 100644 Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PlayerJoinAddresses.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PlayerJoinAddresses.java new file mode 100644 index 000000000..276c90387 --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PlayerJoinAddresses.java @@ -0,0 +1,67 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.delivery.domain.datatransfer; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +/** + * Represents data returned by {@link com.djrapitops.plan.delivery.webserver.resolver.json.PlayerJoinAddressJSONResolver}. + * + * @author AuroraLS3 + */ +public class PlayerJoinAddresses { + + private final List joinAddresses; + private final Map joinAddressByPlayer; + + public PlayerJoinAddresses(List joinAddresses, Map joinAddressByPlayer) { + this.joinAddresses = joinAddresses; + this.joinAddressByPlayer = joinAddressByPlayer; + } + + public List getJoinAddresses() { + return joinAddresses; + } + + public Map getJoinAddressByPlayer() { + return joinAddressByPlayer; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlayerJoinAddresses that = (PlayerJoinAddresses) o; + return Objects.equals(getJoinAddresses(), that.getJoinAddresses()) && Objects.equals(getJoinAddressByPlayer(), that.getJoinAddressByPlayer()); + } + + @Override + public int hashCode() { + return Objects.hash(getJoinAddresses(), getJoinAddressByPlayer()); + } + + @Override + public String toString() { + return "PlayerJoinAddresses{" + + "joinAddresses=" + joinAddresses + + ", joinAddressByPlayer=" + joinAddressByPlayer + + '}'; + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java index c2c2afe39..b86102408 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java @@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.rendering.json; import com.djrapitops.plan.delivery.domain.DateObj; import com.djrapitops.plan.delivery.domain.RetentionData; +import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses; import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto; import com.djrapitops.plan.delivery.domain.mutators.PlayerKillMutator; import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator; @@ -160,14 +161,25 @@ public class JSONFactory { return db.query(PlayerRetentionQueries.fetchRetentionData()); } - public Map playerJoinAddresses(ServerUUID serverUUID) { + public PlayerJoinAddresses playerJoinAddresses(ServerUUID serverUUID, boolean includeByPlayerMap) { Database db = dbSystem.getDatabase(); - return db.query(JoinAddressQueries.latestJoinAddressesOfPlayers(serverUUID)); + if (includeByPlayerMap) { + Map addresses = db.query(JoinAddressQueries.latestJoinAddressesOfPlayers(serverUUID)); + return new PlayerJoinAddresses( + addresses.values().stream().distinct().sorted().collect(Collectors.toList()), + addresses + ); + } else { + return new PlayerJoinAddresses(db.query(JoinAddressQueries.uniqueJoinAddresses(serverUUID)), null); + } } - public Map playerJoinAddresses() { + public PlayerJoinAddresses playerJoinAddresses(boolean includeByPlayerMap) { Database db = dbSystem.getDatabase(); - return db.query(JoinAddressQueries.latestJoinAddressesOfPlayers()); + return new PlayerJoinAddresses( + db.query(JoinAddressQueries.uniqueJoinAddresses()), + includeByPlayerMap ? db.query(JoinAddressQueries.latestJoinAddressesOfPlayers()) : null + ); } public List> serverSessionsAsJSONMap(ServerUUID serverUUID) { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java index 5299d545f..045426158 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/PlayerJoinAddressJSONResolver.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.delivery.webserver.resolver.json; import com.djrapitops.plan.delivery.domain.auth.WebPermission; +import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses; import com.djrapitops.plan.delivery.formatting.Formatter; import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.web.resolver.MimeType; @@ -34,6 +35,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.ws.rs.GET; @@ -42,7 +44,6 @@ import org.jetbrains.annotations.Nullable; import javax.inject.Inject; import javax.inject.Singleton; -import java.util.Collections; import java.util.Optional; /** @@ -79,7 +80,7 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver { @Operation( description = "Get join address information of players for server or network", responses = { - @ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)), + @ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, schema = @Schema(implementation = PlayerJoinAddresses.class))), @ApiResponse(responseCode = "400", description = "If 'server' parameter is not an existing server") }, parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = { @@ -105,12 +106,12 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver { if (request.getQuery().get("server").isPresent()) { ServerUUID serverUUID = identifiers.getServerUUID(request); return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES, serverUUID, - theUUID -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses(theUUID)) + serverUUID1 -> jsonFactory.playerJoinAddresses(serverUUID1, request.getQuery().get("listOnly").isEmpty()) ); } // Assume network return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES, - () -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses()) + () -> jsonFactory.playerJoinAddresses(request.getQuery().get("listOnly").isEmpty()) ); } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java index 33386e85e..a8413b4aa 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/JoinAddressQueries.java @@ -142,6 +142,28 @@ public class JoinAddressQueries { }; } + public static QueryStatement> allJoinAddresses(ServerUUID serverUUID) { + String sql = SELECT + DISTINCT + JoinAddressTable.JOIN_ADDRESS + + FROM + JoinAddressTable.TABLE_NAME + " j" + + INNER_JOIN + SessionsTable.TABLE_NAME + " s ON s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID + + WHERE + SessionsTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID + + ORDER_BY + JoinAddressTable.JOIN_ADDRESS + " ASC"; + + return new QueryStatement<>(sql, 100) { + @Override + public void prepare(PreparedStatement statement) throws SQLException { + statement.setString(1, serverUUID.toString()); + } + + @Override + public List processResults(ResultSet set) throws SQLException { + List joinAddresses = new ArrayList<>(); + while (set.next()) joinAddresses.add(set.getString(JoinAddressTable.JOIN_ADDRESS)); + return joinAddresses; + } + }; + } + public static Query> uniqueJoinAddresses() { return db -> { List addresses = db.query(allJoinAddresses()); @@ -152,6 +174,16 @@ public class JoinAddressQueries { }; } + public static Query> uniqueJoinAddresses(ServerUUID serverUUID) { + return db -> { + List addresses = db.query(allJoinAddresses(serverUUID)); + if (!addresses.contains(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP)) { + addresses.add(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP); + } + return addresses; + }; + } + public static Query> userIdsOfPlayersWithJoinAddresses(@Untrusted List joinAddresses) { String sql = SELECT + DISTINCT + SessionsTable.USER_ID + FROM + JoinAddressTable.TABLE_NAME + " j" + diff --git a/Plan/react/dashboard/src/components/cards/common/AddressListCard.jsx b/Plan/react/dashboard/src/components/cards/common/AddressListCard.jsx new file mode 100644 index 000000000..3e74eabf6 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/AddressListCard.jsx @@ -0,0 +1,53 @@ +import {useTranslation} from "react-i18next"; +import React, {useCallback, useEffect, useState} from "react"; +import {Card, Form} from "react-bootstrap"; +import CardHeader from "../CardHeader.jsx"; +import {faCheck, faList, faPencil} from "@fortawesome/free-solid-svg-icons"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import MultiSelect from "../../input/MultiSelect.jsx"; +import {faTrashAlt} from "@fortawesome/free-regular-svg-icons"; + +const AddressListCard = ({n, group, editGroup, allAddresses, remove}) => { + const {t} = useTranslation(); + const [selectedIndexes, setSelectedIndexes] = useState([]); + const [editingName, setEditingName] = useState(false); + const [name, setName] = useState(group.name); + + const isUpToDate = group.addresses === allAddresses.filter((a, i) => selectedIndexes.includes(i)); + const applySelected = useCallback(() => { + editGroup({...group, addresses: allAddresses.filter((a, i) => selectedIndexes.includes(i))}) + }, [editGroup, group, allAddresses, selectedIndexes]); + const editName = useCallback(newName => { + editGroup({...group, name: newName}); + }, [editGroup, group]); + useEffect(() => { + if (!editingName && name !== group.name) editName(name); + }, [editName, editingName, name]) + + return ( + + setName(e.target.value)}/> : group.name + }> + + + + + + + + + ) +} +export default AddressListCard; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/common/JoinAddresses.jsx b/Plan/react/dashboard/src/components/cards/common/JoinAddresses.jsx new file mode 100644 index 000000000..8e90a1396 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/JoinAddresses.jsx @@ -0,0 +1,48 @@ +import LoadIn from "../../animation/LoadIn.jsx"; +import ExtendableRow from "../../layout/extension/ExtendableRow.jsx"; +import JoinAddressGraphCard from "../server/graphs/JoinAddressGraphCard.jsx"; +import {Col, Row} from "react-bootstrap"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faPlus} from "@fortawesome/free-solid-svg-icons"; +import React from "react"; +import {useAuth} from "../../../hooks/authenticationHook.jsx"; +import {useTranslation} from "react-i18next"; +import {useJoinAddressListContext} from "../../../hooks/context/joinAddressListContextHook.jsx"; +import AddressListCard from "./AddressListCard.jsx"; + +const JoinAddresses = ({id, permission, identifier}) => { + const {t} = useTranslation(); + const {hasPermission} = useAuth(); + const {list, add, remove, replace, allAddresses} = useJoinAddressListContext(); + + const seeTime = hasPermission(permission); + + return ( + +
+ + + {seeTime && } + + + + {list.map((group, i) => + + replace(replacement, i)} + allAddresses={allAddresses} + remove={() => remove(i)}/> + )} + + + + +
+
+ ) +} + +export default JoinAddresses; \ 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 1e0786a01..0eba802b2 100644 --- a/Plan/react/dashboard/src/components/cards/common/PlayerRetentionGraphCard.jsx +++ b/Plan/react/dashboard/src/components/cards/common/PlayerRetentionGraphCard.jsx @@ -209,7 +209,7 @@ const PlayerRetentionGraphCard = ({identifier}) => { useEffect(() => { if (!data || !joinAddressData) return; - createSeries(data.player_retention, joinAddressData.join_address_by_player).then(series => setSeries(series.flat())); + createSeries(data.player_retention, joinAddressData.joinAddressByPlayer).then(series => setSeries(series.flat())); }, [data, joinAddressData, createSeries, setSeries]); useEffect(() => { diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.jsx b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.jsx index d92182740..73fc80255 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.jsx +++ b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.jsx @@ -1,23 +1,73 @@ -import React, {useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {useTranslation} from "react-i18next"; -import {useDataRequest} from "../../../../hooks/dataFetchHook"; import {fetchJoinAddressByDay} from "../../../../service/serverService"; import {ErrorViewCard} from "../../../../views/ErrorView"; -import {CardLoader} from "../../../navigation/Loader"; +import {ChartLoader} from "../../../navigation/Loader"; import {Card} from "react-bootstrap"; import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import {faChartColumn} from "@fortawesome/free-solid-svg-icons"; import JoinAddressGraph from "../../../graphs/JoinAddressGraph"; import Toggle from "../../../input/Toggle"; +import {useJoinAddressListContext} from "../../../../hooks/context/joinAddressListContextHook.jsx"; +import {useNavigation} from "../../../../hooks/navigationHook.jsx"; -const JoinAddressGraphCard = ({id, identifier, addresses}) => { +const JoinAddressGraphCard = ({identifier}) => { const {t} = useTranslation(); const [stack, setStack] = useState(true); + const {updateRequested} = useNavigation(); - const {data, loadingError} = useDataRequest(fetchJoinAddressByDay, [addresses || [], identifier]); + const {list} = useJoinAddressListContext(); + const noSelectedAddresses = !list.filter(group => group.addresses.length).length; + + const [data, setData] = useState(undefined); + const [loadingError, setLoadingError] = useState(undefined); + const loadAddresses = useCallback(async () => { + if (noSelectedAddresses) return; + + let colors = ['#4ab4de']; + const dataByGroup = []; + for (const group of list) { + const {data, error} = await fetchJoinAddressByDay(updateRequested, group.addresses, identifier); + if (error) { + setLoadingError(error); + return; + } + colors = data?.colors; + dataByGroup.push({...group, data: data?.join_addresses_by_date || []}); + } + + // First group points from endpoint into frontend based groups + const points = {}; + for (const group of dataByGroup) { + const groupName = group.name; + for (const point of group.data || []) { + if (!points[point.date]) points[point.date] = []; + + const count = point.joinAddresses.map(j => j.count).reduce((partialSum, a) => partialSum + a, 0); + points[point.date].push({date: point.date, joinAddresses: [{joinAddress: groupName, count}]}) + } + } + + // expected output: [{date: number, addresses: [{joinAddress: "name", count: number}]}] + const flattened = Object.entries(points) + .sort((a, b) => Number(b.date) - Number(a.date)) + .map(([date, pointList]) => { + return { + date: Number(date), joinAddresses: pointList.map(point => point.joinAddresses).flat() + } + }); + + setData({ + join_addresses_by_date: flattened, + colors + }); + }, [setData, setLoadingError, identifier, updateRequested, list]); + + useEffect(() => { + loadAddresses(); + }, [loadAddresses]); if (loadingError) return - if (!data) return ; return ( @@ -27,8 +77,12 @@ const JoinAddressGraphCard = ({id, identifier, addresses}) => { {t('html.label.stacked')} - + {data && + } + {!data && noSelectedAddresses && +

Select some addresses

} + {!data && !noSelectedAddresses && }
) }; diff --git a/Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx b/Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx new file mode 100644 index 000000000..dc9b51b4c --- /dev/null +++ b/Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx @@ -0,0 +1,44 @@ +import {createContext, useCallback, useContext, useEffect, useMemo, useState} from "react"; +import {randomUuid} from "../../util/uuid.js"; +import {fetchPlayerJoinAddresses} from "../../service/serverService.js"; +import {useNavigation} from "../navigationHook.jsx"; + +const JoinAddressListContext = createContext({}); + +export const JoinAddressListContextProvider = ({identifier, children}) => { + const {updateRequested} = useNavigation(); + const [list, setList] = useState([]); + + const add = useCallback(() => { + setList([...list, {name: "Address group " + (list.length + 1), addresses: [], uuid: randomUuid()}]) + }, [list, setList]); + const remove = useCallback(index => { + setList(list.filter((f, i) => i !== index)); + }, [setList, list]); + const replace = useCallback((replacement, index) => { + const newList = [...list]; + newList[index] = replacement; + setList(newList) + }, [setList, list]); + + const [allAddresses, setAllAddresses] = useState([]); + const loadAddresses = useCallback(async () => { + const {data, error} = await fetchPlayerJoinAddresses(updateRequested, identifier, true); + setAllAddresses(data?.joinAddresses || [error]); + }, [setAllAddresses, identifier, updateRequested]); + useEffect(() => { + loadAddresses(); + }, [loadAddresses]); + + const sharedState = useMemo(() => { + return {list, add, remove, replace, allAddresses}; + }, [list, add, remove, replace]); + return ( + {children} + + ) +} + +export const useJoinAddressListContext = () => { + return useContext(JoinAddressListContext); +} \ 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 63e450522..51229f82b 100644 --- a/Plan/react/dashboard/src/service/serverService.js +++ b/Plan/react/dashboard/src/service/serverService.js @@ -321,22 +321,22 @@ const fetchNetworkRetentionData = async (timestamp) => { return doGetRequest(url, timestamp); } -export const fetchPlayerJoinAddresses = async (timestamp, identifier) => { +export const fetchPlayerJoinAddresses = async (timestamp, identifier, justList) => { if (identifier) { - return await fetchServerPlayerJoinAddresses(timestamp, identifier); + return await fetchServerPlayerJoinAddresses(timestamp, identifier, justList); } else { - return await fetchNetworkPlayerJoinAddresses(timestamp); + return await fetchNetworkPlayerJoinAddresses(timestamp, justList); } } -const fetchServerPlayerJoinAddresses = async (timestamp, identifier) => { - let url = `/v1/joinAddresses?server=${identifier}`; +const fetchServerPlayerJoinAddresses = async (timestamp, identifier, justList) => { + let url = `/v1/joinAddresses?server=${identifier}${justList ? "&listOnly=true" : ""}`; if (staticSite) url = `/data/joinAddresses-${identifier}.json`; return doGetRequest(url, timestamp); } -const fetchNetworkPlayerJoinAddresses = async (timestamp) => { - let url = `/v1/joinAddresses`; +const fetchNetworkPlayerJoinAddresses = async (timestamp, justList) => { + let url = `/v1/joinAddresses${justList ? "?listOnly=true" : ""}`; if (staticSite) url = `/data/joinAddresses.json`; return doGetRequest(url, timestamp); } diff --git a/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.jsx b/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.jsx index 9fb4f83b0..586ede325 100644 --- a/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.jsx +++ b/Plan/react/dashboard/src/views/network/NetworkJoinAddresses.jsx @@ -1,117 +1,13 @@ -import React, {useCallback, useEffect, useState} from 'react'; -import {Card, Col, Form, Row} from "react-bootstrap"; -import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard"; -import LoadIn from "../../components/animation/LoadIn"; -import ExtendableRow from "../../components/layout/extension/ExtendableRow"; -import {useAuth} from "../../hooks/authenticationHook"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faCheck, faList, faPencil, faPlus} from "@fortawesome/free-solid-svg-icons"; -import MultiSelect from "../../components/input/MultiSelect.jsx"; -import {useDataRequest} from "../../hooks/dataFetchHook.js"; -import {fetchPlayerJoinAddresses} from "../../service/serverService.js"; -import {useTranslation} from "react-i18next"; -import {faTrashAlt} from "@fortawesome/free-regular-svg-icons"; -import CardHeader from "../../components/cards/CardHeader.jsx"; -import {randomUuid} from "../../util/uuid.js"; - -const AddressListCard = ({n, group, editGroup, allAddresses, remove}) => { - const {t} = useTranslation(); - const [selectedIndexes, setSelectedIndexes] = useState([]); - const [editingName, setEditingName] = useState(false); - const [name, setName] = useState(group.name); - - const isUpToDate = group.addresses === allAddresses.filter((a, i) => selectedIndexes.includes(i)); - const applySelected = useCallback(() => { - editGroup({...group, addresses: allAddresses.filter((a, i) => selectedIndexes.includes(i))}) - }, [editGroup, group, allAddresses, selectedIndexes]); - const editName = useCallback(newName => { - editGroup({...group, name: newName}); - }, [editGroup, group]); - useEffect(() => { - if (!editingName && name !== group.name) editName(name); - }, [editName, editingName, name]) - - return ( - - - setName(e.target.value)}/> : group.name - }> - - - - - - - - - - ) -} +import React from 'react'; +import JoinAddresses from "../../components/cards/common/JoinAddresses.jsx"; +import {JoinAddressListContextProvider} from "../../hooks/context/joinAddressListContextHook.jsx"; const NetworkJoinAddresses = () => { - const identifier = undefined; - const {hasPermission} = useAuth(); - - const seeTime = false && hasPermission('page.network.join.addresses.graphs.time'); - const seeLatest = hasPermission('page.network.join.addresses.graphs.pie'); - - // TODO Move to context - const [list, setList] = useState([]); - const add = useCallback(() => { - setList([...list, {name: "Address group " + (list.length + 1), addresses: [], uuid: randomUuid()}]) - }, [list, setList]); - const remove = useCallback(index => { - setList(list.filter((f, i) => i !== index)); - }, [setList, list]); - const replace = useCallback((replacement, index) => { - const newList = [...list]; - newList[index] = replacement; - setList(newList) - }, [setList, list]); - - const { - data: joinAddressData, - loadingError: joinAddressLoadingError - } = useDataRequest(fetchPlayerJoinAddresses, [identifier]); - - let allAddresses = joinAddressData ? Object.values(joinAddressData.join_address_by_player) : []; - - function onlyUnique(value, index, array) { - return array.indexOf(value) === index; - } - - allAddresses = allAddresses.filter(onlyUnique); return ( - -
- - {seeTime && } - - - {list.map((group, i) => - replace(replacement, i)} - allAddresses={allAddresses} - remove={() => remove(i)}/>)} - - - - -
-
+ + + ) }; diff --git a/Plan/react/dashboard/src/views/server/ServerJoinAddresses.jsx b/Plan/react/dashboard/src/views/server/ServerJoinAddresses.jsx index 6164c3974..316eb30f1 100644 --- a/Plan/react/dashboard/src/views/server/ServerJoinAddresses.jsx +++ b/Plan/react/dashboard/src/views/server/ServerJoinAddresses.jsx @@ -1,31 +1,15 @@ import React from 'react'; -import {Col} from "react-bootstrap"; -import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard"; -import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard"; import {useParams} from "react-router-dom"; -import LoadIn from "../../components/animation/LoadIn"; -import ExtendableRow from "../../components/layout/extension/ExtendableRow"; -import {useAuth} from "../../hooks/authenticationHook"; +import {JoinAddressListContextProvider} from "../../hooks/context/joinAddressListContextHook.jsx"; +import JoinAddresses from "../../components/cards/common/JoinAddresses.jsx"; const ServerJoinAddresses = () => { - const {hasPermission} = useAuth(); const {identifier} = useParams(); - - const seeTime = hasPermission('page.server.join.addresses.graphs.time'); - const seeLatest = hasPermission('page.server.join.addresses.graphs.pie'); return ( - -
- - {seeTime && - - } - {seeLatest && - - } - -
-
+ + + ) };