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
This commit is contained in:
Aurora Lahtela 2024-04-07 11:13:18 +03:00 committed by GitHub
parent b6f68936cf
commit f40e1498c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 814 additions and 357 deletions

View File

@ -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 class DateObj<T> implements DateHolder {
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 +
'}';
}
}

View File

@ -19,6 +19,9 @@ package com.djrapitops.plan.delivery.domain.auth;
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<String>, 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<String>, 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 enum WebPermission implements Supplier<String>, Lang {
public String getDefault() {
return description;
}
public static WebPermission[] nonDeprecatedValues() {
return Arrays.stream(values())
.filter(Predicate.not(WebPermission::isDeprecated))
.toArray(WebPermission[]::new);
}
public static Optional<WebPermission> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> joinAddresses;
private final Map<UUID, String> joinAddressByPlayer;
public PlayerJoinAddresses(List<String> joinAddresses, Map<UUID, String> joinAddressByPlayer) {
this.joinAddresses = joinAddresses;
this.joinAddressByPlayer = joinAddressByPlayer;
}
public List<String> getJoinAddresses() {
return joinAddresses;
}
public Map<UUID, String> 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 +
'}';
}
}

View File

@ -121,7 +121,6 @@ public class NetworkPageExporter extends FileExporter {
"graph?type=uniqueAndNew",
"graph?type=hourlyUniqueAndNew",
"graph?type=serverPie",
"graph?type=joinAddressPie",
"graph?type=joinAddressByDay",
"graph?type=activity",
"graph?type=geolocation",

View File

@ -137,7 +137,6 @@ public class ServerPageExporter extends FileExporter {
"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,

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.rendering.json;
import com.djrapitops.plan.delivery.domain.DateObj;
import com.djrapitops.plan.delivery.domain.RetentionData;
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses;
import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto;
import com.djrapitops.plan.delivery.domain.mutators.PlayerKillMutator;
import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator;
@ -160,14 +161,25 @@ public class JSONFactory {
return db.query(PlayerRetentionQueries.fetchRetentionData());
}
public Map<UUID, String> playerJoinAddresses(ServerUUID serverUUID) {
public PlayerJoinAddresses playerJoinAddresses(ServerUUID serverUUID, boolean includeByPlayerMap) {
Database db = dbSystem.getDatabase();
return db.query(JoinAddressQueries.latestJoinAddressesOfPlayers(serverUUID));
if (includeByPlayerMap) {
Map<UUID, String> 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<UUID, String> 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<Map<String, Object>> serverSessionsAsJSONMap(ServerUUID serverUUID) {

View File

@ -32,7 +32,6 @@ import com.djrapitops.plan.delivery.rendering.json.graphs.line.LineGraphFactory;
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.analysis.PlayerCountQueries;
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 class GraphJSONCreator {
.build();
}
public Map<String, Object> playerHostnamePieJSONAsMap() {
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
Map<String, Integer> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.latestJoinAddresses());
translateUnknown(joinAddresses);
List<PieSlice> 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<String, Object> playerHostnamePieJSONAsMap(ServerUUID serverUUID) {
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
Map<String, Integer> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.latestJoinAddresses(serverUUID));
translateUnknown(joinAddresses);
List<PieSlice> 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<String, Integer> joinAddresses) {
Integer unknown = joinAddresses.get(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
if (unknown != null) {
@ -493,16 +464,16 @@ public class GraphJSONCreator {
}
}
public Map<String, Object> joinAddressesByDay(ServerUUID serverUUID, long after, long before) {
public Map<String, Object> joinAddressesByDay(ServerUUID serverUUID, long after, long before, @Untrusted List<String> addressFilter) {
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before));
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(serverUUID, config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter));
return mapToJson(pieColors, joinAddresses);
}
public Map<String, Object> joinAddressesByDay(long after, long before) {
public Map<String, Object> joinAddressesByDay(long after, long before, @Untrusted List<String> addressFilter) {
String[] pieColors = theme.getPieColors(ThemeVal.GRAPH_WORLD_PIE);
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before));
List<DateObj<Map<String, Integer>>> joinAddresses = dbSystem.getDatabase().query(JoinAddressQueries.joinAddressesPerDay(config.getTimeZone().getOffset(System.currentTimeMillis()), after, before, addressFilter));
return mapToJson(pieColors, joinAddresses);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Integer> joinAddresses) {
super(turnToSlices(joinAddresses));
}
private static List<PieSlice> turnToSlices(Map<String, Integer> joinAddresses) {
List<PieSlice> slices = new ArrayList<>();
for (Map.Entry<String, Integer> address : joinAddresses.entrySet()) {
String joinAddress = address.getKey();
Integer total = address.getValue();
slices.add(new PieSlice(joinAddress, total));
}
return slices;
}
}

View File

@ -68,10 +68,6 @@ public class PieGraphFactory {
return new ServerPreferencePie(playtimeByServerName);
}
public Pie joinAddressPie(Map<String, Integer> joinAddresses) {
return new JoinAddressPie(joinAddresses);
}
public WorldPie worldPie(WorldTimes worldTimes) {
WorldAliasSettings worldAliasSettings = config.getWorldAliasSettings();
Map<String, Long> playtimePerAlias = worldAliasSettings.getPlaytimePerAlias(worldTimes);

View File

@ -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;

View File

@ -74,6 +74,10 @@ public interface JSONStorage extends SubSystem {
this.timestamp = timestamp;
}
public static StoredJSON fromObject(Object json, long timestamp) {
return new StoredJSON(new Gson().toJson(json), timestamp);
}
public String getJson() {
return json;
}

View File

@ -39,11 +39,16 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
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 class GraphsJSONResolver extends JSONResolver {
@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 @@ public class GraphsJSONResolver extends JSONResolver {
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<ServerUUID, Object> 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<Object> 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 @@ public class GraphsJSONResolver extends JSONResolver {
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 @@ public class GraphsJSONResolver extends JSONResolver {
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 @@ public class GraphsJSONResolver extends JSONResolver {
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 @@ public class GraphsJSONResolver extends JSONResolver {
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 @@ public class GraphsJSONResolver extends JSONResolver {
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<String, Object> 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<String> 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 @@ public class GraphsJSONResolver extends JSONResolver {
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<String, Object> 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<String> 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)");
}
}
}

View File

@ -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.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
@ -42,7 +45,6 @@ import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.Optional;
/**
@ -69,17 +71,27 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
@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<String> 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 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
if (request.getQuery().get("server").isPresent()) {
ServerUUID serverUUID = identifiers.getServerUUID(request);
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES, serverUUID,
theUUID -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses(theUUID))
serverUUID1 -> jsonFactory.playerJoinAddresses(serverUUID1, request.getQuery().get("listOnly").isEmpty())
);
}
// Assume network
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES,
() -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses())
() -> jsonFactory.playerJoinAddresses(request.getQuery().get("listOnly").isEmpty())
);
}
}

View File

@ -37,6 +37,8 @@ import javax.inject.Inject;
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 class WebPermissionJSONResolver implements Resolver {
}
private Response getResponse() {
List<String> permissions = dbSystem.getDatabase().query(WebUserQueries.fetchAvailablePermissions());
List<String> permissions = dbSystem.getDatabase().query(WebUserQueries.fetchAvailablePermissions())
.stream()
.filter(Predicate.not(WebPermission::isDeprecated))
.collect(Collectors.toList());
WebPermissionList permissionList = new WebPermissionList(permissions);
return Response.builder()

View File

@ -108,7 +108,7 @@ public class LocaleSystem implements SubSystem {
HtmlLang.values(),
JSLang.values(),
PluginLang.values(),
WebPermission.values(),
WebPermission.nonDeprecatedValues(),
};
}

View File

@ -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"),

View File

@ -28,6 +28,8 @@ import com.djrapitops.plan.storage.database.sql.tables.ServerTable;
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 @@ public class JoinAddressQueries {
/* Static method class */
}
@VisibleForTesting
public static Query<Map<String, Integer>> latestJoinAddresses() {
String selectLatestJoinAddresses = SELECT +
"COUNT(1) as total," +
@ -66,6 +69,7 @@ public class JoinAddressQueries {
joinAddresses.put(UUID.fromString(set.getString(UsersTable.USER_UUID)), set.getString(JoinAddressTable.JOIN_ADDRESS));
}
@VisibleForTesting
public static Query<Map<String, Integer>> 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 class JoinAddressQueries {
};
}
public static QueryStatement<List<String>> 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<String> processResults(ResultSet set) throws SQLException {
List<String> joinAddresses = new ArrayList<>();
while (set.next()) joinAddresses.add(set.getString(JoinAddressTable.JOIN_ADDRESS));
return joinAddresses;
}
};
}
public static Query<List<String>> uniqueJoinAddresses() {
return db -> {
List<String> addresses = db.query(allJoinAddresses());
@ -151,6 +177,16 @@ public class JoinAddressQueries {
};
}
public static Query<List<String>> uniqueJoinAddresses(ServerUUID serverUUID) {
return db -> {
List<String> 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<Set<Integer>> userIdsOfPlayersWithJoinAddresses(@Untrusted List<String> joinAddresses) {
String sql = SELECT + DISTINCT + SessionsTable.USER_ID +
FROM + JoinAddressTable.TABLE_NAME + " j" +
@ -162,21 +198,27 @@ public class JoinAddressQueries {
return db -> db.querySet(sql, RowExtractors.getInt(SessionsTable.USER_ID), joinAddresses.toArray());
}
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(ServerUUID serverUUID, long timezoneOffset, long after, long before) {
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(ServerUUID serverUUID, long timezoneOffset, long after, long before, @Untrusted List<String> addressFilter) {
return db -> {
Sql sql = db.getSql();
List<Integer> 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 class JoinAddressQueries {
while (set.next()) {
long date = set.getLong("date");
String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS);
int count = set.getInt("count");
Map<String, Integer> 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 class JoinAddressQueries {
};
}
public static Query<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(long timezoneOffset, long after, long before) {
public static Query<List<Integer>> joinAddressIds(@Untrusted List<String> 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<List<DateObj<Map<String, Integer>>>> joinAddressesPerDay(long timezoneOffset, long after, long before, @Untrusted List<String> addressFilter) {
return db -> {
Sql sql = db.getSql();
List<Integer> 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 class JoinAddressQueries {
while (set.next()) {
long date = set.getLong("date");
String joinAddress = set.getString(JoinAddressTable.JOIN_ADDRESS);
int count = set.getInt("count");
Map<String, Integer> 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()

View File

@ -39,7 +39,7 @@ public class UpdateWebPermissionsPatch extends Patch {
@Override
public boolean hasBeenApplied() {
List<String> defaultPermissions = Arrays.stream(WebPermission.values())
List<String> defaultPermissions = Arrays.stream(WebPermission.nonDeprecatedValues())
.map(WebPermission::getPermission)
.collect(Collectors.toList());
List<String> storedPermissions = query(WebUserQueries.fetchAvailablePermissions());

View File

@ -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: "查看在线活动图表"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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</label>"
makeAnother: "Faire une autre Requête"
servers:

View File

@ -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"

View File

@ -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: "オンラインアクティビティグラフを表示"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@ -95,7 +95,6 @@ class AccessControlTest {
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 @@ class AccessControlTest {
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 @@ class AccessControlTest {
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),

View File

@ -141,8 +141,7 @@ class AccessControlVisibilityTest {
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 @@ class AccessControlVisibilityTest {
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"),

View File

@ -264,13 +264,5 @@ class PlayerLeaveEventConsumerTest {
List<String> expected = List.of("PLAY.UPPERCASE.COM", "play.uppercase.com", JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
List<String> result = database.query(JoinAddressQueries.allJoinAddresses());
assertEquals(expected, result);
Map<String, Integer> expectedMap = Map.of("PLAY.UPPERCASE.COM", 1);
Map<String, Integer> resultMap = database.query(JoinAddressQueries.latestJoinAddresses(serverUUID));
assertEquals(expectedMap, resultMap);
expectedMap = Map.of("PLAY.UPPERCASE.COM", 1);
resultMap = database.query(JoinAddressQueries.latestJoinAddresses());
assertEquals(expectedMap, resultMap);
}
}

View File

@ -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.sql.tables.JoinAddressTable;
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 @@ public interface JoinAddressQueriesTest extends DatabaseTestPreparer {
}
}
@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<DateObj<Map<String, Integer>>> 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<DateObj<Map<String, Integer>>> result2 = db().query(JoinAddressQueries.joinAddressesPerDay(0, 0, System.currentTimeMillis(), List.of(expectedAddress)));
assertEquals(List.of(new DateObj<>(startOfDay, Map.of(expectedAddress, 1))), result2);
List<DateObj<Map<String, Integer>>> 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<DateObj<Map<String, Integer>>> 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<DateObj<Map<String, Integer>>> 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<DateObj<Map<String, Integer>>> 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 @@ public interface JoinAddressQueriesTest extends DatabaseTestPreparer {
assertEquals(expected, result);
}
@Test
default void serverJoinAddressQueryHasNoNullValues() {
joinAddressCanBeUnknown();
Map<String, Integer> expected = Collections.singletonMap(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP, 1);
Map<String, Integer> result = db().query(JoinAddressQueries.latestJoinAddresses(serverUUID()));
assertEquals(expected, result);
}
@Test
default void joinAddressQueryHasDistinctPlayers() {
joinAddressCanBeUnknown();

View File

@ -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 (
<Card>
<CardHeader icon={faList} color={"amber"} label={
editingName ?
<Form.Control
style={{position: "absolute", top: "0.5rem", left: "2.5rem", width: "calc(100% - 3rem)"}}
value={name}
onChange={e => setName(e.target.value)}/> : group.name
}>
<button
style={editingName ? {position: "absolute", right: "1rem", top: "1.2rem"} : {marginLeft: "0.5rem"}}
onClick={() => setEditingName(!editingName)}>
<FontAwesomeIcon icon={editingName ? faCheck : faPencil}/>
</button>
</CardHeader>
<Card.Body>
<MultiSelect options={allAddresses} selectedIndexes={selectedIndexes}
setSelectedIndexes={setSelectedIndexes}/>
<button className={'mt-2 btn ' + (isUpToDate ? 'bg-transparent' : 'bg-theme')}
onClick={applySelected} disabled={isUpToDate}>
{t('html.label.apply')}
</button>
<button className={'mt-2 btn btn-outline-secondary float-end'}
onClick={remove}>
<FontAwesomeIcon icon={faTrashAlt}/>
</button>
</Card.Body>
</Card>
)
}
export default AddressGroupCard;

View File

@ -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 (
<Row id={"address-selector"}>
{list.map((group, i) =>
<Col lg={2} key={group.uuid}>
<AddressGroupCard n={i + 1}
group={group}
editGroup={replacement => replace(replacement, i)}
allAddresses={allAddresses}
remove={() => remove(i)}/>
</Col>)}
<Col lg={2}>
<button className={"btn bg-theme mb-4"} onClick={add}>
<FontAwesomeIcon icon={faPlus}/> {t('html.label.addJoinAddressGroup')}
</button>
</Col>
</Row>
)
}
export default AddressGroupSelectorRow;

View File

@ -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 (
<LoadIn>
{seeTime && <section id={id} className={id}>
<JoinAddressListContextProvider identifier={identifier} isAllowed={seeTime}>
<ExtendableRow id={`row-${id}-0`}>
<Col lg={12}>
<JoinAddressGraphCard identifier={identifier}/>
</Col>
</ExtendableRow>
{!staticSite && <AddressGroupSelectorRow/>}
</JoinAddressListContextProvider>
</section>}
</LoadIn>
)
}
export default JoinAddresses;

View File

@ -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 (
<LoadIn>
{seeRetention && <section id={id} className={id}>
<JoinAddressListContextProvider identifier={identifier} isAllowed={seeRetention}
loadIndividualAddresses>
<ExtendableRow id={`row-${id}-0`}>
<Col lg={12}>
<PlayerRetentionGraphCard identifier={identifier}
selectedGroupBy={selectedGroupBy}
setSelectedGroupBy={setSelectedGroupBy}/>
</Col>
</ExtendableRow>
{selectedGroupBy === 'joinAddress' && <AddressGroupSelectorRow/>}
</JoinAddressListContextProvider>
</section>}
</LoadIn>
)
};
export default PlayerRetention

View File

@ -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} - ' : '') + '<b>{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '</b>'
pointFormat: (selectedGroupBy !== 'none' ? '{series.name} - ' : '') + '<b>{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '</b><br>'
} : {
enabled: true,
shared: series.length <= 10,
valueDecimals: 2,
headerFormat: '{point.x} ' + windowName + '<br>',
pointFormat: (selectedGroupBy !== 'none' ? '{series.name} - ' : '') + '<b>{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '</b>'
pointFormat: (selectedGroupBy !== 'none' ? '{series.name} - ' : '') + '<b>{point.y} ' + (selectedYAxis === 'percentage' ? '%' : t('html.label.players')) + '</b><br>'
},
series: series
})

View File

@ -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;
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 <ErrorViewCard error={loadingError}/>
if (!data) return <CardLoader/>;
return (
<Card>
@ -28,8 +84,12 @@ const JoinAddressGraphCard = ({identifier}) => {
</h6>
<Toggle value={stack} onValueChange={setStack} color={'amber'}>{t('html.label.stacked')}</Toggle>
</Card.Header>
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors}
stack={stack}/>
{data &&
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors}
stack={stack}/>}
{!data && noSelectedAddresses &&
<div className="chart-area" style={{height: "450px"}}><p>Select some addresses</p></div>}
{!data && !noSelectedAddresses && <ChartLoader/>}
</Card>
)
};

View File

@ -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 <ErrorViewCard error={loadingError}/>
if (!data) return <CardLoader/>;
return (
<Card id={'join-address-groups'}>
<Card.Header>
<h6 className="col-black" style={{width: '100%'}}>
<Fa icon={faLocationArrow} className="col-amber"/> {t('html.label.latestJoinAddresses')}
</h6>
</Card.Header>
<GroupVisualizer groups={data.slices} colors={data.colors}/>
</Card>
)
};
export default JoinAddressGroupCard

View File

@ -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 (
<select className="form-control" multiple
<select className={"form-control " + className} multiple
onChange={handleChange}>
{options.map((option, i) => {
return (

View File

@ -58,7 +58,7 @@ const Header = ({page, tab, hideUpdater}) => {
</button>}
{staticSite && <Fa icon={faClockRotateLeft} title={t('html.label.exported')}/>}
{' '}
<span className="refresh-time"><FormattedDate date={lastUpdate.date}/></span>
<span className="refresh-time"><FormattedDate date={lastUpdate.date} react/></span>
</div>
</>}

View File

@ -40,7 +40,7 @@ export function formatDate(date, offset, pattern, recentDays, recentDaysPattern,
return date !== 0 ? new SimpleDateFormat(format).format(timestamp) : '-'
}
const FormattedDate = ({date}) => {
const FormattedDate = ({date, react}) => {
const {t} = useTranslation();
const {pattern, recentDays, recentDaysPattern, offset} = useDatePreferences();
@ -48,6 +48,12 @@ const FormattedDate = ({date}) => {
if (!pattern || date === undefined || date === null) return <></>;
if (!isNumber(date)) return date;
if (react) {
return <span title={formatDate(date, offset, pattern, false, null, t)}>
{formatDate(date, offset, pattern, recentDays, recentDaysPattern, t)}
</span>
}
return formatDate(date, offset, pattern, recentDays, recentDaysPattern, t);
};

View File

@ -0,0 +1,71 @@
import {createContext, useCallback, useContext, useEffect, useMemo, useState} from "react";
import {randomUuid} from "../../util/uuid.js";
import {fetchPlayerJoinAddresses} from "../../service/serverService.js";
import {useNavigation} from "../navigationHook.jsx";
import {usePreferences} from "../preferencesHook.jsx";
import {useTranslation} from "react-i18next";
const JoinAddressListContext = createContext({});
export const JoinAddressListContextProvider = ({identifier, children, loadIndividualAddresses, isAllowed}) => {
const {t} = useTranslation();
const {updateRequested} = useNavigation();
const {preferencesLoaded, getKeyedPreference, setSomePreferences} = usePreferences();
const [list, setList] = useState([]);
const updateList = useCallback(newValue => {
setList(newValue);
const userPreferences = {}
userPreferences["join-addresses-" + identifier] = newValue;
setSomePreferences(userPreferences);
}, [setList])
useEffect(() => {
if (preferencesLoaded && !list.length) {
const value = getKeyedPreference("join-addresses-" + identifier);
if (value?.length) {
setList(value)
}
}
}, [list, setList, preferencesLoaded, getKeyedPreference]);
const add = useCallback(() => {
updateList([...list, {
name: t('html.label.addressGroup').replace("{{n}}", list.length + 1),
addresses: [],
uuid: randomUuid()
}])
}, [updateList, list]);
const remove = useCallback(index => {
updateList(list.filter((f, i) => i !== index));
}, [updateList, list]);
const replace = useCallback((replacement, index) => {
const newList = [...list];
newList[index] = replacement;
updateList(newList)
}, [updateList, list]);
const [allAddresses, setAllAddresses] = useState([]);
const [playerAddresses, setPlayerAddresses] = useState(undefined);
const loadAddresses = useCallback(async () => {
if (!isAllowed) return;
const {data, error} = await fetchPlayerJoinAddresses(updateRequested, identifier, !loadIndividualAddresses);
setAllAddresses(data?.joinAddresses || [error]);
setPlayerAddresses(data?.joinAddressByPlayer);
}, [setAllAddresses, identifier, updateRequested, isAllowed]);
useEffect(() => {
loadAddresses();
}, [loadAddresses]);
const sharedState = useMemo(() => {
return {list, add, remove, replace, allAddresses, playerAddresses};
}, [list, add, remove, replace, allAddresses, playerAddresses]);
return (<JoinAddressListContext.Provider value={sharedState}>
{children}
</JoinAddressListContext.Provider>
)
}
export const useJoinAddressListContext = () => {
return useContext(JoinAddressListContext);
}

View File

@ -33,7 +33,15 @@ export const PreferencesContextProvider = ({children}) => {
} else {
localStorage.setItem("preferences", JSON.stringify(withDefaultsRemoved));
}
}, [defaultPreferences, authRequired, authLoaded, loggedIn]);
setPreferences({...defaultPreferences, ...(withDefaultsRemoved || {})});
}, [defaultPreferences, authRequired, authLoaded, loggedIn, setPreferences]);
const setSomePreferences = useCallback(userPref => {
storePreferences({...preferences, ...userPref});
}, [storePreferences, preferences]);
const getKeyedPreference = useCallback(key => {
return preferences[key];
}, [preferences]);
useEffect(() => {
updatePreferences();
@ -42,12 +50,13 @@ export const PreferencesContextProvider = ({children}) => {
const sharedState = useMemo(() => {
return {
...preferences,
storePreferences,
getKeyedPreference,
setSomePreferences,
defaultPreferences,
preferencesLoaded: Object.keys(defaultPreferences || {}).length > 0
}
},
[preferences, defaultPreferences, storePreferences]);
[preferences, defaultPreferences, storePreferences, getKeyedPreference, setSomePreferences]);
return (<PreferencesContext.Provider value={sharedState}>
{children}
</PreferencesContext.Provider>

View File

@ -261,42 +261,22 @@ export const fetchPingGraph = async (timestamp, identifier) => {
return doGetRequest(url, timestamp);
}
export const fetchJoinAddressPie = async (timestamp, identifier) => {
export const fetchJoinAddressByDay = async (timestamp, addresses, identifier) => {
if (identifier) {
return await fetchJoinAddressPieServer(timestamp, identifier);
return await fetchJoinAddressByDayServer(timestamp, addresses, identifier);
} else {
return await fetchJoinAddressPieNetwork(timestamp);
return await fetchJoinAddressByDayNetwork(timestamp, addresses);
}
}
const fetchJoinAddressPieServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=joinAddressPie&server=${identifier}`;
if (staticSite) url = `/data/graph-joinAddressPie_${identifier}.json`;
return doGetRequest(url, timestamp);
}
const fetchJoinAddressPieNetwork = async (timestamp) => {
let url = `/v1/graph?type=joinAddressPie`;
if (staticSite) url = `/data/graph-joinAddressPie.json`;
return doGetRequest(url, timestamp);
}
export const fetchJoinAddressByDay = async (timestamp, identifier) => {
if (identifier) {
return await fetchJoinAddressByDayServer(timestamp, identifier);
} else {
return await fetchJoinAddressByDayNetwork(timestamp);
}
}
const fetchJoinAddressByDayServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=joinAddressByDay&server=${identifier}`;
const fetchJoinAddressByDayServer = async (timestamp, addresses, identifier) => {
let url = `/v1/graph?type=joinAddressByDay&server=${identifier}&addresses=${addresses.join(',')}`;
if (staticSite) url = `/data/graph-joinAddressByDay_${identifier}.json`;
return doGetRequest(url, timestamp);
}
const fetchJoinAddressByDayNetwork = async (timestamp) => {
let url = `/v1/graph?type=joinAddressByDay`;
const fetchJoinAddressByDayNetwork = async (timestamp, addresses) => {
let url = `/v1/graph?type=joinAddressByDay&addresses=${addresses.join(',')}`;
if (staticSite) url = `/data/graph-joinAddressByDay.json`;
return doGetRequest(url, timestamp);
}
@ -321,22 +301,22 @@ const fetchNetworkRetentionData = async (timestamp) => {
return doGetRequest(url, timestamp);
}
export const fetchPlayerJoinAddresses = async (timestamp, identifier) => {
export const fetchPlayerJoinAddresses = async (timestamp, identifier, justList) => {
if (identifier) {
return await fetchServerPlayerJoinAddresses(timestamp, identifier);
return await fetchServerPlayerJoinAddresses(timestamp, identifier, justList);
} else {
return await fetchNetworkPlayerJoinAddresses(timestamp);
return await fetchNetworkPlayerJoinAddresses(timestamp, justList);
}
}
const fetchServerPlayerJoinAddresses = async (timestamp, identifier) => {
let url = `/v1/joinAddresses?server=${identifier}`;
const fetchServerPlayerJoinAddresses = async (timestamp, identifier, justList) => {
let url = `/v1/joinAddresses?server=${identifier}${justList ? "&listOnly=true" : ""}`;
if (staticSite) url = `/data/joinAddresses-${identifier}.json`;
return doGetRequest(url, timestamp);
}
const fetchNetworkPlayerJoinAddresses = async (timestamp) => {
let url = `/v1/joinAddresses`;
const fetchNetworkPlayerJoinAddresses = async (timestamp, justList) => {
let url = `/v1/joinAddresses${justList ? "?listOnly=true" : ""}`;
if (staticSite) url = `/data/joinAddresses.json`;
return doGetRequest(url, timestamp);
}

View File

@ -0,0 +1,6 @@
// https://stackoverflow.com/a/2117523/20825073
export function randomUuid() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}

View File

@ -1,29 +1,12 @@
import React from 'react';
import {Col} from "react-bootstrap";
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
import LoadIn from "../../components/animation/LoadIn";
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {useAuth} from "../../hooks/authenticationHook";
import JoinAddresses from "../../components/cards/common/JoinAddresses.jsx";
import {useAuth} from "../../hooks/authenticationHook.jsx";
const NetworkJoinAddresses = () => {
const {hasPermission} = useAuth();
const seeTime = hasPermission('page.network.join.addresses.graphs.time');
const seeLatest = hasPermission('page.network.join.addresses.graphs.pie');
return (
<LoadIn>
<section className={"network-join-addresses"}>
<ExtendableRow id={'row-network-join-addresses-0'}>
{seeTime && <Col lg={8}>
<JoinAddressGraphCard identifier={undefined}/>
</Col>}
{seeLatest && <Col lg={4}>
<JoinAddressGroupCard identifier={undefined}/>
</Col>}
</ExtendableRow>
</section>
</LoadIn>
<JoinAddresses id={'network-join-addresses'} identifier={null} seeTime={seeTime}/>
)
};

View File

@ -1,24 +1,12 @@
import React from 'react';
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {Col} from "react-bootstrap";
import LoadIn from "../../components/animation/LoadIn";
import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard";
import {useAuth} from "../../hooks/authenticationHook";
import PlayerRetention from "../../components/cards/common/PlayerRetention.jsx";
const NetworkPlayerRetention = () => {
const {hasPermission} = useAuth();
const seeRetention = hasPermission('page.network.retention');
return (
<LoadIn>
{seeRetention && <section className="network-retention">
<ExtendableRow id={'row-network-retention-0'}>
<Col lg={12}>
<PlayerRetentionGraphCard identifier={null}/>
</Col>
</ExtendableRow>
</section>}
</LoadIn>
<PlayerRetention id={"network-retention"} identifier={null} seeRetention={seeRetention}/>
)
};

View File

@ -1,31 +1,14 @@
import React from 'react';
import {Col} from "react-bootstrap";
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
import {useParams} from "react-router-dom";
import LoadIn from "../../components/animation/LoadIn";
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {useAuth} from "../../hooks/authenticationHook";
import JoinAddresses from "../../components/cards/common/JoinAddresses.jsx";
import {useAuth} from "../../hooks/authenticationHook.jsx";
const ServerJoinAddresses = () => {
const {hasPermission} = useAuth();
const {identifier} = useParams();
const {hasPermission} = useAuth();
const seeTime = hasPermission('page.server.join.addresses.graphs.time');
const seeLatest = hasPermission('page.server.join.addresses.graphs.pie');
return (
<LoadIn>
<section className={"server-join-addresses"}>
<ExtendableRow id={'row-server-join-addresses-0'}>
{seeTime && <Col lg={8}>
<JoinAddressGraphCard identifier={identifier}/>
</Col>}
{seeLatest && <Col lg={4}>
<JoinAddressGroupCard identifier={identifier}/>
</Col>}
</ExtendableRow>
</section>
</LoadIn>
<JoinAddresses id={'server-join-addresses'} identifier={identifier} seeTime={seeTime}/>
)
};

View File

@ -1,10 +1,7 @@
import React from 'react';
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {Col} from "react-bootstrap";
import LoadIn from "../../components/animation/LoadIn";
import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard";
import {useParams} from "react-router-dom";
import {useAuth} from "../../hooks/authenticationHook";
import PlayerRetention from "../../components/cards/common/PlayerRetention.jsx";
const ServerPlayerRetention = () => {
const {hasPermission} = useAuth();
@ -12,15 +9,7 @@ const ServerPlayerRetention = () => {
const seeRetention = hasPermission('page.server.retention');
return (
<LoadIn>
<section className="server-retention">
{seeRetention && <ExtendableRow id={'row-server-retention-0'}>
<Col lg={12}>
<PlayerRetentionGraphCard identifier={identifier}/>
</Col>
</ExtendableRow>}
</section>
</LoadIn>
<PlayerRetention id={"server-retention"} identifier={identifier} seeRetention={seeRetention}/>
)
};