Implement most of the requirements for this feature

This commit is contained in:
Aurora Lahtela 2024-03-24 11:12:10 +02:00
parent 530c5186b0
commit 3c83cf6baa
12 changed files with 348 additions and 157 deletions

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

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

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan.delivery.webserver.resolver.json; package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.domain.auth.WebPermission; 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.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.JSONFactory; import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
import com.djrapitops.plan.delivery.web.resolver.MimeType; import com.djrapitops.plan.delivery.web.resolver.MimeType;
@ -34,6 +35,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject; 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.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
@ -42,7 +44,6 @@ import org.jetbrains.annotations.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.util.Collections;
import java.util.Optional; import java.util.Optional;
/** /**
@ -79,7 +80,7 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
@Operation( @Operation(
description = "Get join address information of players for server or network", description = "Get join address information of players for server or network",
responses = { 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") @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 = { parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
@ -105,12 +106,12 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
if (request.getQuery().get("server").isPresent()) { if (request.getQuery().get("server").isPresent()) {
ServerUUID serverUUID = identifiers.getServerUUID(request); ServerUUID serverUUID = identifiers.getServerUUID(request);
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES, serverUUID, 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 // Assume network
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES, 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

@ -142,6 +142,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() { public static Query<List<String>> uniqueJoinAddresses() {
return db -> { return db -> {
List<String> addresses = db.query(allJoinAddresses()); List<String> addresses = db.query(allJoinAddresses());
@ -152,6 +174,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) { public static Query<Set<Integer>> userIdsOfPlayersWithJoinAddresses(@Untrusted List<String> joinAddresses) {
String sql = SELECT + DISTINCT + SessionsTable.USER_ID + String sql = SELECT + DISTINCT + SessionsTable.USER_ID +
FROM + JoinAddressTable.TABLE_NAME + " j" + FROM + JoinAddressTable.TABLE_NAME + " j" +

View File

@ -0,0 +1,53 @@
import {useTranslation} from "react-i18next";
import React, {useCallback, useEffect, useState} from "react";
import {Card, Form} from "react-bootstrap";
import CardHeader from "../CardHeader.jsx";
import {faCheck, faList, faPencil} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import MultiSelect from "../../input/MultiSelect.jsx";
import {faTrashAlt} from "@fortawesome/free-regular-svg-icons";
const AddressListCard = ({n, group, editGroup, allAddresses, remove}) => {
const {t} = useTranslation();
const [selectedIndexes, setSelectedIndexes] = useState([]);
const [editingName, setEditingName] = useState(false);
const [name, setName] = useState(group.name);
const isUpToDate = group.addresses === allAddresses.filter((a, i) => selectedIndexes.includes(i));
const applySelected = useCallback(() => {
editGroup({...group, addresses: allAddresses.filter((a, i) => selectedIndexes.includes(i))})
}, [editGroup, group, allAddresses, selectedIndexes]);
const editName = useCallback(newName => {
editGroup({...group, name: newName});
}, [editGroup, group]);
useEffect(() => {
if (!editingName && name !== group.name) editName(name);
}, [editName, editingName, name])
return (
<Card>
<CardHeader icon={faList} color={"amber"} label={
editingName ?
<Form.Control style={{maxWidth: "75%", marginTop: "-1rem", marginBottom: "-1rem"}} 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>
)
}
export default AddressListCard;

View File

@ -0,0 +1,48 @@
import LoadIn from "../../animation/LoadIn.jsx";
import ExtendableRow from "../../layout/extension/ExtendableRow.jsx";
import JoinAddressGraphCard from "../server/graphs/JoinAddressGraphCard.jsx";
import {Col, Row} from "react-bootstrap";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faPlus} from "@fortawesome/free-solid-svg-icons";
import React from "react";
import {useAuth} from "../../../hooks/authenticationHook.jsx";
import {useTranslation} from "react-i18next";
import {useJoinAddressListContext} from "../../../hooks/context/joinAddressListContextHook.jsx";
import AddressListCard from "./AddressListCard.jsx";
const JoinAddresses = ({id, permission, identifier}) => {
const {t} = useTranslation();
const {hasPermission} = useAuth();
const {list, add, remove, replace, allAddresses} = useJoinAddressListContext();
const seeTime = hasPermission(permission);
return (
<LoadIn>
<section className={id}>
<ExtendableRow id={`row-${id}-0`}>
<Col lg={12}>
{seeTime && <JoinAddressGraphCard identifier={identifier}/>}
</Col>
</ExtendableRow>
<Row>
{list.map((group, i) =>
<Col lg={2} key={group.uuid}>
<AddressListCard 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}/> Add address group
</button>
</Col>
</Row>
</section>
</LoadIn>
)
}
export default JoinAddresses;

View File

@ -209,7 +209,7 @@ const PlayerRetentionGraphCard = ({identifier}) => {
useEffect(() => { useEffect(() => {
if (!data || !joinAddressData) return; if (!data || !joinAddressData) return;
createSeries(data.player_retention, joinAddressData.join_address_by_player).then(series => setSeries(series.flat())); createSeries(data.player_retention, joinAddressData.joinAddressByPlayer).then(series => setSeries(series.flat()));
}, [data, joinAddressData, createSeries, setSeries]); }, [data, joinAddressData, createSeries, setSeries]);
useEffect(() => { useEffect(() => {

View File

@ -1,23 +1,73 @@
import React, {useState} from 'react'; import React, {useCallback, useEffect, useState} from 'react';
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {useDataRequest} from "../../../../hooks/dataFetchHook";
import {fetchJoinAddressByDay} from "../../../../service/serverService"; import {fetchJoinAddressByDay} from "../../../../service/serverService";
import {ErrorViewCard} from "../../../../views/ErrorView"; import {ErrorViewCard} from "../../../../views/ErrorView";
import {CardLoader} from "../../../navigation/Loader"; import {ChartLoader} from "../../../navigation/Loader";
import {Card} from "react-bootstrap"; import {Card} from "react-bootstrap";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faChartColumn} from "@fortawesome/free-solid-svg-icons"; import {faChartColumn} from "@fortawesome/free-solid-svg-icons";
import JoinAddressGraph from "../../../graphs/JoinAddressGraph"; import JoinAddressGraph from "../../../graphs/JoinAddressGraph";
import Toggle from "../../../input/Toggle"; import Toggle from "../../../input/Toggle";
import {useJoinAddressListContext} from "../../../../hooks/context/joinAddressListContextHook.jsx";
import {useNavigation} from "../../../../hooks/navigationHook.jsx";
const JoinAddressGraphCard = ({id, identifier, addresses}) => { const JoinAddressGraphCard = ({identifier}) => {
const {t} = useTranslation(); const {t} = useTranslation();
const [stack, setStack] = useState(true); const [stack, setStack] = useState(true);
const {updateRequested} = useNavigation();
const {data, loadingError} = useDataRequest(fetchJoinAddressByDay, [addresses || [], identifier]); const {list} = useJoinAddressListContext();
const noSelectedAddresses = !list.filter(group => group.addresses.length).length;
const [data, setData] = useState(undefined);
const [loadingError, setLoadingError] = useState(undefined);
const loadAddresses = useCallback(async () => {
if (noSelectedAddresses) return;
let colors = ['#4ab4de'];
const dataByGroup = [];
for (const group of list) {
const {data, error} = await fetchJoinAddressByDay(updateRequested, group.addresses, identifier);
if (error) {
setLoadingError(error);
return;
}
colors = data?.colors;
dataByGroup.push({...group, data: data?.join_addresses_by_date || []});
}
// First group points from endpoint into frontend based groups
const points = {};
for (const group of dataByGroup) {
const groupName = group.name;
for (const point of group.data || []) {
if (!points[point.date]) points[point.date] = [];
const count = point.joinAddresses.map(j => j.count).reduce((partialSum, a) => partialSum + a, 0);
points[point.date].push({date: point.date, joinAddresses: [{joinAddress: groupName, count}]})
}
}
// expected output: [{date: number, addresses: [{joinAddress: "name", count: number}]}]
const flattened = Object.entries(points)
.sort((a, b) => Number(b.date) - Number(a.date))
.map(([date, pointList]) => {
return {
date: Number(date), joinAddresses: pointList.map(point => point.joinAddresses).flat()
}
});
setData({
join_addresses_by_date: flattened,
colors
});
}, [setData, setLoadingError, identifier, updateRequested, list]);
useEffect(() => {
loadAddresses();
}, [loadAddresses]);
if (loadingError) return <ErrorViewCard error={loadingError}/> if (loadingError) return <ErrorViewCard error={loadingError}/>
if (!data) return <CardLoader/>;
return ( return (
<Card> <Card>
@ -27,8 +77,12 @@ const JoinAddressGraphCard = ({id, identifier, addresses}) => {
</h6> </h6>
<Toggle value={stack} onValueChange={setStack} color={'amber'}>{t('html.label.stacked')}</Toggle> <Toggle value={stack} onValueChange={setStack} color={'amber'}>{t('html.label.stacked')}</Toggle>
</Card.Header> </Card.Header>
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors} {data &&
stack={stack}/> <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> </Card>
) )
}; };

View File

@ -0,0 +1,44 @@
import {createContext, useCallback, useContext, useEffect, useMemo, useState} from "react";
import {randomUuid} from "../../util/uuid.js";
import {fetchPlayerJoinAddresses} from "../../service/serverService.js";
import {useNavigation} from "../navigationHook.jsx";
const JoinAddressListContext = createContext({});
export const JoinAddressListContextProvider = ({identifier, children}) => {
const {updateRequested} = useNavigation();
const [list, setList] = useState([]);
const add = useCallback(() => {
setList([...list, {name: "Address group " + (list.length + 1), addresses: [], uuid: randomUuid()}])
}, [list, setList]);
const remove = useCallback(index => {
setList(list.filter((f, i) => i !== index));
}, [setList, list]);
const replace = useCallback((replacement, index) => {
const newList = [...list];
newList[index] = replacement;
setList(newList)
}, [setList, list]);
const [allAddresses, setAllAddresses] = useState([]);
const loadAddresses = useCallback(async () => {
const {data, error} = await fetchPlayerJoinAddresses(updateRequested, identifier, true);
setAllAddresses(data?.joinAddresses || [error]);
}, [setAllAddresses, identifier, updateRequested]);
useEffect(() => {
loadAddresses();
}, [loadAddresses]);
const sharedState = useMemo(() => {
return {list, add, remove, replace, allAddresses};
}, [list, add, remove, replace]);
return (<JoinAddressListContext.Provider value={sharedState}>
{children}
</JoinAddressListContext.Provider>
)
}
export const useJoinAddressListContext = () => {
return useContext(JoinAddressListContext);
}

View File

@ -321,22 +321,22 @@ const fetchNetworkRetentionData = async (timestamp) => {
return doGetRequest(url, timestamp); return doGetRequest(url, timestamp);
} }
export const fetchPlayerJoinAddresses = async (timestamp, identifier) => { export const fetchPlayerJoinAddresses = async (timestamp, identifier, justList) => {
if (identifier) { if (identifier) {
return await fetchServerPlayerJoinAddresses(timestamp, identifier); return await fetchServerPlayerJoinAddresses(timestamp, identifier, justList);
} else { } else {
return await fetchNetworkPlayerJoinAddresses(timestamp); return await fetchNetworkPlayerJoinAddresses(timestamp, justList);
} }
} }
const fetchServerPlayerJoinAddresses = async (timestamp, identifier) => { const fetchServerPlayerJoinAddresses = async (timestamp, identifier, justList) => {
let url = `/v1/joinAddresses?server=${identifier}`; let url = `/v1/joinAddresses?server=${identifier}${justList ? "&listOnly=true" : ""}`;
if (staticSite) url = `/data/joinAddresses-${identifier}.json`; if (staticSite) url = `/data/joinAddresses-${identifier}.json`;
return doGetRequest(url, timestamp); return doGetRequest(url, timestamp);
} }
const fetchNetworkPlayerJoinAddresses = async (timestamp) => { const fetchNetworkPlayerJoinAddresses = async (timestamp, justList) => {
let url = `/v1/joinAddresses`; let url = `/v1/joinAddresses${justList ? "?listOnly=true" : ""}`;
if (staticSite) url = `/data/joinAddresses.json`; if (staticSite) url = `/data/joinAddresses.json`;
return doGetRequest(url, timestamp); return doGetRequest(url, timestamp);
} }

View File

@ -1,117 +1,13 @@
import React, {useCallback, useEffect, useState} from 'react'; import React from 'react';
import {Card, Col, Form, Row} from "react-bootstrap"; import JoinAddresses from "../../components/cards/common/JoinAddresses.jsx";
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard"; import {JoinAddressListContextProvider} from "../../hooks/context/joinAddressListContextHook.jsx";
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 NetworkJoinAddresses = () => {
const identifier = undefined;
const {hasPermission} = useAuth();
const seeTime = false && hasPermission('page.network.join.addresses.graphs.time');
const seeLatest = hasPermission('page.network.join.addresses.graphs.pie');
// TODO Move to context
const [list, setList] = useState([]);
const add = useCallback(() => {
setList([...list, {name: "Address group " + (list.length + 1), addresses: [], uuid: randomUuid()}])
}, [list, setList]);
const remove = useCallback(index => {
setList(list.filter((f, i) => i !== index));
}, [setList, list]);
const replace = useCallback((replacement, index) => {
const newList = [...list];
newList[index] = replacement;
setList(newList)
}, [setList, list]);
const {
data: joinAddressData,
loadingError: joinAddressLoadingError
} = useDataRequest(fetchPlayerJoinAddresses, [identifier]);
let allAddresses = joinAddressData ? Object.values(joinAddressData.join_address_by_player) : [];
function onlyUnique(value, index, array) {
return array.indexOf(value) === index;
}
allAddresses = allAddresses.filter(onlyUnique);
return ( return (
<LoadIn> <JoinAddressListContextProvider identifier={null}>
<section className={"network-join-addresses"}> <JoinAddresses id={'network-join-addresses'} identifier={null}
<ExtendableRow id={'row-network-join-addresses-0'}> permission={'page.network.join.addresses.graphs.time'}/>
{seeTime && <JoinAddressGraphCard identifier={undefined} addresses={[]}/>} </JoinAddressListContextProvider>
</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>
) )
}; };

View File

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