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.RetentionData;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.PlayerKillMutator;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator;
|
||||
@ -160,14 +161,25 @@ public class JSONFactory {
|
||||
return db.query(PlayerRetentionQueries.fetchRetentionData());
|
||||
}
|
||||
|
||||
public Map<UUID, String> playerJoinAddresses(ServerUUID serverUUID) {
|
||||
public PlayerJoinAddresses playerJoinAddresses(ServerUUID serverUUID, boolean includeByPlayerMap) {
|
||||
Database db = dbSystem.getDatabase();
|
||||
return db.query(JoinAddressQueries.latestJoinAddressesOfPlayers(serverUUID));
|
||||
if (includeByPlayerMap) {
|
||||
Map<UUID, String> addresses = db.query(JoinAddressQueries.latestJoinAddressesOfPlayers(serverUUID));
|
||||
return new PlayerJoinAddresses(
|
||||
addresses.values().stream().distinct().sorted().collect(Collectors.toList()),
|
||||
addresses
|
||||
);
|
||||
} else {
|
||||
return new PlayerJoinAddresses(db.query(JoinAddressQueries.uniqueJoinAddresses(serverUUID)), null);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<UUID, String> playerJoinAddresses() {
|
||||
public PlayerJoinAddresses playerJoinAddresses(boolean includeByPlayerMap) {
|
||||
Database db = dbSystem.getDatabase();
|
||||
return db.query(JoinAddressQueries.latestJoinAddressesOfPlayers());
|
||||
return new PlayerJoinAddresses(
|
||||
db.query(JoinAddressQueries.uniqueJoinAddresses()),
|
||||
includeByPlayerMap ? db.query(JoinAddressQueries.latestJoinAddressesOfPlayers()) : null
|
||||
);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> serverSessionsAsJSONMap(ServerUUID serverUUID) {
|
||||
|
@ -17,6 +17,7 @@
|
||||
package com.djrapitops.plan.delivery.webserver.resolver.json;
|
||||
|
||||
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses;
|
||||
import com.djrapitops.plan.delivery.formatting.Formatter;
|
||||
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
|
||||
import com.djrapitops.plan.delivery.web.resolver.MimeType;
|
||||
@ -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.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import jakarta.ws.rs.GET;
|
||||
@ -42,7 +44,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@ -79,7 +80,7 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
|
||||
@Operation(
|
||||
description = "Get join address information of players for server or network",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON)),
|
||||
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, schema = @Schema(implementation = PlayerJoinAddresses.class))),
|
||||
@ApiResponse(responseCode = "400", description = "If 'server' parameter is not an existing server")
|
||||
},
|
||||
parameters = @Parameter(in = ParameterIn.QUERY, name = "server", description = "Server identifier to get data for (optional)", examples = {
|
||||
@ -105,12 +106,12 @@ public class PlayerJoinAddressJSONResolver extends JSONResolver {
|
||||
if (request.getQuery().get("server").isPresent()) {
|
||||
ServerUUID serverUUID = identifiers.getServerUUID(request);
|
||||
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES, serverUUID,
|
||||
theUUID -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses(theUUID))
|
||||
serverUUID1 -> jsonFactory.playerJoinAddresses(serverUUID1, request.getQuery().get("listOnly").isEmpty())
|
||||
);
|
||||
}
|
||||
// Assume network
|
||||
return jsonResolverService.resolve(timestamp, DataID.PLAYER_JOIN_ADDRESSES,
|
||||
() -> Collections.singletonMap("join_address_by_player", jsonFactory.playerJoinAddresses())
|
||||
() -> jsonFactory.playerJoinAddresses(request.getQuery().get("listOnly").isEmpty())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
return db -> {
|
||||
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) {
|
||||
String sql = SELECT + DISTINCT + SessionsTable.USER_ID +
|
||||
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(() => {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,23 +1,73 @@
|
||||
import React, {useState} from 'react';
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchJoinAddressByDay} from "../../../../service/serverService";
|
||||
import {ErrorViewCard} from "../../../../views/ErrorView";
|
||||
import {CardLoader} from "../../../navigation/Loader";
|
||||
import {ChartLoader} from "../../../navigation/Loader";
|
||||
import {Card} from "react-bootstrap";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faChartColumn} from "@fortawesome/free-solid-svg-icons";
|
||||
import JoinAddressGraph from "../../../graphs/JoinAddressGraph";
|
||||
import Toggle from "../../../input/Toggle";
|
||||
import {useJoinAddressListContext} from "../../../../hooks/context/joinAddressListContextHook.jsx";
|
||||
import {useNavigation} from "../../../../hooks/navigationHook.jsx";
|
||||
|
||||
const JoinAddressGraphCard = ({id, identifier, addresses}) => {
|
||||
const JoinAddressGraphCard = ({identifier}) => {
|
||||
const {t} = useTranslation();
|
||||
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 (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -27,8 +77,12 @@ const JoinAddressGraphCard = ({id, identifier, addresses}) => {
|
||||
</h6>
|
||||
<Toggle value={stack} onValueChange={setStack} color={'amber'}>{t('html.label.stacked')}</Toggle>
|
||||
</Card.Header>
|
||||
{data &&
|
||||
<JoinAddressGraph id={'join-address-graph'} data={data?.join_addresses_by_date} colors={data?.colors}
|
||||
stack={stack}/>
|
||||
stack={stack}/>}
|
||||
{!data && noSelectedAddresses &&
|
||||
<div className="chart-area" style={{height: "450px"}}><p>Select some addresses</p></div>}
|
||||
{!data && !noSelectedAddresses && <ChartLoader/>}
|
||||
</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);
|
||||
}
|
||||
|
||||
export const fetchPlayerJoinAddresses = async (timestamp, identifier) => {
|
||||
export const fetchPlayerJoinAddresses = async (timestamp, identifier, justList) => {
|
||||
if (identifier) {
|
||||
return await fetchServerPlayerJoinAddresses(timestamp, identifier);
|
||||
return await fetchServerPlayerJoinAddresses(timestamp, identifier, justList);
|
||||
} else {
|
||||
return await fetchNetworkPlayerJoinAddresses(timestamp);
|
||||
return await fetchNetworkPlayerJoinAddresses(timestamp, justList);
|
||||
}
|
||||
}
|
||||
|
||||
const fetchServerPlayerJoinAddresses = async (timestamp, identifier) => {
|
||||
let url = `/v1/joinAddresses?server=${identifier}`;
|
||||
const fetchServerPlayerJoinAddresses = async (timestamp, identifier, justList) => {
|
||||
let url = `/v1/joinAddresses?server=${identifier}${justList ? "&listOnly=true" : ""}`;
|
||||
if (staticSite) url = `/data/joinAddresses-${identifier}.json`;
|
||||
return doGetRequest(url, timestamp);
|
||||
}
|
||||
|
||||
const fetchNetworkPlayerJoinAddresses = async (timestamp) => {
|
||||
let url = `/v1/joinAddresses`;
|
||||
const fetchNetworkPlayerJoinAddresses = async (timestamp, justList) => {
|
||||
let url = `/v1/joinAddresses${justList ? "?listOnly=true" : ""}`;
|
||||
if (staticSite) url = `/data/joinAddresses.json`;
|
||||
return doGetRequest(url, timestamp);
|
||||
}
|
||||
|
@ -1,117 +1,13 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
import React from 'react';
|
||||
import JoinAddresses from "../../components/cards/common/JoinAddresses.jsx";
|
||||
import {JoinAddressListContextProvider} from "../../hooks/context/joinAddressListContextHook.jsx";
|
||||
|
||||
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 (
|
||||
<LoadIn>
|
||||
<section className={"network-join-addresses"}>
|
||||
<ExtendableRow id={'row-network-join-addresses-0'}>
|
||||
{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>
|
||||
<JoinAddressListContextProvider identifier={null}>
|
||||
<JoinAddresses id={'network-join-addresses'} identifier={null}
|
||||
permission={'page.network.join.addresses.graphs.time'}/>
|
||||
</JoinAddressListContextProvider>
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -1,31 +1,15 @@
|
||||
import React from 'react';
|
||||
import {Col} from "react-bootstrap";
|
||||
import JoinAddressGroupCard from "../../components/cards/server/graphs/JoinAddressGroupCard";
|
||||
import JoinAddressGraphCard from "../../components/cards/server/graphs/JoinAddressGraphCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
import LoadIn from "../../components/animation/LoadIn";
|
||||
import ExtendableRow from "../../components/layout/extension/ExtendableRow";
|
||||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
import {JoinAddressListContextProvider} from "../../hooks/context/joinAddressListContextHook.jsx";
|
||||
import JoinAddresses from "../../components/cards/common/JoinAddresses.jsx";
|
||||
|
||||
const ServerJoinAddresses = () => {
|
||||
const {hasPermission} = useAuth();
|
||||
const {identifier} = useParams();
|
||||
|
||||
const seeTime = hasPermission('page.server.join.addresses.graphs.time');
|
||||
const seeLatest = hasPermission('page.server.join.addresses.graphs.pie');
|
||||
return (
|
||||
<LoadIn>
|
||||
<section className={"server-join-addresses"}>
|
||||
<ExtendableRow id={'row-server-join-addresses-0'}>
|
||||
{seeTime && <Col lg={8}>
|
||||
<JoinAddressGraphCard identifier={identifier}/>
|
||||
</Col>}
|
||||
{seeLatest && <Col lg={4}>
|
||||
<JoinAddressGroupCard identifier={identifier}/>
|
||||
</Col>}
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
<JoinAddressListContextProvider identifier={identifier}>
|
||||
<JoinAddresses id={'server-join-addresses'} identifier={identifier}
|
||||
permission={'page.server.join.addresses.graphs.time'}/>
|
||||
</JoinAddressListContextProvider>
|
||||
)
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user