mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-11-27 05:01:43 +08:00
Use real data
This commit is contained in:
parent
482f43a8c6
commit
9c7d3007f8
@ -24,8 +24,10 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -41,6 +43,7 @@ public class ViewDto {
|
||||
private final String beforeDate;
|
||||
private final String beforeTime;
|
||||
private final List<ServerDto> servers;
|
||||
private final Set<String> wantedData;
|
||||
|
||||
public ViewDto(Formatters formatters, List<ServerDto> servers) {
|
||||
this.servers = servers;
|
||||
@ -55,6 +58,8 @@ public class ViewDto {
|
||||
this.afterTime = after[1];
|
||||
this.beforeDate = before[0];
|
||||
this.beforeTime = before[1];
|
||||
|
||||
this.wantedData = new HashSet<>();
|
||||
}
|
||||
|
||||
public long getAfterEpochMs() throws ParseException {
|
||||
@ -72,6 +77,10 @@ public class ViewDto {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isWanted(String key) {
|
||||
return wantedData == null || wantedData.isEmpty() || wantedData.contains(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -22,10 +22,12 @@ import com.djrapitops.plan.delivery.domain.datatransfer.InputFilterDto;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.InputQueryDto;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerListDto;
|
||||
import com.djrapitops.plan.delivery.domain.datatransfer.ViewDto;
|
||||
import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator;
|
||||
import com.djrapitops.plan.delivery.formatting.Formatter;
|
||||
import com.djrapitops.plan.delivery.formatting.Formatters;
|
||||
import com.djrapitops.plan.delivery.rendering.json.PlayersTableJSONCreator;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.GraphJSONCreator;
|
||||
import com.djrapitops.plan.delivery.rendering.json.graphs.Graphs;
|
||||
import com.djrapitops.plan.delivery.web.resolver.MimeType;
|
||||
import com.djrapitops.plan.delivery.web.resolver.Resolver;
|
||||
import com.djrapitops.plan.delivery.web.resolver.Response;
|
||||
@ -35,6 +37,7 @@ import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
|
||||
import com.djrapitops.plan.delivery.webserver.RequestBodyConverter;
|
||||
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
|
||||
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionQueryResultTableDataQuery;
|
||||
import com.djrapitops.plan.gathering.domain.FinishedSession;
|
||||
import com.djrapitops.plan.identification.ServerInfo;
|
||||
import com.djrapitops.plan.identification.ServerUUID;
|
||||
import com.djrapitops.plan.settings.config.PlanConfig;
|
||||
@ -81,6 +84,7 @@ public class QueryJSONResolver implements Resolver {
|
||||
private final DBSystem dbSystem;
|
||||
private final ServerInfo serverInfo;
|
||||
private final JSONStorage jsonStorage;
|
||||
private final Graphs graphs;
|
||||
private final GraphJSONCreator graphJSONCreator;
|
||||
private final Locale locale;
|
||||
private final Formatters formatters;
|
||||
@ -91,7 +95,9 @@ public class QueryJSONResolver implements Resolver {
|
||||
QueryFilters filters,
|
||||
PlanConfig config,
|
||||
DBSystem dbSystem,
|
||||
ServerInfo serverInfo, JSONStorage jsonStorage,
|
||||
ServerInfo serverInfo,
|
||||
JSONStorage jsonStorage,
|
||||
Graphs graphs,
|
||||
GraphJSONCreator graphJSONCreator,
|
||||
Locale locale,
|
||||
Formatters formatters,
|
||||
@ -102,6 +108,7 @@ public class QueryJSONResolver implements Resolver {
|
||||
this.dbSystem = dbSystem;
|
||||
this.serverInfo = serverInfo;
|
||||
this.jsonStorage = jsonStorage;
|
||||
this.graphs = graphs;
|
||||
this.graphJSONCreator = graphJSONCreator;
|
||||
this.locale = locale;
|
||||
this.formatters = formatters;
|
||||
@ -111,7 +118,9 @@ public class QueryJSONResolver implements Resolver {
|
||||
@Override
|
||||
public boolean canAccess(Request request) {
|
||||
WebUser user = request.getUser().orElse(new WebUser(""));
|
||||
return user.hasPermission(WebPermission.ACCESS_QUERY);
|
||||
return user.hasPermission(WebPermission.ACCESS_QUERY)
|
||||
|| user.hasPermission(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_CALENDAR)
|
||||
|| user.hasPermission(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_CALENDAR);
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -213,12 +222,21 @@ public class QueryJSONResolver implements Resolver {
|
||||
long before = view.getBeforeEpochMs();
|
||||
List<ServerUUID> serverUUIDs = view.getServerUUIDs();
|
||||
|
||||
return Maps.builder(String.class, Object.class)
|
||||
.put("players", getPlayersTableData(userIds, serverUUIDs, after, before))
|
||||
.put("activity", getActivityGraphData(userIds, serverUUIDs, after, before))
|
||||
.put("geolocation", getGeolocationData(userIds))
|
||||
.put("sessions", getSessionSummaryData(userIds, serverUUIDs, after, before))
|
||||
.build();
|
||||
Maps.Builder<String, Object> data = Maps.builder(String.class, Object.class);
|
||||
|
||||
if (view.isWanted("players")) data.put("players", getPlayersTableData(userIds, serverUUIDs, after, before));
|
||||
if (view.isWanted("activity")) data.put("activity", getActivityGraphData(userIds, serverUUIDs, after, before));
|
||||
if (view.isWanted("geolocation")) data.put("geolocation", getGeolocationData(userIds));
|
||||
if (view.isWanted("sessions")) data.put("sessions", getSessionSummaryData(userIds, serverUUIDs, after, before));
|
||||
if (view.isWanted("sessionList")) data.put("sessionList", getSessionList(userIds, serverUUIDs, after, before));
|
||||
|
||||
return data.build();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> getSessionList(Set<Integer> userIds, List<ServerUUID> serverUUIDs, long after, long before) {
|
||||
Database database = dbSystem.getDatabase();
|
||||
List<FinishedSession> sessions = database.query(SessionQueries.fetchQuerySessions(userIds, serverUUIDs, after, before));
|
||||
return new SessionsMutator(sessions).toPlayerNameJSONMaps(graphs, config.getWorldAliasSettings(), formatters);
|
||||
}
|
||||
|
||||
private Map<String, String> getSessionSummaryData(Set<Integer> userIds, List<ServerUUID> serverUUIDs, long after, long before) {
|
||||
|
@ -946,6 +946,33 @@ public class SessionQueries {
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<List<FinishedSession>> fetchQuerySessions(Set<Integer> userIds, List<ServerUUID> serverUUIDs, long after, long before) {
|
||||
String uuidsInSet = " IN (" + new TextStringBuilder().appendWithSeparators(userIds, ",") + ")";
|
||||
String selectServerIds = SELECT + ServerTable.ID +
|
||||
FROM + ServerTable.TABLE_NAME +
|
||||
WHERE + ServerTable.SERVER_UUID + " IN ('" + new TextStringBuilder().appendWithSeparators(serverUUIDs, "','") + "')";
|
||||
|
||||
String sql = SELECT_SESSIONS_STATEMENT +
|
||||
WHERE + SessionsTable.SESSION_START + ">?" +
|
||||
AND + SessionsTable.SESSION_END + "<?" +
|
||||
AND + "s." + SessionsTable.USER_ID + uuidsInSet +
|
||||
(serverUUIDs.isEmpty() ? "" : AND + "s." + SessionsTable.SERVER_ID + " IN (" + selectServerIds + ")") +
|
||||
ORDER_BY_SESSION_START_DESC;
|
||||
|
||||
return new QueryStatement<>(sql, 2000) {
|
||||
@Override
|
||||
public void prepare(PreparedStatement statement) throws SQLException {
|
||||
statement.setLong(1, after);
|
||||
statement.setLong(2, before);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FinishedSession> processResults(ResultSet set) throws SQLException {
|
||||
return extractDataFromSessionSelectStatement(set);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Query<Map<String, Long>> summaryOfPlayers(Set<Integer> userIds, List<ServerUUID> serverUUIDs, long after, long before) {
|
||||
String uuidsInSet = " IN (" + new TextStringBuilder().appendWithSeparators(userIds, ",") + ")";
|
||||
String selectServerIds = SELECT + ServerTable.ID +
|
||||
|
@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {Card} from "react-bootstrap";
|
||||
import CardHeader from "../CardHeader";
|
||||
import {faChevronLeft, faChevronRight, faHandHoldingHeart} from "@fortawesome/free-solid-svg-icons";
|
||||
import {fetchFirstMoments} from "../../../service/serverService";
|
||||
import {fetchFirstMoments, fetchPlayersOnlineGraph} from "../../../service/serverService";
|
||||
import {CardLoader} from "../../navigation/Loader";
|
||||
import XRangeGraph from "../../graphs/XRangeGraph";
|
||||
import {Link} from "react-router-dom";
|
||||
@ -10,50 +10,65 @@ import {tooltip} from "../../../util/graphs";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import Graph from "../../graphs/Graph";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {useDataRequest} from "../../../hooks/dataFetchHook";
|
||||
import {useMetadata} from "../../../hooks/metadataHook";
|
||||
import {ErrorViewBody} from "../../../views/ErrorView";
|
||||
import FormattedTime from "../../text/FormattedTime";
|
||||
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
const FirstMomentsCard = ({identifier}) => {
|
||||
const {t} = useTranslation();
|
||||
const {timeZoneOffsetMinutes, networkMetadata} = useMetadata();
|
||||
|
||||
const [data, setData] = useState(undefined);
|
||||
const [selectedDay, setSelectedDay] = useState(1648760400000); //Date.now())
|
||||
|
||||
const {data: playersOnline, loadingError} = useDataRequest(fetchPlayersOnlineGraph, [identifier]);
|
||||
const [sessionPlots, setSessionPlots] = useState([]);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
const loaded = await fetchFirstMoments(0, 0, identifier);
|
||||
setData(loaded);
|
||||
const startOfDay = selectedDay - (selectedDay + timeZoneOffsetMinutes * 60 * 1000) % dayMs;
|
||||
const endOfDay = startOfDay + dayMs;
|
||||
const {
|
||||
data: loaded,
|
||||
error
|
||||
} = await fetchFirstMoments(startOfDay, endOfDay, networkMetadata?.servers.find(s => s.serverUUID === identifier));
|
||||
console.log(loaded);
|
||||
const sessionsByPlayer = {};
|
||||
for (const session of loaded.sessions) {
|
||||
const player = session.player_name;
|
||||
if (!sessionsByPlayer[player]) sessionsByPlayer[player] = [];
|
||||
sessionsByPlayer[player].push(session);
|
||||
if (loaded?.data) {
|
||||
for (const session of loaded.data.sessionList) {
|
||||
const player = session.player_name;
|
||||
if (!sessionsByPlayer[player]) sessionsByPlayer[player] = [];
|
||||
sessionsByPlayer[player].push(session);
|
||||
}
|
||||
}
|
||||
const sessionPlots = [];
|
||||
let i = 1;
|
||||
for (const entry of Object.entries(sessionsByPlayer)) {
|
||||
sessionPlots.push({
|
||||
name: "Player " + i,
|
||||
name: entry[1][0].player_name,
|
||||
uuid: entry[1][0].player_uuid,
|
||||
points: entry[1].map(session => {
|
||||
const dayMs = 24 * 60 * 60 * 1000;
|
||||
const addStart = Math.floor(Math.random() * dayMs);
|
||||
const start = Date.now() - (Date.now() % dayMs) + addStart;
|
||||
const end = start + Math.floor(Math.random() * (dayMs - addStart));
|
||||
const start = session.startMillis;
|
||||
const end = session.endMillis;
|
||||
return {x: start, x2: end, color: session.first_session ? '#4caf50' : '#4ab4de'};
|
||||
}).sort((a, b) => a.x - b.x > 0 ? 1 : -1)
|
||||
}).sort((a, b) => a.x - b.x > 0 ? 1 : -1),
|
||||
playtime: entry[1].reduce((partialSum, session) => partialSum + session.endMillis - session.startMillis, 0)
|
||||
})
|
||||
i++;
|
||||
}
|
||||
setSessionPlots(sessionPlots.sort((a, b) => a.points[0].x - b.points[0].x > 0 ? 1 : -1));
|
||||
}, [setData, setSessionPlots, identifier]);
|
||||
}, [selectedDay, setSessionPlots, identifier]);
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [loadData]);
|
||||
|
||||
|
||||
const playersOnlineOptions = useMemo(() => {
|
||||
if (!data || !sessionPlots) return {};
|
||||
if (!playersOnline || !sessionPlots?.length) return {};
|
||||
|
||||
const startOfDay = sessionPlots ? (sessionPlots[0].points[0].x - sessionPlots[0].points[0].x % (24 * 60 * 60 * 1000)) : 0;
|
||||
const endOfDay = startOfDay + (24 * 60 * 60 * 1000);
|
||||
const startOfDay = selectedDay + timeZoneOffsetMinutes;
|
||||
const endOfDay = startOfDay + dayMs;
|
||||
return {
|
||||
yAxis: {
|
||||
title: {
|
||||
@ -80,22 +95,25 @@ const FirstMomentsCard = ({identifier}) => {
|
||||
name: t('html.label.playersOnline'),
|
||||
type: 'spline',
|
||||
tooltip: tooltip.zeroDecimals,
|
||||
data: data ? data.graphs[0].points : [],
|
||||
data: playersOnline ? playersOnline.playersOnline : [],
|
||||
color: "#90b7f3",
|
||||
yAxis: 0
|
||||
}]
|
||||
}
|
||||
}, [data, t, sessionPlots]);
|
||||
}, [playersOnline, t, sessionPlots]);
|
||||
|
||||
if (!data) return <CardLoader/>
|
||||
if (!sessionPlots) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader icon={faHandHoldingHeart} color="light-green" label={"First moments"}>
|
||||
<div className={"float-end"}>
|
||||
<span style={{marginRight: '0.5rem'}}>on 2023-04-10</span>
|
||||
<button style={{marginRight: '0.5rem'}}><FontAwesomeIcon icon={faChevronLeft}/></button>
|
||||
<button><FontAwesomeIcon icon={faChevronRight}/></button>
|
||||
<span style={{marginRight: '0.5rem'}}>on {new Date(selectedDay).toISOString().split("T")[0]}</span>
|
||||
<button style={{marginRight: '0.5rem'}} onClick={() => setSelectedDay(selectedDay - dayMs)}>
|
||||
<FontAwesomeIcon icon={faChevronLeft}/></button>
|
||||
<button onClick={() => setSelectedDay(selectedDay + dayMs)}>
|
||||
<FontAwesomeIcon icon={faChevronRight}/>
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{/*<ExtendableCardBody id={"card-body-first-moments"} style={{marginTop: "-0.5rem"}}>*/}
|
||||
@ -105,25 +123,28 @@ const FirstMomentsCard = ({identifier}) => {
|
||||
<table className={"table table-striped"}>
|
||||
<thead>
|
||||
<tr style={{position: 'sticky', top: 0, backgroundColor: "white", zIndex: 1}}>
|
||||
<td>Players Online</td>
|
||||
<td>
|
||||
{loadingError && <ErrorViewBody error={loadingError}/>}
|
||||
{!loadingError &&
|
||||
<Graph id={"players-online-graph"} options={playersOnlineOptions} className={''}
|
||||
style={{height: "100px"}}/>}
|
||||
</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr style={{position: 'sticky', top: "7.8rem", backgroundColor: "white", zIndex: 1}}>
|
||||
<th>Player</th>
|
||||
<th>Sessions</th>
|
||||
<th>Playtime</th>
|
||||
</tr>
|
||||
<tr style={{position: 'sticky', top: "3rem", backgroundColor: "white", zIndex: 1}}>
|
||||
<td>Players Online</td>
|
||||
<td>
|
||||
<Graph id={"players-online-graph"} options={playersOnlineOptions} className={''}
|
||||
style={{height: "100px"}}/>
|
||||
</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sessionPlots.map((plot, i) => <tr key={plot.name}>
|
||||
<td><Link to={`/player/${plot.uuid}`}>{plot.name}</Link></td>
|
||||
<td style={{padding: 0}}><XRangeGraph id={'xrange-' + i} pointsByAxis={[plot]} height={"60px"}/>
|
||||
<td style={{padding: 0}}><XRangeGraph id={`xrange-${plot.uuid}`} pointsByAxis={[plot]}
|
||||
height={"60px"}/>
|
||||
</td>
|
||||
<td>0s</td>
|
||||
<td><FormattedTime timeMs={plot.playtime}/></td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -121,7 +121,8 @@ const QueryOptionsCard = () => {
|
||||
afterTime: fromTime ? fromTime : options.view.afterTime,
|
||||
beforeDate: toDate ? toDate : options.view.beforeDate,
|
||||
beforeTime: toTime ? toTime : options.view.beforeTime,
|
||||
servers: selectedServers.map(index => options.view.servers[index])
|
||||
servers: selectedServers.map(index => options.view.servers[index]),
|
||||
wantedData: ["players", "activity", "geolocations", "sessions"]
|
||||
},
|
||||
filters
|
||||
}
|
||||
|
@ -94,7 +94,8 @@ const NetworkCalendarTab = () => {
|
||||
view: {
|
||||
afterDate: start, afterTime: "00:00",
|
||||
beforeDate: end, beforeTime: "00:00",
|
||||
servers: []
|
||||
servers: [],
|
||||
wantedData: ["players"]
|
||||
}
|
||||
}
|
||||
setQueryData(undefined);
|
||||
|
@ -71,7 +71,8 @@ const ServerCalendarTab = () => {
|
||||
view: {
|
||||
afterDate: start, afterTime: "00:00",
|
||||
beforeDate: end, beforeTime: "00:00",
|
||||
servers: networkMetadata?.servers.filter(server => server.serverUUID === identifier) || []
|
||||
servers: networkMetadata?.servers.filter(server => server.serverUUID === identifier) || [],
|
||||
wantedData: ["players"]
|
||||
}
|
||||
}
|
||||
setQueryData(undefined);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {doGetRequest, staticSite} from "./backendConfiguration";
|
||||
import {firstMoments} from "./mockData";
|
||||
import Highcharts from "highcharts/highstock";
|
||||
import {postQuery} from "./queryService";
|
||||
|
||||
export const fetchServerIdentity = async (timestamp, identifier) => {
|
||||
let url = `/v1/serverIdentity?server=${identifier}`;
|
||||
@ -341,6 +342,23 @@ export const fetchPluginHistory = async (timestamp, identifier) => {
|
||||
return doGetRequest(url, timestamp);
|
||||
}
|
||||
|
||||
export const fetchFirstMoments = async (timestamp, after, before, identifier) => {
|
||||
return firstMoments;
|
||||
export const fetchFirstMoments = async (after, before, server) => {
|
||||
const start = Highcharts.dateFormat('%d/%m/%Y', after);
|
||||
const end = Highcharts.dateFormat('%d/%m/%Y', before);
|
||||
const query = {
|
||||
filters: [{
|
||||
kind: "registeredBetween",
|
||||
parameters: {
|
||||
afterDate: start, afterTime: "00:00",
|
||||
beforeDate: end, beforeTime: "00:00"
|
||||
}
|
||||
}],
|
||||
view: {
|
||||
afterDate: start, afterTime: "00:00",
|
||||
beforeDate: end, beforeTime: "00:00",
|
||||
servers: server ? [server] : [],
|
||||
wantedData: ["sessionList"]
|
||||
}
|
||||
}
|
||||
return await postQuery(query);
|
||||
}
|
@ -5,6 +5,7 @@ import LoadIn from "../../components/animation/LoadIn";
|
||||
import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useAuth} from "../../hooks/authenticationHook";
|
||||
import FirstMomentsCard from "../../components/cards/common/FirstMomentsCard";
|
||||
|
||||
const ServerPlayerRetention = () => {
|
||||
const {hasPermission} = useAuth();
|
||||
@ -19,6 +20,11 @@ const ServerPlayerRetention = () => {
|
||||
<PlayerRetentionGraphCard identifier={identifier}/>
|
||||
</Col>
|
||||
</ExtendableRow>}
|
||||
<ExtendableRow id={'row-server-retention-1'}>
|
||||
<Col lg={12}>
|
||||
<FirstMomentsCard identifier={identifier}/>
|
||||
</Col>
|
||||
</ExtendableRow>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user