Started work on #3268
TODO: - Change the graph endpoint to accept multiple groups of addresses, or make multiple requests on frontend - Add endpoint for getting list of join addresses - Make the join address graph show the groups of addresses - Clean up the code in the frontend big time
This commit is contained in:
parent
6d9494d680
commit
530c5186b0
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ import com.djrapitops.plan.storage.database.queries.objects.*;
|
|||
import com.djrapitops.plan.storage.database.sql.tables.JoinAddressTable;
|
||||
import com.djrapitops.plan.utilities.comparators.DateHolderOldestComparator;
|
||||
import com.djrapitops.plan.utilities.comparators.PieSliceComparator;
|
||||
import com.djrapitops.plan.utilities.dev.Untrusted;
|
||||
import com.djrapitops.plan.utilities.java.Lists;
|
||||
import com.djrapitops.plan.utilities.java.Maps;
|
||||
import net.playeranalytics.plugin.scheduling.TimeAmount;
|
||||
|
@ -493,16 +494,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);
|
||||
}
|
||||
|
|
|
@ -54,12 +54,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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -156,15 +161,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;
|
||||
}
|
||||
|
@ -294,19 +306,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:
|
||||
|
@ -326,16 +343,21 @@ public class GraphsJSONResolver extends JSONResolver {
|
|||
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)");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ 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 java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
@ -162,21 +163,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 +200,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 +213,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 +259,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()
|
||||
|
|
|
@ -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]));
|
||||
|
|
|
@ -10,16 +10,15 @@ import {faChartColumn} from "@fortawesome/free-solid-svg-icons";
|
|||
import JoinAddressGraph from "../../../graphs/JoinAddressGraph";
|
||||
import Toggle from "../../../input/Toggle";
|
||||
|
||||
const JoinAddressGraphCard = ({identifier}) => {
|
||||
const JoinAddressGraphCard = ({id, identifier, addresses}) => {
|
||||
const {t} = useTranslation();
|
||||
const [stack, setStack] = useState(true);
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchJoinAddressByDay, [identifier]);
|
||||
const {data, loadingError} = useDataRequest(fetchJoinAddressByDay, [addresses || [], identifier]);
|
||||
|
||||
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -281,22 +281,22 @@ const fetchJoinAddressPieNetwork = async (timestamp) => {
|
|||
return doGetRequest(url, timestamp);
|
||||
}
|
||||
|
||||
export const fetchJoinAddressByDay = async (timestamp, identifier) => {
|
||||
export const fetchJoinAddressByDay = async (timestamp, addresses, identifier) => {
|
||||
if (identifier) {
|
||||
return await fetchJoinAddressByDayServer(timestamp, identifier);
|
||||
return await fetchJoinAddressByDayServer(timestamp, addresses, identifier);
|
||||
} else {
|
||||
return await fetchJoinAddressByDayNetwork(timestamp);
|
||||
return await fetchJoinAddressByDayNetwork(timestamp, addresses);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
|
@ -1,27 +1,115 @@
|
|||
import React from 'react';
|
||||
import {Col} from "react-bootstrap";
|
||||
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {Card, Col, Form, Row} from "react-bootstrap";
|
||||
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCheck, faList, faPencil, faPlus} from "@fortawesome/free-solid-svg-icons";
|
||||
import MultiSelect from "../../components/input/MultiSelect.jsx";
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook.js";
|
||||
import {fetchPlayerJoinAddresses} from "../../service/serverService.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {faTrashAlt} from "@fortawesome/free-regular-svg-icons";
|
||||
import CardHeader from "../../components/cards/CardHeader.jsx";
|
||||
import {randomUuid} from "../../util/uuid.js";
|
||||
|
||||
const AddressListCard = ({n, group, editGroup, allAddresses, remove}) => {
|
||||
const {t} = useTranslation();
|
||||
const [selectedIndexes, setSelectedIndexes] = useState([]);
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const [name, setName] = useState(group.name);
|
||||
|
||||
const isUpToDate = group.addresses === allAddresses.filter((a, i) => selectedIndexes.includes(i));
|
||||
const applySelected = useCallback(() => {
|
||||
editGroup({...group, addresses: allAddresses.filter((a, i) => selectedIndexes.includes(i))})
|
||||
}, [editGroup, group, allAddresses, selectedIndexes]);
|
||||
const editName = useCallback(newName => {
|
||||
editGroup({...group, name: newName});
|
||||
}, [editGroup, group]);
|
||||
useEffect(() => {
|
||||
if (!editingName && name !== group.name) editName(name);
|
||||
}, [editName, editingName, name])
|
||||
|
||||
return (
|
||||
<Col lg={2}>
|
||||
<Card>
|
||||
<CardHeader icon={faList} color={"amber"} label={
|
||||
editingName ? <Form.Control value={name} onChange={e => setName(e.target.value)}/> : group.name
|
||||
}>
|
||||
<button style={{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>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
const NetworkJoinAddresses = () => {
|
||||
const identifier = undefined;
|
||||
const {hasPermission} = useAuth();
|
||||
|
||||
const seeTime = hasPermission('page.network.join.addresses.graphs.time');
|
||||
const seeTime = false && hasPermission('page.network.join.addresses.graphs.time');
|
||||
const seeLatest = hasPermission('page.network.join.addresses.graphs.pie');
|
||||
|
||||
// TODO Move to context
|
||||
const [list, setList] = useState([]);
|
||||
const add = useCallback(() => {
|
||||
setList([...list, {name: "Address group " + (list.length + 1), addresses: [], uuid: randomUuid()}])
|
||||
}, [list, setList]);
|
||||
const remove = useCallback(index => {
|
||||
setList(list.filter((f, i) => i !== index));
|
||||
}, [setList, list]);
|
||||
const replace = useCallback((replacement, index) => {
|
||||
const newList = [...list];
|
||||
newList[index] = replacement;
|
||||
setList(newList)
|
||||
}, [setList, list]);
|
||||
|
||||
const {
|
||||
data: joinAddressData,
|
||||
loadingError: joinAddressLoadingError
|
||||
} = useDataRequest(fetchPlayerJoinAddresses, [identifier]);
|
||||
|
||||
let allAddresses = joinAddressData ? Object.values(joinAddressData.join_address_by_player) : [];
|
||||
|
||||
function onlyUnique(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
allAddresses = allAddresses.filter(onlyUnique);
|
||||
return (
|
||||
<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>}
|
||||
{seeTime && <JoinAddressGraphCard identifier={undefined} addresses={[]}/>}
|
||||
</ExtendableRow>
|
||||
<Row>
|
||||
{list.map((group, i) =>
|
||||
<AddressListCard key={group.uuid} n={i + 1}
|
||||
group={group}
|
||||
editGroup={replacement => replace(replacement, i)}
|
||||
allAddresses={allAddresses}
|
||||
remove={() => remove(i)}/>)}
|
||||
<Col lg={2}>
|
||||
<button className={"btn bg-theme mb-4"} onClick={add}>
|
||||
<FontAwesomeIcon icon={faPlus}/> Add address group
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue