mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-11-01 00:10:12 +01:00
Implement most of the requirements for this feature
This commit is contained in:
parent
530c5186b0
commit
3c83cf6baa
@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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" +
|
||||||
|
@ -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;
|
@ -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;
|
@ -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(() => {
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user