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:
Aurora Lahtela 2024-03-23 14:21:42 +02:00
parent 6d9494d680
commit 530c5186b0
12 changed files with 294 additions and 60 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

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

View File

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

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.
@ -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)");
}
}
}

View File

@ -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()

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]));

View File

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

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

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

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