Skip to content

Commit

Permalink
Implement most of the requirements for this feature
Browse files Browse the repository at this point in the history
  • Loading branch information
AuroraLS3 committed Mar 24, 2024
1 parent 530c518 commit 3c83cf6
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 157 deletions.
Original file line number Diff line number Diff line change
@@ -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 +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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;
Expand Down Expand Up @@ -160,14 +161,25 @@ public List<RetentionData> networkPlayerRetentionAsJSONMap() {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,6 +35,7 @@
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;
Expand All @@ -42,7 +44,6 @@

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.Optional;

/**
Expand Down Expand Up @@ -79,7 +80,7 @@ public boolean canAccess(@Untrusted Request request) {
@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 = {
Expand All @@ -105,12 +106,12 @@ private JSONStorage.StoredJSON getStoredJSON(Request request) {
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())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ public List<String> processResults(ResultSet set) throws SQLException {
};
}

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());
Expand All @@ -152,6 +174,16 @@ public static Query<List<String>> uniqueJoinAddresses() {
};
}

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" +
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
48 changes: 48 additions & 0 deletions Plan/react/dashboard/src/components/cards/common/JoinAddresses.jsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
Loading

0 comments on commit 3c83cf6

Please sign in to comment.