Skip to content

Commit e858478

Browse files
committed
Add Players online tooltip to player online graphs
After 500ms when hovering/clicking on the players online chart you can see who was online up to 63 players. Further improvement would be able to click in order to see more than 63, but that will need 'Click to see' components Affects issues: - Close #4315 - Close #2294
1 parent 5101a50 commit e858478

16 files changed

Lines changed: 352 additions & 48 deletions

File tree

Plan/common/src/main/java/com/djrapitops/plan/delivery/domain/PlayerIdentifier.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
*/
1717
package com.djrapitops.plan.delivery.domain;
1818

19+
import org.jspecify.annotations.NonNull;
20+
1921
import java.util.Objects;
2022
import java.util.UUID;
2123

22-
public class PlayerIdentifier {
24+
public class PlayerIdentifier implements Comparable<PlayerIdentifier> {
2325
private final UUID uuid;
2426
private final String name;
2527

@@ -64,4 +66,9 @@ public String toString() {
6466
public String toJson() {
6567
return "{\"name\": \"" + name + "\", \"uuid\": \"" + uuid + "\"}";
6668
}
69+
70+
@Override
71+
public int compareTo(@NonNull PlayerIdentifier o) {
72+
return String.CASE_INSENSITIVE_ORDER.compare(name, o.name);
73+
}
6774
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/JSONFactory.java

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.djrapitops.plan.delivery.rendering.json;
1818

1919
import com.djrapitops.plan.delivery.domain.DateObj;
20+
import com.djrapitops.plan.delivery.domain.PlayerIdentifier;
21+
import com.djrapitops.plan.delivery.domain.PlayerName;
2022
import com.djrapitops.plan.delivery.domain.RetentionData;
2123
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerJoinAddresses;
2224
import com.djrapitops.plan.delivery.domain.datatransfer.ServerDto;
@@ -97,6 +99,23 @@ public JSONFactory(
9799
this.formatters = formatters;
98100
}
99101

102+
private static void removeFiltered(Map<UUID, String> addressByPlayerUUID, List<Pattern> filteredJoinAddresses) {
103+
if (filteredJoinAddresses.isEmpty() || filteredJoinAddresses.equals(List.of(Pattern.compile("play\\.example\\.com")))) {
104+
return;
105+
}
106+
107+
Set<UUID> toRemove = new HashSet<>();
108+
// Remove filtered addresses from the data
109+
for (Map.Entry<UUID, String> entry : addressByPlayerUUID.entrySet()) {
110+
if (filteredJoinAddresses.stream().anyMatch(pattern -> pattern.matcher(entry.getValue()).matches())) {
111+
toRemove.add(entry.getKey());
112+
}
113+
}
114+
for (UUID playerUUID : toRemove) {
115+
addressByPlayerUUID.put(playerUUID, JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
116+
}
117+
}
118+
100119
public PlayersTableJSONCreator serverPlayersTableJSON(ServerUUID serverUUID) {
101120
Integer xMostRecentPlayers = config.get(DisplaySettings.PLAYERS_PER_SERVER_PAGE);
102121
Long playtimeThreshold = config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD);
@@ -161,21 +180,6 @@ public List<RetentionData> networkPlayerRetentionAsJSONMap() {
161180
return db.query(PlayerRetentionQueries.fetchRetentionData());
162181
}
163182

164-
private static void removeFiltered(Map<UUID, String> addressByPlayerUUID, List<Pattern> filteredJoinAddresses) {
165-
if (filteredJoinAddresses.isEmpty() || filteredJoinAddresses.equals(List.of(Pattern.compile("play\\.example\\.com")))) return;
166-
167-
Set<UUID> toRemove = new HashSet<>();
168-
// Remove filtered addresses from the data
169-
for (Map.Entry<UUID, String> entry : addressByPlayerUUID.entrySet()) {
170-
if (filteredJoinAddresses.stream().anyMatch(pattern -> pattern.matcher(entry.getValue()).matches())) {
171-
toRemove.add(entry.getKey());
172-
}
173-
}
174-
for (UUID playerUUID : toRemove) {
175-
addressByPlayerUUID.put(playerUUID, JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
176-
}
177-
}
178-
179183
public PlayerJoinAddresses playerJoinAddresses(ServerUUID serverUUID, boolean includeByPlayerMap) {
180184
Database db = dbSystem.getDatabase();
181185
List<Pattern> filteredJoinAddresses = Lists.map(config.get(DataGatheringSettings.FILTER_JOIN_ADDRESSES), Pattern::compile);
@@ -375,4 +379,28 @@ public Map<String, List<ServerDto>> listServers() {
375379
.map(ServerDto::fromServer)
376380
.collect(Collectors.toList()));
377381
}
382+
383+
public List<PlayerIdentifier> playersOnlineOn(long date) {
384+
Set<PlayerIdentifier> online = new HashSet<>(dbSystem.getDatabase().query(SessionQueries.playersOnlineOn(date)));
385+
SessionCache.getActiveSessions().stream()
386+
.filter(session -> session.getStart() <= date)
387+
.map(session -> new PlayerIdentifier(session.getPlayerUUID(), session.getExtraData(PlayerName.class).map(PlayerName::get).orElse(session.getPlayerUUID().toString())))
388+
.forEach(online::add);
389+
return online.stream()
390+
.sorted()
391+
.collect(Collectors.toList());
392+
}
393+
394+
public List<PlayerIdentifier> playersOnlineOn(long date, ServerUUID serverUUID) {
395+
Set<PlayerIdentifier> online = new HashSet<>(dbSystem.getDatabase().query(SessionQueries.playersOnlineOn(date, serverUUID)));
396+
if (serverUUID.equals(serverInfo.getServerUUID())) {
397+
SessionCache.getActiveSessions().stream()
398+
.filter(session -> session.getStart() <= date)
399+
.map(session -> new PlayerIdentifier(session.getPlayerUUID(), session.getExtraData(PlayerName.class).map(PlayerName::get).orElse(session.getPlayerUUID().toString())))
400+
.forEach(online::add);
401+
}
402+
return online.stream()
403+
.sorted()
404+
.collect(Collectors.toList());
405+
}
378406
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/InternalRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,5 @@ default Optional<Authentication> getAuthentication(WebserverConfiguration webser
8282
return authenticationExtractor.extractAuthentication(this);
8383
}
8484

85-
String getRequestedPath();
85+
String getRequestedPathAndQuery();
8686
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/JettyInternalRequest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ public String getRequestedURIString() {
130130
}
131131

132132
@Override
133-
public String getRequestedPath() {
134-
return baseRequest.getHttpURI().getDecodedPath();
133+
public String getRequestedPathAndQuery() {
134+
return baseRequest.getHttpURI().getDecodedPath() + baseRequest.getHttpURI().getQuery();
135135
}
136136

137137
@Override

Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/http/RequestHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public RequestHandler(WebserverConfiguration webserverConfiguration, ResponseFac
5757

5858
public Response getResponse(InternalRequest internalRequest) {
5959
@Untrusted(reason = "from header") String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
60-
@Untrusted String requestedPath = internalRequest.getRequestedPath();
60+
@Untrusted String requestedPath = internalRequest.getRequestedPathAndQuery();
6161

6262
boolean blocked = false;
6363
Response response;
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* This file is part of Player Analytics (Plan).
3+
*
4+
* Plan is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Lesser General Public License v3 as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* Plan is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package com.djrapitops.plan.delivery.webserver.resolver.json;
18+
19+
import com.djrapitops.plan.delivery.domain.PlayerIdentifier;
20+
import com.djrapitops.plan.delivery.domain.auth.WebPermission;
21+
import com.djrapitops.plan.delivery.domain.datatransfer.ThemeDto;
22+
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
23+
import com.djrapitops.plan.delivery.web.resolver.MimeType;
24+
import com.djrapitops.plan.delivery.web.resolver.Resolver;
25+
import com.djrapitops.plan.delivery.web.resolver.Response;
26+
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
27+
import com.djrapitops.plan.delivery.web.resolver.request.Request;
28+
import com.djrapitops.plan.delivery.web.resolver.request.URIQuery;
29+
import com.djrapitops.plan.identification.Identifiers;
30+
import com.djrapitops.plan.identification.ServerUUID;
31+
import io.swagger.v3.oas.annotations.Operation;
32+
import io.swagger.v3.oas.annotations.Parameter;
33+
import io.swagger.v3.oas.annotations.enums.ParameterIn;
34+
import io.swagger.v3.oas.annotations.media.Content;
35+
import io.swagger.v3.oas.annotations.media.ExampleObject;
36+
import io.swagger.v3.oas.annotations.media.Schema;
37+
import io.swagger.v3.oas.annotations.parameters.RequestBody;
38+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
39+
import jakarta.ws.rs.GET;
40+
import jakarta.ws.rs.Path;
41+
42+
import javax.inject.Inject;
43+
import javax.inject.Singleton;
44+
import java.util.List;
45+
import java.util.Optional;
46+
47+
/**
48+
* Endpoint for getting players online at specific date
49+
*
50+
* @author AuroraLS3
51+
*/
52+
@Singleton
53+
@Path("/v1/playersOnline")
54+
public class PlayersOnlineJSONResolver implements Resolver {
55+
56+
private final Identifiers identifiers;
57+
private final JSONFactory jsonFactory;
58+
59+
@Inject
60+
public PlayersOnlineJSONResolver(Identifiers identifiers, JSONFactory jsonFactory) {
61+
62+
this.identifiers = identifiers;
63+
this.jsonFactory = jsonFactory;
64+
}
65+
66+
@Override
67+
public boolean canAccess(Request request) {
68+
boolean forServer = request.getQuery().get("server").isPresent();
69+
WebPermission permission = forServer ? WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH
70+
: WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE;
71+
72+
return request.getUser().map(user -> user.hasPermission(permission)).orElse(false);
73+
}
74+
75+
@GET
76+
@Operation(
77+
description = "Get theme json for a name",
78+
responses = {
79+
@ApiResponse(responseCode = "200", content = @Content(mediaType = MimeType.JSON, schema = @Schema(implementation = ThemeDto.class))),
80+
@ApiResponse(responseCode = "400", description = "If 'date' parameter is not specified or invalid")
81+
},
82+
parameters = {
83+
@Parameter(in = ParameterIn.QUERY, name = "date", description = "Epoch millisecond", required = true),
84+
@Parameter(in = ParameterIn.QUERY, name = "server", description = "Server UUID")
85+
},
86+
requestBody = @RequestBody(content = @Content(examples = @ExampleObject()))
87+
)
88+
@Override
89+
public Optional<Response> resolve(Request request) {
90+
return Optional.of(Response.builder()
91+
.setJSONContent(getResponse(request))
92+
.build());
93+
}
94+
95+
private List<PlayerIdentifier> getResponse(Request request) {
96+
URIQuery query = request.getQuery();
97+
try {
98+
Long date = query.get("date").map(Long::parseLong)
99+
.orElseThrow(() -> new BadRequestException("Missing date"));
100+
if (query.get("server").isPresent()) {
101+
ServerUUID serverUUID = identifiers.getServerUUID(request);
102+
return jsonFactory.playersOnlineOn(date, serverUUID);
103+
} else {
104+
return jsonFactory.playersOnlineOn(date);
105+
}
106+
} catch (NumberFormatException e) {
107+
throw new BadRequestException("Invalid 'date'");
108+
}
109+
}
110+
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/RootJSONResolver.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public RootJSONResolver(
9696
PlayerJoinAddressJSONResolver playerJoinAddressJSONResolver,
9797
PluginHistoryJSONResolver pluginHistoryJSONResolver,
9898
AllowlistJSONResolver allowlistJSONResolver,
99+
PlayersOnlineJSONResolver playersOnlineJSONResolver,
99100

100101
ThemeJSONResolver themeJSONResolver,
101102
SaveThemeJSONResolver saveThemeJSONResolver,
@@ -142,7 +143,8 @@ public RootJSONResolver(
142143
.add("joinAddresses", playerJoinAddressJSONResolver)
143144
.add("preferences", preferencesJSONResolver)
144145
.add("gameAllowlistBounces", allowlistJSONResolver)
145-
.add("theme", themeJSONResolver);
146+
.add("theme", themeJSONResolver)
147+
.add("playersOnline", playersOnlineJSONResolver);
146148

147149
this.webServer = webServer;
148150
// These endpoints require authentication to be enabled.

Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/SessionQueries.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616
*/
1717
package com.djrapitops.plan.storage.database.queries.objects;
1818

19-
import com.djrapitops.plan.delivery.domain.DateHolder;
20-
import com.djrapitops.plan.delivery.domain.PlayerName;
21-
import com.djrapitops.plan.delivery.domain.ServerIdentifier;
22-
import com.djrapitops.plan.delivery.domain.ServerName;
19+
import com.djrapitops.plan.delivery.domain.*;
2320
import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator;
2421
import com.djrapitops.plan.gathering.domain.*;
2522
import com.djrapitops.plan.gathering.domain.event.JoinAddress;
@@ -1062,4 +1059,35 @@ public static Query<List<SessionsTable.Row>> fetchRows(int currentId, int rowLim
10621059
.toString();
10631060
return db -> db.queryList(sql, SessionsTable.Row::extract);
10641061
}
1062+
1063+
public static Query<List<PlayerIdentifier>> playersOnlineOn(long date) {
1064+
String sql = SELECT + DISTINCT + UsersTable.USER_UUID + ',' + UsersTable.USER_NAME +
1065+
FROM + SessionsTable.TABLE_NAME + " s" +
1066+
INNER_JOIN + UsersTable.TABLE_NAME + " u ON u." + UsersTable.ID + "=s." + SessionsTable.USER_ID +
1067+
WHERE + SessionsTable.SESSION_START + "<?" +
1068+
AND + SessionsTable.SESSION_END + ">?";
1069+
return db -> db.queryList(sql,
1070+
row -> new PlayerIdentifier(
1071+
UUID.fromString(row.getString(UsersTable.USER_UUID)),
1072+
row.getString(UsersTable.USER_NAME)
1073+
), date, date);
1074+
}
1075+
1076+
public static Query<List<PlayerIdentifier>> playersOnlineOn(long date, ServerUUID serverUUID) {
1077+
String sql = SELECT + DISTINCT + UsersTable.USER_UUID + ',' + UsersTable.USER_NAME +
1078+
FROM + SessionsTable.TABLE_NAME + " s" +
1079+
INNER_JOIN + UsersTable.TABLE_NAME + " u ON u." + UsersTable.ID + "=s." + SessionsTable.USER_ID +
1080+
WHERE + SessionsTable.SERVER_ID + "=" + ServerTable.SELECT_SERVER_ID +
1081+
AND + SessionsTable.SESSION_START + "<?" +
1082+
AND + SessionsTable.SESSION_END + ">?";
1083+
return db -> db.queryList(sql,
1084+
row -> new PlayerIdentifier(
1085+
UUID.fromString(row.getString(UsersTable.USER_UUID)),
1086+
row.getString(UsersTable.USER_NAME)
1087+
), serverUUID, date, date);
1088+
1089+
// Start Sat Feb 14 2026 14:57:57.247
1090+
// Sat Feb 14 2026 15:10:00.299
1091+
// Sat Feb 14 2026 15:01:35.661
1092+
}
10651093
}

Plan/react/dashboard/src/components/cards/server/graphs/NetworkOnlineActivityGraphsCard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const SingleProxyPlayersOnlineGraph = ({serverUUID}) => {
3131
if (loadingError) return <ErrorViewBody error={loadingError}/>
3232
if (!serverUUID || !data) return <ChartLoader/>;
3333

34-
return <PlayersOnlineGraph data={data}/>
34+
return <PlayersOnlineGraph data={data} showPlayersOnline/>
3535
}
3636

3737
const MultiProxyPlayersOnlineGraph = () => {

Plan/react/dashboard/src/components/cards/server/graphs/OnlineActivityCard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const OnlineActivityCard = () => {
2828
<Fa className="col-players-online" icon={faChartArea}/> {t('html.label.onlineActivity')}
2929
</h6>
3030
</Card.Header>
31-
<PlayersOnlineGraph data={data}/>
31+
<PlayersOnlineGraph data={data} identifier={identifier} showPlayersOnline/>
3232
</Card>
3333
)
3434
}

0 commit comments

Comments
 (0)