From f40e1498c16c9b24c70d6252a7a63842c7feae27 Mon Sep 17 00:00:00 2001 From: Aurora Lahtela <24460436+AuroraLS3@users.noreply.github.com> Date: Sun, 7 Apr 2024 11:13:18 +0300 Subject: [PATCH] 3268/redesign join address visualization (#3558) - Join address pie removed - Join address group mechanism added - User can select multiple addresses for each group - User can rename each group to their liking - The groups are stored in preferences so that user doesn't need to add them back every time - Use the join address group mechanism for time series of Join Addresses - Use the join address group mechanism for Player Retention - Small improvement to retention graph: Show multiple labels - Small improvement to site clock: Can now hover to show actual date Affected issues: - Close #3268 --- .../plan/delivery/domain/DateObj.java | 23 ++++++ .../delivery/domain/auth/WebPermission.java | 28 ++++++- .../datatransfer/PlayerJoinAddresses.java | 67 ++++++++++++++++ .../delivery/export/NetworkPageExporter.java | 1 - .../delivery/export/ServerPageExporter.java | 1 - .../delivery/rendering/json/JSONFactory.java | 20 ++++- .../json/graphs/GraphJSONCreator.java | 39 ++------- .../json/graphs/pie/JoinAddressPie.java | 38 --------- .../json/graphs/pie/PieGraphFactory.java | 4 - .../plan/delivery/webserver/cache/DataID.java | 17 +++- .../delivery/webserver/cache/JSONStorage.java | 4 + .../resolver/json/GraphsJSONResolver.java | 79 +++++++++++-------- .../json/PlayerJoinAddressJSONResolver.java | 26 ++++-- .../webgroup/WebPermissionJSONResolver.java | 7 +- .../plan/settings/locale/LocaleSystem.java | 2 +- .../plan/settings/locale/lang/HtmlLang.java | 2 + .../queries/objects/JoinAddressQueries.java | 79 ++++++++++++++++--- .../patches/UpdateWebPermissionsPatch.java | 2 +- .../assets/plan/locale/locale_CN.yml | 4 +- .../assets/plan/locale/locale_CS.yml | 4 +- .../assets/plan/locale/locale_DE.yml | 4 +- .../assets/plan/locale/locale_EN.yml | 4 +- .../assets/plan/locale/locale_ES.yml | 4 +- .../assets/plan/locale/locale_FI.yml | 4 +- .../assets/plan/locale/locale_FR.yml | 11 +-- .../assets/plan/locale/locale_IT.yml | 4 +- .../assets/plan/locale/locale_JA.yml | 4 +- .../assets/plan/locale/locale_KO.yml | 4 +- .../assets/plan/locale/locale_NL.yml | 4 +- .../assets/plan/locale/locale_PT_BR.yml | 4 +- .../assets/plan/locale/locale_RU.yml | 4 +- .../assets/plan/locale/locale_TR.yml | 4 +- .../assets/plan/locale/locale_UK.yml | 4 +- .../assets/plan/locale/locale_ZH_TW.yml | 4 +- .../domain/auth/WebPermissionTest.java | 56 +++++++++++++ .../delivery/webserver/AccessControlTest.java | 5 +- .../AccessControlVisibilityTest.java | 6 +- .../events/PlayerLeaveEventConsumerTest.java | 8 -- .../queries/JoinAddressQueriesTest.java | 62 ++++++++++++--- .../cards/common/AddressGroupCard.jsx | 66 ++++++++++++++++ .../cards/common/AddressGroupSelectorRow.jsx | 32 ++++++++ .../components/cards/common/JoinAddresses.jsx | 27 +++++++ .../cards/common/PlayerRetention.jsx | 30 +++++++ .../cards/common/PlayerRetentionGraphCard.jsx | 28 ++++--- .../server/graphs/JoinAddressGraphCard.jsx | 76 ++++++++++++++++-- .../server/graphs/JoinAddressGroupCard.jsx | 32 -------- .../src/components/input/MultiSelect.jsx | 4 +- .../src/components/navigation/Header.jsx | 2 +- .../src/components/text/FormattedDate.jsx | 8 +- .../context/joinAddressListContextHook.jsx | 71 +++++++++++++++++ .../dashboard/src/hooks/preferencesHook.jsx | 15 +++- .../dashboard/src/service/serverService.js | 48 ++++------- Plan/react/dashboard/src/util/uuid.js | 6 ++ .../views/network/NetworkJoinAddresses.jsx | 23 +----- .../views/network/NetworkPlayerRetention.jsx | 16 +--- .../src/views/server/ServerJoinAddresses.jsx | 25 +----- .../views/server/ServerPlayerRetention.jsx | 15 +--- 57 files changed, 814 insertions(+), 357 deletions(-) create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/datatransfer/PlayerJoinAddresses.java delete mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/JoinAddressPie.java create mode 100644 Plan/common/src/test/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionTest.java create mode 100644 Plan/react/dashboard/src/components/cards/common/AddressGroupCard.jsx create mode 100644 Plan/react/dashboard/src/components/cards/common/AddressGroupSelectorRow.jsx create mode 100644 Plan/react/dashboard/src/components/cards/common/JoinAddresses.jsx create mode 100644 Plan/react/dashboard/src/components/cards/common/PlayerRetention.jsx delete mode 100644 Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.jsx create mode 100644 Plan/react/dashboard/src/hooks/context/joinAddressListContextHook.jsx create mode 100644 Plan/react/dashboard/src/util/uuid.js diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/DateObj.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/DateObj.java index 868055fd6b..4ce0fca418 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/DateObj.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/DateObj.java @@ -16,6 +16,8 @@ */ package com.djrapitops.plan.delivery.domain; +import java.util.Objects; + /** * Object that has a value tied to a date. * @@ -39,4 +41,25 @@ public long getDate() { public T getValue() { return value; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DateObj dateObj = (DateObj) o; + return getDate() == dateObj.getDate() && Objects.equals(getValue(), dateObj.getValue()); + } + + @Override + public int hashCode() { + return Objects.hash(getDate(), getValue()); + } + + @Override + public String toString() { + return "DateObj{" + + "date=" + date + + ", value=" + value + + '}'; + } } \ No newline at end of file diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java index bfb9559603..5aa3502fa4 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/auth/WebPermission.java @@ -19,6 +19,9 @@ import com.djrapitops.plan.settings.locale.lang.Lang; import org.apache.commons.lang3.StringUtils; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Predicate; import java.util.function.Supplier; /** @@ -47,7 +50,8 @@ public enum WebPermission implements Supplier, Lang { PAGE_NETWORK_SESSIONS_LIST("See list of sessions"), PAGE_NETWORK_JOIN_ADDRESSES("See Join Addresses -tab"), PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS("See Join Address graphs"), - PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph"), + @Deprecated + PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph", true), PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME("See Join Addresses over time graph"), PAGE_NETWORK_RETENTION("See Player Retention -tab"), PAGE_NETWORK_GEOLOCATIONS("See Geolocations tab"), @@ -82,7 +86,8 @@ public enum WebPermission implements Supplier, Lang { PAGE_SERVER_SESSIONS_LIST("See list of sessions"), PAGE_SERVER_JOIN_ADDRESSES("See Join Addresses -tab"), PAGE_SERVER_JOIN_ADDRESSES_GRAPHS("See Join Address graphs"), - PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph"), + @Deprecated + PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE("See Latest Join Addresses graph", true), PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME("See Join Addresses over time graph"), PAGE_SERVER_RETENTION("See Player Retention -tab"), PAGE_SERVER_GEOLOCATIONS("See Geolocations tab"), @@ -156,4 +161,23 @@ public String getKey() { public String getDefault() { return description; } + + public static WebPermission[] nonDeprecatedValues() { + return Arrays.stream(values()) + .filter(Predicate.not(WebPermission::isDeprecated)) + .toArray(WebPermission[]::new); + } + + public static Optional findByPermission(String permission) { + String name = StringUtils.upperCase(permission).replace('.', '_'); + try { + return Optional.of(valueOf(name)); + } catch (IllegalArgumentException noSuchEnum) { + return Optional.empty(); + } + } + + public static boolean isDeprecated(String permission) { + return findByPermission(permission).map(WebPermission::isDeprecated).orElse(false); + } } 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 0000000000..276c903879 --- /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/export/NetworkPageExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/NetworkPageExporter.java index bc436c1b53..7625c76d74 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/NetworkPageExporter.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/NetworkPageExporter.java @@ -121,7 +121,6 @@ public void exportJSON(ExportPaths exportPaths, Path toDirectory, Server server) "graph?type=uniqueAndNew", "graph?type=hourlyUniqueAndNew", "graph?type=serverPie", - "graph?type=joinAddressPie", "graph?type=joinAddressByDay", "graph?type=activity", "graph?type=geolocation", diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java index 08fa0aa430..fcc4cbd45c 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/export/ServerPageExporter.java @@ -137,7 +137,6 @@ public void exportJSON(Path toDirectory, Server server) throws IOException { "graph?type=geolocation&server=" + serverUUID, "graph?type=uniqueAndNew&server=" + serverUUID, "graph?type=hourlyUniqueAndNew&server=" + serverUUID, - "graph?type=joinAddressPie&server=" + serverUUID, "graph?type=joinAddressByDay&server=" + serverUUID, "graph?type=serverCalendar&server=" + serverUUID, "graph?type=punchCard&server=" + serverUUID, 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 c2c2afe39d..b861024087 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 @@ 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 List networkPlayerRetentionAsJSONMap() { 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/rendering/json/graphs/GraphJSONCreator.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java index 57c8ee8b62..f2007ae8e2 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/GraphJSONCreator.java @@ -32,7 +32,6 @@ import com.djrapitops.plan.delivery.rendering.json.graphs.line.PingGraph; import com.djrapitops.plan.delivery.rendering.json.graphs.line.Point; import com.djrapitops.plan.delivery.rendering.json.graphs.pie.Pie; -import com.djrapitops.plan.delivery.rendering.json.graphs.pie.PieSlice; import com.djrapitops.plan.delivery.rendering.json.graphs.pie.WorldPie; import com.djrapitops.plan.delivery.rendering.json.graphs.special.WorldMap; import com.djrapitops.plan.delivery.rendering.json.graphs.stack.StackGraph; @@ -57,7 +56,7 @@ import com.djrapitops.plan.storage.database.queries.objects.*; import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable; import com.djrapitops.plan.utilities.comparators.DateHolderOldestComparator; -import com.djrapitops.plan.utilities.comparators.PieSliceComparator; +import com.djrapitops.plan.utilities.dev.Untrusted; import com.djrapitops.plan.utilities.java.Lists; import com.djrapitops.plan.utilities.java.Maps; import net.playeranalytics.plugin.scheduling.TimeAmount; @@ -457,34 +456,6 @@ public Map serverPreferencePieJSONAsMap() { .build(); } - public Map playerHostnamePieJSONAsMap() { - String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); - Map joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.latestJoinAddresses()); - - translateUnknown(joinAddresses); - - List slices = graphs.pie().joinAddressPie(joinAddresses).getSlices(); - slices.sort(new PieSliceComparator()); - return Maps.builder(String.class, Object.class) - .put("colors", pieColors) - .put("slices", slices) - .build(); - } - - public Map playerHostnamePieJSONAsMap(ServerUUID serverUUID) { - String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); - Map joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.latestJoinAddresses(serverUUID)); - - translateUnknown(joinAddresses); - - List slices = graphs.pie().joinAddressPie(joinAddresses).getSlices(); - slices.sort(new PieSliceComparator()); - return Maps.builder(String.class, Object.class) - .put("colors", pieColors) - .put("slices", slices) - .build(); - } - public void translateUnknown(Map joinAddresses) { Integer unknown = joinAddresses.get(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP); if (unknown != null) { @@ -493,16 +464,16 @@ public void translateUnknown(Map joinAddresses) { } } - public Map joinAddressesByDay(ServerUUID serverUUID, long after, long before) { + public Map joinAddressesByDay(ServerUUID serverUUID, long after, long before, @Untrusted List addressFilter) { String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); - List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before)); + List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter)); return mapToJson(pieColors, joinAddresses); } - public Map joinAddressesByDay(long after, long before) { + public Map joinAddressesByDay(long after, long before, @Untrusted List addressFilter) { String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE); - List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before)); + List>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter)); return mapToJson(pieColors, joinAddresses); } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/JoinAddressPie.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/JoinAddressPie.java deleted file mode 100644 index 1653be16e6..0000000000 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/JoinAddressPie.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.rendering.json.graphs.pie; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class JoinAddressPie extends Pie { - - JoinAddressPie(Map joinAddresses) { - super(turnToSlices(joinAddresses)); - } - - private static List turnToSlices(Map joinAddresses) { - List slices = new ArrayList<>(); - for (Map.Entry address : joinAddresses.entrySet()) { - String joinAddress = address.getKey(); - Integer total = address.getValue(); - slices.add(new PieSlice(joinAddress, total)); - } - return slices; - } -} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java index 21ba98eb8c..4dd589dece 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/graphs/pie/PieGraphFactory.java @@ -68,10 +68,6 @@ public Pie serverPreferencePie(Map playtimeByServerName) { return new ServerPreferencePie(playtimeByServerName); } - public Pie joinAddressPie(Map joinAddresses) { - return new JoinAddressPie(joinAddresses); - } - public WorldPie worldPie(WorldTimes worldTimes) { WorldAliasSettings worldAliasSettings = config.getWorldAliasSettings(); Map playtimePerAlias = worldAliasSettings.getPlaytimePerAlias(worldTimes); diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/DataID.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/DataID.java index 2a852d44aa..05e125a2bc 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/DataID.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/DataID.java @@ -42,7 +42,6 @@ public enum DataID { GRAPH_ACTIVITY, GRAPH_PING, GRAPH_SERVER_PIE, - GRAPH_HOSTNAME_PIE, GRAPH_PUNCHCARD, SERVER_OVERVIEW, ONLINE_OVERVIEW, @@ -54,12 +53,26 @@ public enum DataID { EXTENSION_TABS, EXTENSION_JSON, LIST_SERVERS, - JOIN_ADDRESSES_BY_DAY, + JOIN_ADDRESSES_BY_DAY(false), PLAYER_RETENTION, PLAYER_JOIN_ADDRESSES, PLAYER_ALLOWLIST_BOUNCES, ; + private final boolean cacheable; + + DataID() { + this(true); + } + + DataID(boolean cacheable) { + this.cacheable = cacheable; + } + + public boolean isCacheable() { + return cacheable; + } + public String of(ServerUUID serverUUID) { if (serverUUID == null) return name(); return name() + "_" + serverUUID; diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/JSONStorage.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/JSONStorage.java index 7b9dddb399..19766a8889 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/JSONStorage.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/cache/JSONStorage.java @@ -74,6 +74,10 @@ public StoredJSON(String json, long timestamp) { this.timestamp = timestamp; } + public static StoredJSON fromObject(Object json, long timestamp) { + return new StoredJSON(new Gson().toJson(json), timestamp); + } + public String getJson() { return json; } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java index cd8c7db7de..e1236bc7bb 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/GraphsJSONResolver.java @@ -39,11 +39,16 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import org.apache.commons.lang3.StringUtils; import javax.inject.Inject; import javax.inject.Singleton; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; /** * Resolves /v1/graph JSON requests. @@ -117,7 +122,6 @@ public boolean canAccess(Request request) { @ExampleObject("aggregatedPing"), @ExampleObject("punchCard"), @ExampleObject("serverPie"), - @ExampleObject("joinAddressPie"), @ExampleObject("joinAddressByDay"), }), @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for", examples = { @@ -156,15 +160,22 @@ private JSONStorage.StoredJSON getGraphJSON(@Untrusted Request request, DataID d JSONStorage.StoredJSON storedJSON; if (request.getQuery().get("server").isPresent()) { ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException - storedJSON = jsonResolverService.resolve( - timestamp, dataID, serverUUID, - theServerUUID -> generateGraphDataJSONOfType(dataID, theServerUUID, request.getQuery()) - ); + Function generationFunction = theServerUUID -> generateGraphDataJSONOfType(dataID, theServerUUID, request.getQuery()); + if (dataID.isCacheable()) { + storedJSON = jsonResolverService.resolve(timestamp, dataID, serverUUID, generationFunction); + } else { + storedJSON = JSONStorage.StoredJSON.fromObject(generationFunction.apply(serverUUID), System.currentTimeMillis()); + } } else { // Assume network - storedJSON = jsonResolverService.resolve( - timestamp, dataID, () -> generateGraphDataJSONOfType(dataID, request.getQuery()) - ); + Supplier generationFunction = () -> generateGraphDataJSONOfType(dataID, request.getQuery()); + if (dataID.isCacheable()) { + storedJSON = jsonResolverService.resolve( + timestamp, dataID, generationFunction + ); + } else { + storedJSON = JSONStorage.StoredJSON.fromObject(generationFunction.get(), System.currentTimeMillis()); + } } return storedJSON; } @@ -197,8 +208,6 @@ private DataID getDataID(@Untrusted String type) { return DataID.GRAPH_PUNCHCARD; case "serverPie": return DataID.GRAPH_SERVER_PIE; - case "joinAddressPie": - return DataID.GRAPH_HOSTNAME_PIE; case "joinAddressByDay": return DataID.JOIN_ADDRESSES_BY_DAY; default: @@ -229,8 +238,6 @@ private List getRequiredPermission(DataID dataID) { return List.of(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS); case GRAPH_WORLD_MAP: return List.of(WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP); - case GRAPH_HOSTNAME_PIE: - return List.of(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE); case JOIN_ADDRESSES_BY_DAY: return List.of(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME); default: @@ -258,8 +265,6 @@ private List getRequiredNetworkPermission(DataID dataID) { return List.of(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP); case GRAPH_ONLINE_PROXIES: return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE); - case GRAPH_HOSTNAME_PIE: - return List.of(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE); case JOIN_ADDRESSES_BY_DAY: return List.of(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME); default: @@ -283,8 +288,6 @@ private Object generateGraphDataJSONOfType(DataID id, ServerUUID serverUUID, @Un return graphJSON.serverCalendarJSON(serverUUID); case GRAPH_WORLD_PIE: return graphJSON.serverWorldPieJSONAsMap(serverUUID); - case GRAPH_HOSTNAME_PIE: - return graphJSON.playerHostnamePieJSONAsMap(serverUUID); case GRAPH_ACTIVITY: return graphJSON.activityGraphsJSONAsMap(serverUUID); case GRAPH_WORLD_MAP: @@ -294,19 +297,24 @@ private Object generateGraphDataJSONOfType(DataID id, ServerUUID serverUUID, @Un case GRAPH_PUNCHCARD: return graphJSON.punchCardJSONAsMap(serverUUID); case JOIN_ADDRESSES_BY_DAY: - try { - return graphJSON.joinAddressesByDay(serverUUID, - query.get("after").map(Long::parseLong).orElse(0L), - query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis()) - ); - } catch (@Untrusted NumberFormatException e) { - throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)"); - } + return joinAddressGraph(serverUUID, query); default: throw new BadRequestException("Graph type not supported with server-parameter (" + id.name() + ")"); } } + private Map joinAddressGraph(ServerUUID serverUUID, @Untrusted URIQuery query) { + try { + Long after = query.get("after").map(Long::parseLong).orElse(0L); + Long before = query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis()); + @Untrusted List addressFilter = query.get("addresses").map(s -> StringUtils.split(s, ',')) + .map(Arrays::asList).orElse(List.of()); + return graphJSON.joinAddressesByDay(serverUUID, after, before, addressFilter); + } catch (@Untrusted NumberFormatException e) { + throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)"); + } + } + private Object generateGraphDataJSONOfType(DataID id, @Untrusted URIQuery query) { switch (id) { case GRAPH_ACTIVITY: @@ -319,23 +327,26 @@ private Object generateGraphDataJSONOfType(DataID id, @Untrusted URIQuery query) return graphJSON.networkCalendarJSON(); case GRAPH_SERVER_PIE: return graphJSON.serverPreferencePieJSONAsMap(); - case GRAPH_HOSTNAME_PIE: - return graphJSON.playerHostnamePieJSONAsMap(); case GRAPH_WORLD_MAP: return graphJSON.geolocationGraphsJSONAsMap(); case GRAPH_ONLINE_PROXIES: return graphJSON.proxyPlayersOnlineGraphs(); case JOIN_ADDRESSES_BY_DAY: - try { - return graphJSON.joinAddressesByDay( - query.get("after").map(Long::parseLong).orElse(0L), - query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis()) - ); - } catch (@Untrusted NumberFormatException e) { - throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)"); - } + return joinAddressGraph(query); default: throw new BadRequestException("Graph type not supported without server-parameter (" + id.name() + ")"); } } + + private Map joinAddressGraph(URIQuery query) { + try { + Long after = query.get("after").map(Long::parseLong).orElse(0L); + Long before = query.get("before").map(Long::parseLong).orElse(System.currentTimeMillis()); + @Untrusted List addressFilter = query.get("addresses").map(s -> StringUtils.split(s, ',')) + .map(Arrays::asList).orElse(List.of()); + return graphJSON.joinAddressesByDay(after, before, addressFilter); + } catch (@Untrusted NumberFormatException e) { + throw new BadRequestException("'after' or 'before' is not a epoch millisecond (number)"); + } + } } \ No newline at end of file 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 5299d545f2..86807d8d28 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,11 +17,13 @@ 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; import com.djrapitops.plan.delivery.web.resolver.Response; import com.djrapitops.plan.delivery.web.resolver.request.Request; +import com.djrapitops.plan.delivery.web.resolver.request.URIQuery; import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService; import com.djrapitops.plan.delivery.webserver.cache.DataID; @@ -34,6 +36,7 @@ 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 +45,6 @@ import javax.inject.Inject; import javax.inject.Singleton; -import java.util.Collections; import java.util.Optional; /** @@ -69,17 +71,27 @@ public PlayerJoinAddressJSONResolver(Identifiers identifiers, AsyncJSONResolverS @Override public boolean canAccess(@Untrusted Request request) { WebUser user = request.getUser().orElse(new WebUser("")); - if (request.getQuery().get("server").isPresent()) { - return user.hasPermission(WebPermission.PAGE_SERVER_RETENTION); + @Untrusted URIQuery query = request.getQuery(); + Optional listOnly = query.get("listOnly"); + if (query.get("server").isPresent()) { + if (listOnly.isEmpty()) { + return user.hasPermission(WebPermission.PAGE_SERVER_RETENTION); + } else { + return user.hasPermission(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME); + } + } + if (listOnly.isEmpty()) { + return user.hasPermission(WebPermission.PAGE_NETWORK_RETENTION); + } else { + return user.hasPermission(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME); } - return user.hasPermission(WebPermission.PAGE_NETWORK_RETENTION); } @GET @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 +117,12 @@ private JSONStorage.StoredJSON getStoredJSON(Request request) { 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/delivery/webserver/resolver/json/webgroup/WebPermissionJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebPermissionJSONResolver.java index f2b20d7964..dd46a295ec 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebPermissionJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/webgroup/WebPermissionJSONResolver.java @@ -37,6 +37,8 @@ import javax.inject.Singleton; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; /** * Endpoint for getting list of available Plan web permissions. @@ -75,7 +77,10 @@ public Optional resolve(Request request) { } private Response getResponse() { - List permissions = dbSystem.getDatabase().query(WebUserQueries.fetchAvailablePermissions()); + List permissions = dbSystem.getDatabase().query(WebUserQueries.fetchAvailablePermissions()) + .stream() + .filter(Predicate.not(WebPermission::isDeprecated)) + .collect(Collectors.toList()); WebPermissionList permissionList = new WebPermissionList(permissions); return Response.builder() diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java index ffa7991f2e..47b002ccec 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/LocaleSystem.java @@ -108,7 +108,7 @@ private static Lang[][] getValuesArray() { HtmlLang.values(), JSLang.values(), PluginLang.values(), - WebPermission.values(), + WebPermission.nonDeprecatedValues(), }; } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java index 00b46125f7..92c82c317e 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java @@ -219,6 +219,8 @@ public enum HtmlLang implements Lang { LABEL_TITLE_SERVER_CALENDAR("html.label.serverCalendar", "Server Calendar"), LABEL_TITLE_NETWORK_CALENDAR("html.label.networkCalendar", "Network Calendar"), LABEL_LABEL_JOIN_ADDRESS("html.label.joinAddress", "Join Address"), + LABEL_ADD_JOIN_ADDRESS_GROUP("html.label.addJoinAddressGroup", "Add address group"), + LABEL_ADDRESS_GROUP("html.label.addressGroup", "Address group {{n}}"), LABEL_LABEL_SESSION_MEDIAN("html.label.medianSessionLength", "Median Session Length"), LABEL_LABEL_KDR("html.label.kdr", "KDR"), LABEL_TITLE_INSIGHTS("html.label.insights", "Insights"), 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 a326297780..a98f93bcb2 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 @@ -28,6 +28,8 @@ import com.djrapitops.plan.storage.database.sql.tables.SessionsTable; import com.djrapitops.plan.storage.database.sql.tables.UsersTable; import com.djrapitops.plan.utilities.dev.Untrusted; +import org.apache.commons.text.TextStringBuilder; +import org.jetbrains.annotations.VisibleForTesting; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -43,6 +45,7 @@ private JoinAddressQueries() { /* Static method class */ } + @VisibleForTesting public static Query> latestJoinAddresses() { String selectLatestJoinAddresses = SELECT + "COUNT(1) as total," + @@ -66,6 +69,7 @@ private static void extractJoinAddress(ResultSet set, Map joinAddr joinAddresses.put(UUID.fromString(set.getString(UsersTable.USER_UUID)), set.getString(JoinAddressTable.JOIN_ADDRESS)); } + @VisibleForTesting public static Query> latestJoinAddresses(ServerUUID serverUUID) { String selectLatestSessionStarts = SELECT + SessionsTable.USER_ID + ",MAX(" + SessionsTable.SESSION_START + ") as max_start" + FROM + SessionsTable.TABLE_NAME + " max_s" + @@ -141,6 +145,28 @@ public List processResults(ResultSet set) throws SQLException { }; } + 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()); @@ -151,6 +177,16 @@ public static Query> uniqueJoinAddresses() { }; } + 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" + @@ -162,21 +198,27 @@ public static Query> userIdsOfPlayersWithJoinAddresses(@Untrusted L return db -> db.querySet(sql, RowExtractors.getInt(SessionsTable.USER_ID), joinAddresses.toArray()); } - public static Query>>> joinAddressesPerDay(ServerUUID serverUUID, long timezoneOffset, long after, long before) { + public static Query>>> joinAddressesPerDay(ServerUUID serverUUID, long timezoneOffset, long after, long before, @Untrusted List addressFilter) { return db -> { Sql sql = db.getSql(); + List ids = db.query(joinAddressIds(addressFilter)); + if (ids != null && ids.isEmpty()) return List.of(); + String selectAddresses = SELECT + sql.dateToEpochSecond(sql.dateToDayStamp(sql.epochSecondToDate('(' + SessionsTable.SESSION_START + "+?)/1000"))) + "*1000 as date," + - JoinAddressTable.JOIN_ADDRESS + + JoinAddressTable.JOIN_ADDRESS + ',' + + SessionsTable.USER_ID + ", COUNT(1) as count" + FROM + SessionsTable.TABLE_NAME + " s" + LEFT_JOIN + JoinAddressTable.TABLE_NAME + " j on s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID + WHERE + SessionsTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID + AND + SessionsTable.SESSION_START + ">?" + AND + SessionsTable.SESSION_START + "<=?" + - GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS; + (ids == null ? "" : AND + "j." + JoinAddressTable.ID + + " IN (" + new TextStringBuilder().appendWithSeparators(ids, ",").build() + ")") + + GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS + ',' + SessionsTable.USER_ID; return db.query(new QueryStatement<>(selectAddresses, 1000) { @Override @@ -193,9 +235,9 @@ public List>> processResults(ResultSet set) throws while (set.next()) { long date = set.getLong("date"); String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS); - int count = set.getInt("count"); Map joinAddresses = addressesByDate.computeIfAbsent(date, k -> new TreeMap<>()); - joinAddresses.put(joinAddress, count); + // We ignore the count and get the number of players instead of sessions + joinAddresses.compute(joinAddress, (key, oldValue) -> oldValue != null ? oldValue + 1 : 1); } return addressesByDate.entrySet() @@ -206,20 +248,37 @@ public List>> processResults(ResultSet set) throws }; } - public static Query>>> joinAddressesPerDay(long timezoneOffset, long after, long before) { + public static Query> joinAddressIds(@Untrusted List addresses) { + return db -> { + if (addresses.isEmpty()) return null; + + String selectJoinAddressIds = SELECT + JoinAddressTable.ID + + FROM + JoinAddressTable.TABLE_NAME + + WHERE + JoinAddressTable.JOIN_ADDRESS + " IN (" + Sql.nParameters(addresses.size()) + ")"; + return db.queryList(selectJoinAddressIds, set -> set.getInt(JoinAddressTable.ID), addresses); + }; + } + + public static Query>>> joinAddressesPerDay(long timezoneOffset, long after, long before, @Untrusted List addressFilter) { return db -> { Sql sql = db.getSql(); + List ids = db.query(joinAddressIds(addressFilter)); + if (ids != null && ids.isEmpty()) return List.of(); + String selectAddresses = SELECT + sql.dateToEpochSecond(sql.dateToDayStamp(sql.epochSecondToDate('(' + SessionsTable.SESSION_START + "+?)/1000"))) + "*1000 as date," + - JoinAddressTable.JOIN_ADDRESS + + JoinAddressTable.JOIN_ADDRESS + ',' + + SessionsTable.USER_ID + ", COUNT(1) as count" + FROM + SessionsTable.TABLE_NAME + " s" + LEFT_JOIN + JoinAddressTable.TABLE_NAME + " j on s." + SessionsTable.JOIN_ADDRESS_ID + "=j." + JoinAddressTable.ID + WHERE + SessionsTable.SESSION_START + ">?" + AND + SessionsTable.SESSION_START + "<=?" + - GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS; + (ids == null ? "" : AND + "j." + JoinAddressTable.ID + + " IN (" + new TextStringBuilder().appendWithSeparators(ids, ",").build() + ")") + + GROUP_BY + "date,j." + JoinAddressTable.JOIN_ADDRESS + ',' + SessionsTable.USER_ID; return db.query(new QueryStatement<>(selectAddresses, 1000) { @Override @@ -235,9 +294,9 @@ public List>> processResults(ResultSet set) throws while (set.next()) { long date = set.getLong("date"); String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS); - int count = set.getInt("count"); Map joinAddresses = addressesByDate.computeIfAbsent(date, k -> new TreeMap<>()); - joinAddresses.put(joinAddress, count); + // We ignore the count and get the number of players instead of sessions + joinAddresses.compute(joinAddress, (key, oldValue) -> oldValue != null ? oldValue + 1 : 1); } return addressesByDate.entrySet() diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java index 0fa7ceb580..d4891a7754 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/patches/UpdateWebPermissionsPatch.java @@ -39,7 +39,7 @@ public class UpdateWebPermissionsPatch extends Patch { @Override public boolean hasBeenApplied() { - List defaultPermissions = Arrays.stream(WebPermission.values()) + List defaultPermissions = Arrays.stream(WebPermission.nonDeprecatedValues()) .map(WebPermission::getPermission) .collect(Collectors.toList()); List storedPermissions = query(WebUserQueries.fetchAvailablePermissions()); diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml index 7d4b21a629..b3098bffae 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CN.yml @@ -290,6 +290,8 @@ html: active: "活跃" activePlaytime: "活跃时间" activityIndex: "活跃指数" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "挂机" afkTime: "挂机时间" all: "全部" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "查看按国家划分的Ping表" page_network_join_addresses: "查看加入地址 - 选项卡" page_network_join_addresses_graphs: "查看加入地址图表" - page_network_join_addresses_graphs_pie: "查看最新加入地址图表" page_network_join_addresses_graphs_time: "查看加入地址随时间变化的图表" page_network_overview: "查看网络总览 - 选项卡" page_network_overview_graphs: "查看网络总览图表" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "查看按国家划分的延迟表" page_server_join_addresses: "查看服务器加入地址 - 选项卡" page_server_join_addresses_graphs: "查看服务器加入地址图表" - page_server_join_addresses_graphs_pie: "查看最新加入地址图表" page_server_join_addresses_graphs_time: "查看服务器加入地址随时间变化的图表" page_server_online_activity: "查看在线活动 - 选项卡" page_server_online_activity_graphs: "查看在线活动图表" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml index 8beef49363..2275f6f1c3 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_CS.yml @@ -290,6 +290,8 @@ html: active: "Aktivní" activePlaytime: "Aktivní herní čas" activityIndex: "Index aktivity" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "AFK čas" all: "Vše" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml index 6783cbbba6..857f5b0dc7 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_DE.yml @@ -290,6 +290,8 @@ html: active: "Aktiv" activePlaytime: "Aktive Spielzeit" activityIndex: "Aktivitätsindex" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "AFK Zeit" all: "Gesamt" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml index 79eec9005e..eb5ba4e1ce 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_EN.yml @@ -290,6 +290,8 @@ html: active: "Active" activePlaytime: "Active Playtime" activityIndex: "Activity Index" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "AFK Time" all: "All" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml index aab4957431..93f656ad24 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ES.yml @@ -290,6 +290,8 @@ html: active: "Activo" activePlaytime: "Tiempo de juego activo" activityIndex: "Índice de actividad" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "Tiempo AFK" all: "Todo" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml index a9a65d3839..88f41758e8 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FI.yml @@ -290,6 +290,8 @@ html: active: "Aktiivinen" activePlaytime: "Aktiivinen peliaika" activityIndex: "Aktiivisuus Indeksi" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "Aika AFK:ina" all: "Kaikki" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "Näkee Viive per Maa -taulun" page_network_join_addresses: "Näkee Liittymäosoitteet osion" page_network_join_addresses_graphs: "Näkee Liittymisosoite kaaviot" - page_network_join_addresses_graphs_pie: "Näkee kaavion Viimeisimmistä liittymisosoitteista" page_network_join_addresses_graphs_time: "Näkee kaavion Liittymisosoitteista ajan yli" page_network_overview: "Näkee Verkoston katsaus osion" page_network_overview_graphs: "Näkee Verkoston katsaus kaaviot" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "Näkee Viive per Maa -taulun" page_server_join_addresses: "Näkee Liittymäosoitteet osion" page_server_join_addresses_graphs: "Näkee Liittymisosoite kaaviot" - page_server_join_addresses_graphs_pie: "Näkee kaavion Viimeisimmistä liittymisosoitteista" page_server_join_addresses_graphs_time: "Näkee kaavion Liittymisosoitteista ajan yli" page_server_online_activity: "Näkee Online Aktiivisuus osion" page_server_online_activity_graphs: "Näkee Online Aktiivisuus kaaviot" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml index fa18173ebf..7a20bc5717 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_FR.yml @@ -290,6 +290,8 @@ html: active: "Actif" activePlaytime: "Temps Actif" activityIndex: "Indice d'Activité" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "Temps AFK" all: "Tout" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "Voir le tableau Ping par pays" page_network_join_addresses: "Voir les adresses de jointure -tab" page_network_join_addresses_graphs: "Voir les graphiques de l'adresse de jonction" - page_network_join_addresses_graphs_pie: "Voir le graphique des dernières adresses de jointure" page_network_join_addresses_graphs_time: "Voir le graphique des adresses de jointure dans le temps" page_network_overview: "Voir Aperçu du réseau -tab" page_network_overview_graphs: "Voir les graphiques de l'aperçu du réseau" @@ -682,7 +683,7 @@ html: page_network_plugins: "Voir l'onglet Plugins de Proxy" page_network_retention: "Voir l'onglet Rétention des joueurs" page_network_server_list: "Voir la liste des serveurs" - page_network_sessions : "Voir l'onglet Sessions" + page_network_sessions: "Voir l'onglet Sessions" page_network_sessions_list: "See list of sessions" page_network_sessions_overview: "Voir les perspectives de la session" page_network_sessions_server_pie: "Voir le graphique à secteurs du serveur" @@ -690,7 +691,7 @@ html: page_player: "Voir toute la page du joueur" page_player_overview: "Voir l'aperçu des joueurs -tab" page_player_plugins: "Voir les plugins -tabs" - page_player_servers : "Voir les serveurs -tab" + page_player_servers: "Voir les serveurs -tab" page_player_sessions: "Voir les sessions des joueurs -tab" page_player_versus: "Voir PvP & PvE -tab" page_server: "Voir toute la page du serveur" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "Voir le tableau Ping par pays" page_server_join_addresses: "Voir les adresses de jointure -tab" page_server_join_addresses_graphs: "Voir les graphiques de l'adresse de connexion" - page_server_join_addresses_graphs_pie: "Voir le graphique des dernières adresses de connexion" page_server_join_addresses_graphs_time: "Voir le graphique des adresses de connexion dans le temps" page_server_online_activity: "Voir l'activité en ligne -tab" page_server_online_activity_graphs: "Voir les graphiques de l'activité en ligne" @@ -725,7 +725,7 @@ html: page_server_plugin_history: "Voir l'historique du plugin" page_server_plugins: "Voir les onglets Plugins des serveurs" page_server_retention: "Voir l'onglet Rétention des joueurs" - page_server_sessions : "Voir l'onglet Sessions" + page_server_sessions: "Voir l'onglet Sessions" page_server_sessions_list: "Voir la liste des sessions" page_server_sessions_overview: "Voir les perspectives de la session" page_server_sessions_world_pie: "Voir le graphique de la carte du monde" @@ -794,6 +794,7 @@ html: generic: are: "`sont`" label: + editQuery: "Edit Query" from: ">de" makeAnother: "Faire une autre Requête" servers: diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml index dec833ffaa..1eee8f8368 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_IT.yml @@ -290,6 +290,8 @@ html: active: "Attivo" activePlaytime: "Active Playtime" activityIndex: "Indice Inattività" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "Tempo AFK" all: "Tutto" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml index fa608b8c93..205ea931f8 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_JA.yml @@ -290,6 +290,8 @@ html: active: "よくログインしている" activePlaytime: "アクティブなプレイ時間" activityIndex: "活動指数" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "離席" afkTime: "離席時間" all: "全て" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "国ごとのPing表を表示" page_network_join_addresses: "参加アドレスタブを表示" page_network_join_addresses_graphs: "参加アドレスのグラフを表示" - page_network_join_addresses_graphs_pie: "最後に参加したアドレスのグラフを表示" page_network_join_addresses_graphs_time: "参加アドレスの経時変化のグラフを表示" page_network_overview: "ネットワークの概要タブを表示" page_network_overview_graphs: "ネットワークの概要グラフを表示" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "国ごとのPing表を表示" page_server_join_addresses: "参加アドレスタブを表示" page_server_join_addresses_graphs: "参加アドレスグラフを表示" - page_server_join_addresses_graphs_pie: "最後に参加したアドレスのグラフを表示" page_server_join_addresses_graphs_time: "参加アドレスの経時変化のグラフを表示" page_server_online_activity: "オンラインアクティビティタブを表示" page_server_online_activity_graphs: "オンラインアクティビティグラフを表示" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml index ad1619bb51..8e3fb5d6df 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_KO.yml @@ -290,6 +290,8 @@ html: active: "활동적인" activePlaytime: "Active Playtime" activityIndex: "활동 색인" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "AFK 시간" all: "모두" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml index a0318480e9..08b41e3e29 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_NL.yml @@ -290,6 +290,8 @@ html: active: "Actief" activePlaytime: "Actieve Speeltijd" activityIndex: "Activiteitsindex" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "AFK Tijd" all: "Alle" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml index f519c18c80..401d50d049 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_PT_BR.yml @@ -290,6 +290,8 @@ html: active: "Ativo" activePlaytime: "Active Playtime" activityIndex: "Índice de Atividade" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "AFK Time" all: "Todos" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml index 645d904deb..6e9e651556 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_RU.yml @@ -290,6 +290,8 @@ html: active: "Активный" activePlaytime: "Активное время игры" activityIndex: "Индекс активности" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "Время AFK" all: "Все" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml index 3832aaf5f2..c363139a67 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_TR.yml @@ -290,6 +290,8 @@ html: active: "Aktivite" activePlaytime: "Aktif Oyun Süresi" activityIndex: "Aktivite göstergesi" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "AFK Süresi" all: "Tamamı" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml index 620a45e5e1..0477308274 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_UK.yml @@ -290,6 +290,8 @@ html: active: "Активний" activePlaytime: "Активний час гри" activityIndex: "Індекс активності" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "AFK" afkTime: "Час AFK" all: "Всі" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml index faddbd8368..eb0b4c8fab 100644 --- a/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml +++ b/Plan/common/src/main/resources/assets/plan/locale/locale_ZH_TW.yml @@ -290,6 +290,8 @@ html: active: "活躍" activePlaytime: "活躍時間" activityIndex: "活躍指數" + addJoinAddressGroup: "Add address group" + addressGroup: "Address group {{n}}" afk: "掛機" afkTime: "掛機時間" all: "全部" @@ -664,7 +666,6 @@ html: page_network_geolocations_ping_per_country: "See Ping Per Country table" page_network_join_addresses: "See Join Addresses -tab" page_network_join_addresses_graphs: "See Join Address graphs" - page_network_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_network_join_addresses_graphs_time: "See Join Addresses over time graph" page_network_overview: "See Network Overview -tab" page_network_overview_graphs: "See Network Overview graphs" @@ -700,7 +701,6 @@ html: page_server_geolocations_ping_per_country: "See Ping Per Country table" page_server_join_addresses: "See Join Addresses -tab" page_server_join_addresses_graphs: "See Join Address graphs" - page_server_join_addresses_graphs_pie: "See Latest Join Addresses graph" page_server_join_addresses_graphs_time: "See Join Addresses over time graph" page_server_online_activity: "See Online Activity -tab" page_server_online_activity_graphs: "See Online Activity graphs" diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionTest.java new file mode 100644 index 0000000000..0d65e0d1c8 --- /dev/null +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/domain/auth/WebPermissionTest.java @@ -0,0 +1,56 @@ +/* + * 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.auth; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link WebPermission}. + * + * @author AuroraLS3 + */ +class WebPermissionTest { + + @Test + void webPermissionIsFound() { + String permission = "access.player.self"; + WebPermission found = WebPermission.findByPermission(permission).orElseThrow(AssertionError::new); + WebPermission expected = WebPermission.ACCESS_PLAYER_SELF; + assertEquals(expected, found); + } + + @Test + void webPermissionIsDetectedAsDeprecated() { + String permission = "page.server.join.addresses.graphs.pie"; + assertTrue(WebPermission.isDeprecated(permission)); + } + + @Test + void webPermissionIsDetectedAsNonDeprecated() { + String permission = "access.player.self"; + assertFalse(WebPermission.isDeprecated(permission)); + } + + @Test + void customWebPermissionIsDetectedAsNonDeprecated() { + String permission = "custom.permission"; + assertFalse(WebPermission.isDeprecated(permission)); + } + +} \ No newline at end of file diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java index dd19ffff74..6aa26ed0af 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlTest.java @@ -95,7 +95,6 @@ static Stream testCases() { Arguments.of("/v1/graph?type=aggregatedPing&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, 200, 403), Arguments.of("/v1/graph?type=worldPie&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_SESSIONS_WORLD_PIE, 200, 403), Arguments.of("/v1/graph?type=activity&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, 200, 403), - Arguments.of("/v1/graph?type=joinAddressPie&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE, 200, 403), Arguments.of("/v1/graph?type=geolocation&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP, 200, 403), Arguments.of("/v1/graph?type=uniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY, 200, 403), Arguments.of("/v1/graph?type=hourlyUniqueAndNew&server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_HOUR_BY_HOUR, 200, 403), @@ -107,7 +106,10 @@ static Stream testCases() { Arguments.of("/v1/pingTable?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_GEOLOCATIONS_PING_PER_COUNTRY, 200, 403), Arguments.of("/v1/sessions?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_SESSIONS_LIST, 200, 403), Arguments.of("/v1/retention?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_RETENTION, 200, 403), + Arguments.of("/v1/joinAddresses", WebPermission.PAGE_NETWORK_RETENTION, 200, 403), + Arguments.of("/v1/joinAddresses?listOnly=true", WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME, 200, 403), Arguments.of("/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + "", WebPermission.PAGE_SERVER_RETENTION, 200, 403), + Arguments.of("/v1/joinAddresses?server=" + TestConstants.SERVER_UUID_STRING + "&listOnly=true", WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, 200, 403), Arguments.of("/network", WebPermission.ACCESS_NETWORK, 302, 403), Arguments.of("/v1/network/overview", WebPermission.PAGE_NETWORK_OVERVIEW_NUMBERS, 200, 403), Arguments.of("/v1/network/servers", WebPermission.PAGE_NETWORK_SERVER_LIST, 200, 403), @@ -119,7 +121,6 @@ static Stream testCases() { Arguments.of("/v1/graph?type=hourlyUniqueAndNew", WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR, 200, 403), Arguments.of("/v1/graph?type=serverCalendar", WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_CALENDAR, 200, 403), Arguments.of("/v1/graph?type=serverPie", WebPermission.PAGE_NETWORK_SESSIONS_SERVER_PIE, 200, 403), - Arguments.of("/v1/graph?type=joinAddressPie", WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE, 200, 403), Arguments.of("/v1/graph?type=activity", WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, 200, 403), Arguments.of("/v1/graph?type=geolocation", WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP, 200, 403), Arguments.of("/v1/network/pingTable", WebPermission.PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY, 200, 403), diff --git a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java index 3feaa34ec4..544619633e 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/delivery/webserver/AccessControlVisibilityTest.java @@ -141,8 +141,7 @@ static Stream serverPageElementVisibleCases() { Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_OVERVIEW, "playerbase-insights", "playerbase"), Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, "playerbase-graph", "playerbase"), Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERBASE_GRAPHS, "playerbase-current", "playerbase"), - Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, "join-address-graph", "join-addresses"), - Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_PIE, "join-address-groups", "join-addresses"), + Arguments.arguments(WebPermission.PAGE_SERVER_JOIN_ADDRESSES_GRAPHS_TIME, "server-join-addresses", "join-addresses"), Arguments.arguments(WebPermission.PAGE_SERVER_RETENTION, "retention-graph", "retention"), Arguments.arguments(WebPermission.PAGE_SERVER_PLAYERS, "players-table", "players"), Arguments.arguments(WebPermission.PAGE_SERVER_GEOLOCATIONS_MAP, "geolocations", "geolocations"), @@ -172,8 +171,7 @@ static Stream networkPageElementVisibleCases() { Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_OVERVIEW, "playerbase-insights", "playerbase"), Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, "playerbase-graph", "playerbase"), Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERBASE_GRAPHS, "playerbase-current", "playerbase"), - Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME, "join-address-graph", "join-addresses"), - Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_PIE, "join-address-groups", "join-addresses"), + Arguments.arguments(WebPermission.PAGE_NETWORK_JOIN_ADDRESSES_GRAPHS_TIME, "network-join-addresses", "join-addresses"), Arguments.arguments(WebPermission.PAGE_NETWORK_RETENTION, "retention-graph", "retention"), Arguments.arguments(WebPermission.PAGE_NETWORK_PLAYERS, "players-table", "players"), Arguments.arguments(WebPermission.PAGE_NETWORK_GEOLOCATIONS_MAP, "geolocations", "geolocations"), diff --git a/Plan/common/src/test/java/com/djrapitops/plan/gathering/events/PlayerLeaveEventConsumerTest.java b/Plan/common/src/test/java/com/djrapitops/plan/gathering/events/PlayerLeaveEventConsumerTest.java index 07a8196293..2a03b4ceb8 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/gathering/events/PlayerLeaveEventConsumerTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/gathering/events/PlayerLeaveEventConsumerTest.java @@ -264,13 +264,5 @@ void joinAddressCaseIsPreserved(PlanSystem system, PlanConfig config, Database d List expected = List.of("PLAY.UPPERCASE.COM", "play.uppercase.com", JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP); List result = database.query(JoinAddressQueries.allJoinAddresses()); assertEquals(expected, result); - - Map expectedMap = Map.of("PLAY.UPPERCASE.COM", 1); - Map resultMap = database.query(JoinAddressQueries.latestJoinAddresses(serverUUID)); - assertEquals(expectedMap, resultMap); - - expectedMap = Map.of("PLAY.UPPERCASE.COM", 1); - resultMap = database.query(JoinAddressQueries.latestJoinAddresses()); - assertEquals(expectedMap, resultMap); } } \ No newline at end of file diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/JoinAddressQueriesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/JoinAddressQueriesTest.java index d0d596d3dd..805c555802 100644 --- a/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/JoinAddressQueriesTest.java +++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/database/queries/JoinAddressQueriesTest.java @@ -16,8 +16,10 @@ */ package com.djrapitops.plan.storage.database.queries; +import com.djrapitops.plan.delivery.domain.DateObj; import com.djrapitops.plan.gathering.domain.FinishedSession; import com.djrapitops.plan.gathering.domain.event.JoinAddress; +import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.settings.config.paths.DataGatheringSettings; import com.djrapitops.plan.storage.database.DatabaseTestPreparer; import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries; @@ -27,12 +29,14 @@ import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction; import com.djrapitops.plan.storage.database.transactions.events.*; import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import utilities.RandomData; import utilities.TestConstants; import utilities.TestData; import java.util.*; +import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -120,6 +124,55 @@ default void joinAddressPreservesCase() { } } + @Test + @DisplayName("Join address by day is filtered by addresses") + default void joinAddressListIsFilteredByAddress() { + db().executeTransaction(TestData.storeServers()); + + db().executeTransaction(new StoreWorldNameTransaction(TestConstants.SERVER_TWO_UUID, worlds[0])); + db().executeTransaction(new StoreWorldNameTransaction(TestConstants.SERVER_TWO_UUID, worlds[1])); + FinishedSession session = RandomData.randomSession(TestConstants.SERVER_TWO_UUID, worlds, playerUUID, player2UUID); + String expectedAddress = TestConstants.GET_PLAYER_HOSTNAME.get(); + session.getExtraData().put(JoinAddress.class, new JoinAddress(expectedAddress)); + db().executeTransaction(new StoreSessionTransaction(session)); + + List>> result = db().query(JoinAddressQueries.joinAddressesPerDay(0, 0, System.currentTimeMillis(), List.of("nonexistent.com"))); + assertEquals(List.of(), result); + + long startOfDay = session.getDate() - session.getDate() % TimeUnit.DAYS.toMillis(1); + + List>> result2 = db().query(JoinAddressQueries.joinAddressesPerDay(0, 0, System.currentTimeMillis(), List.of(expectedAddress))); + assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result2); + + List>> result3 = db().query(JoinAddressQueries.joinAddressesPerDay(0, 0, System.currentTimeMillis(), List.of())); + assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result3); + } + + @Test + @DisplayName("Server join address by day is filtered by addresses") + default void serverJoinAddressListIsFilteredByAddress() { + db().executeTransaction(TestData.storeServers()); + + ServerUUID serverTwoUuid = TestConstants.SERVER_TWO_UUID; + db().executeTransaction(new StoreWorldNameTransaction(serverTwoUuid, worlds[0])); + db().executeTransaction(new StoreWorldNameTransaction(serverTwoUuid, worlds[1])); + FinishedSession session = RandomData.randomSession(serverTwoUuid, worlds, playerUUID, player2UUID); + String expectedAddress = TestConstants.GET_PLAYER_HOSTNAME.get(); + session.getExtraData().put(JoinAddress.class, new JoinAddress(expectedAddress)); + db().executeTransaction(new StoreSessionTransaction(session)); + + List>> result = db().query(JoinAddressQueries.joinAddressesPerDay(serverTwoUuid, 0, 0, System.currentTimeMillis(), List.of("nonexistent.com"))); + assertEquals(List.of(), result); + + long startOfDay = session.getDate() - session.getDate() % TimeUnit.DAYS.toMillis(1); + + List>> result2 = db().query(JoinAddressQueries.joinAddressesPerDay(serverTwoUuid, 0, 0, System.currentTimeMillis(), List.of(expectedAddress))); + assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result2); + + List>> result3 = db().query(JoinAddressQueries.joinAddressesPerDay(serverTwoUuid, 0, 0, System.currentTimeMillis(), List.of())); + assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result3); + } + @Test default void joinAddressIsTruncated() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); @@ -201,15 +254,6 @@ default void joinAddressQueryHasNoNullValues() { assertEquals(expected, result); } - @Test - default void serverJoinAddressQueryHasNoNullValues() { - joinAddressCanBeUnknown(); - - Map expected = Collections.singletonMap(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP, 1); - Map result = db().query(JoinAddressQueries.latestJoinAddresses(serverUUID())); - assertEquals(expected, result); - } - @Test default void joinAddressQueryHasDistinctPlayers() { joinAddressCanBeUnknown(); diff --git a/Plan/react/dashboard/src/components/cards/common/AddressGroupCard.jsx b/Plan/react/dashboard/src/components/cards/common/AddressGroupCard.jsx new file mode 100644 index 0000000000..623d896496 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/AddressGroupCard.jsx @@ -0,0 +1,66 @@ +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 AddressGroupCard = ({n, group, editGroup, allAddresses, remove}) => { + const {t} = useTranslation(); + const [selectedIndexes, setSelectedIndexes] = useState([]); + const [editingName, setEditingName] = useState(false); + const [name, setName] = useState(group.name); + + useEffect(() => { + if (!selectedIndexes.length && allAddresses?.length && group?.addresses?.length) { + setSelectedIndexes(group.addresses + .map(address => allAddresses.indexOf(address)) + .filter(index => index !== -1)) // Make sure addresses are not selected that no longer exist + } + }, [selectedIndexes, group, allAddresses]) + + 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]) + + const selectedAddresses = allAddresses.filter((a, i) => selectedIndexes.includes(i)); + const isUpToDate = !selectedIndexes.length || selectedAddresses.length === group.addresses.length && selectedAddresses.every((a, i) => a === group.addresses[i]); + return ( + + setName(e.target.value)}/> : group.name + }> + + + + + + + + + ) +} +export default AddressGroupCard; \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/common/AddressGroupSelectorRow.jsx b/Plan/react/dashboard/src/components/cards/common/AddressGroupSelectorRow.jsx new file mode 100644 index 0000000000..152d5123ad --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/AddressGroupSelectorRow.jsx @@ -0,0 +1,32 @@ +import {Col, Row} from "react-bootstrap"; +import AddressGroupCard from "./AddressGroupCard.jsx"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faPlus} from "@fortawesome/free-solid-svg-icons"; +import React from "react"; +import {useTranslation} from "react-i18next"; +import {useJoinAddressListContext} from "../../../hooks/context/joinAddressListContextHook.jsx"; + +const AddressGroupSelectorRow = () => { + const {t} = useTranslation(); + const {list, add, remove, replace, allAddresses} = useJoinAddressListContext(); + + return ( + + {list.map((group, i) => + + replace(replacement, i)} + allAddresses={allAddresses} + remove={() => remove(i)}/> + )} + + + + + ) +} + +export default AddressGroupSelectorRow; \ 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 0000000000..ea42e10626 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/JoinAddresses.jsx @@ -0,0 +1,27 @@ +import LoadIn from "../../animation/LoadIn.jsx"; +import ExtendableRow from "../../layout/extension/ExtendableRow.jsx"; +import JoinAddressGraphCard from "../server/graphs/JoinAddressGraphCard.jsx"; +import {Col} from "react-bootstrap"; +import React from "react"; +import AddressGroupSelectorRow from "./AddressGroupSelectorRow.jsx"; +import {JoinAddressListContextProvider} from "../../../hooks/context/joinAddressListContextHook.jsx"; +import {staticSite} from "../../../service/backendConfiguration.js"; + +const JoinAddresses = ({id, seeTime, identifier}) => { + return ( + + {seeTime &&
+ + + + + + + {!staticSite && } + +
} +
+ ) +} + +export default JoinAddresses; \ No newline at end of file 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 0000000000..ce4679143f --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/common/PlayerRetention.jsx @@ -0,0 +1,30 @@ +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 1e0786a011..0ec076c772 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,11 @@ 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 +181,7 @@ const PlayerRetentionGraphCard = ({identifier}) => { } } return grouped; - }, [groupByOptions, selectedGroupBy]); + }, [groupByOptions, selectedGroupBy, list]); const createSeries = useCallback(async (retentionData, joinAddressData) => { @@ -207,10 +213,10 @@ const PlayerRetentionGraphCard = ({identifier}) => { }, [nightModeEnabled, mapToData, groupOptions, selectedGroup, selectedYAxis, group]); useEffect(() => { - if (!data || !joinAddressData) return; + if (!data || !playerAddresses) return; - createSeries(data.player_retention, joinAddressData.join_address_by_player).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; @@ -261,13 +267,15 @@ const PlayerRetentionGraphCard = ({identifier}) => { }, tooltip: selectedAxis === 'date' || selectedAxis === 'deltas' ? { enabled: true, + shared: series.length <= 10, valueDecimals: 2, - pointFormat: (selectedGroupBy !== 'none' ? '{series.name} - ' : '') + '{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '' + pointFormat: (selectedGroupBy !== 'none' ? '{series.name} - ' : '') + '{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '
' } : { enabled: true, + shared: series.length <= 10, valueDecimals: 2, headerFormat: '{point.x} ' + windowName + '
', - pointFormat: (selectedGroupBy !== 'none' ? '{series.name} - ' : '') + '{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '' + pointFormat: (selectedGroupBy !== 'none' ? '{series.name} - ' : '') + '{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '
' }, series: series }) 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 f6fe624a41..01a8f9ad29 100644 --- a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.jsx +++ b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGraphCard.jsx @@ -1,24 +1,80 @@ -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"; +import {staticSite} from "../../../../service/backendConfiguration.js"; const JoinAddressGraphCard = ({identifier}) => { const {t} = useTranslation(); const [stack, setStack] = useState(true); + const {updateRequested} = useNavigation(); - const {data, loadingError} = useDataRequest(fetchJoinAddressByDay, [identifier]); + const {list} = useJoinAddressListContext(); + const noSelectedAddresses = !list.filter(group => group.addresses.length).length; - if (loadingError) return - if (!data) return ; + const [data, setData] = useState(undefined); + const [loadingError, setLoadingError] = useState(undefined); + const loadAddresses = useCallback(async () => { + if (!staticSite && noSelectedAddresses) return; + + let colors = ['#4ab4de']; + const dataByGroup = []; + const addressGroups = staticSite ? [{addresses: [], name: ""}] : list.filter(group => group.addresses.length); + for (const group of addressGroups) { + 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 || []}); + } + + if (!staticSite) { + // 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 + }); + } else { + // On exported site we get all addresses individually + setData({join_addresses_by_date: dataByGroup[0].data, colors}) + } + }, [setData, setLoadingError, identifier, updateRequested, list]); + + useEffect(() => { + loadAddresses(); + }, [loadAddresses]); + + if (loadingError) return return ( @@ -28,8 +84,12 @@ const JoinAddressGraphCard = ({identifier}) => { {t('html.label.stacked')} - + {data && + } + {!data && noSelectedAddresses && +

Select some addresses

} + {!data && !noSelectedAddresses && }
) }; diff --git a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.jsx b/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.jsx deleted file mode 100644 index 4bb282f8fd..0000000000 --- a/Plan/react/dashboard/src/components/cards/server/graphs/JoinAddressGroupCard.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import {useTranslation} from "react-i18next"; -import {useDataRequest} from "../../../../hooks/dataFetchHook"; -import {fetchJoinAddressPie} from "../../../../service/serverService"; -import {ErrorViewCard} from "../../../../views/ErrorView"; -import {CardLoader} from "../../../navigation/Loader"; -import {Card} from "react-bootstrap"; -import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; -import {faLocationArrow} from "@fortawesome/free-solid-svg-icons"; -import GroupVisualizer from "../../../graphs/GroupVisualizer"; - -const JoinAddressGroupCard = ({identifier}) => { - const {t} = useTranslation(); - - const {data, loadingError} = useDataRequest(fetchJoinAddressPie, [identifier]); - - if (loadingError) return - if (!data) return ; - - return ( - - -
- {t('html.label.latestJoinAddresses')} -
-
- -
- ) -}; - -export default JoinAddressGroupCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/input/MultiSelect.jsx b/Plan/react/dashboard/src/components/input/MultiSelect.jsx index 7e004c0070..68b90768b4 100644 --- a/Plan/react/dashboard/src/components/input/MultiSelect.jsx +++ b/Plan/react/dashboard/src/components/input/MultiSelect.jsx @@ -1,6 +1,6 @@ import React from 'react'; -const MultiSelect = ({options, selectedIndexes, setSelectedIndexes}) => { +const MultiSelect = ({options, selectedIndexes, setSelectedIndexes, className}) => { const handleChange = (event) => { const renderedOptions = Object.values(event.target.selectedOptions) .map(htmlElement => htmlElement.text) @@ -9,7 +9,7 @@ const MultiSelect = ({options, selectedIndexes, setSelectedIndexes}) => { } return ( -