Use real data

This commit is contained in:
Aurora Lahtela 2023-10-10 16:50:40 +03:00
parent 482f43a8c6
commit 9c7d3007f8
9 changed files with 150 additions and 48 deletions

View File

@ -24,8 +24,10 @@ import org.apache.commons.lang3.StringUtils;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -41,6 +43,7 @@ public class ViewDto {
private final String beforeDate; private final String beforeDate;
private final String beforeTime; private final String beforeTime;
private final List<ServerDto> servers; private final List<ServerDto> servers;
private final Set<String> wantedData;
public ViewDto(Formatters formatters, List<ServerDto> servers) { public ViewDto(Formatters formatters, List<ServerDto> servers) {
this.servers = servers; this.servers = servers;
@ -55,6 +58,8 @@ public class ViewDto {
this.afterTime = after[1]; this.afterTime = after[1];
this.beforeDate = before[0]; this.beforeDate = before[0];
this.beforeTime = before[1]; this.beforeTime = before[1];
this.wantedData = new HashSet<>();
} }
public long getAfterEpochMs() throws ParseException { public long getAfterEpochMs() throws ParseException {
@ -72,6 +77,10 @@ public class ViewDto {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public boolean isWanted(String key) {
return wantedData == null || wantedData.isEmpty() || wantedData.contains(key);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -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.InputQueryDto;
import com.djrapitops.plan.delivery.domain.datatransfer.PlayerListDto; import com.djrapitops.plan.delivery.domain.datatransfer.PlayerListDto;
import com.djrapitops.plan.delivery.domain.datatransfer.ViewDto; 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.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters; import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.json.PlayersTableJSONCreator; 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.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.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response; 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.RequestBodyConverter;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage; import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionQueryResultTableDataQuery; 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.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig; import com.djrapitops.plan.settings.config.PlanConfig;
@ -81,6 +84,7 @@ public class QueryJSONResolver implements Resolver {
private final DBSystem dbSystem; private final DBSystem dbSystem;
private final ServerInfo serverInfo; private final ServerInfo serverInfo;
private final JSONStorage jsonStorage; private final JSONStorage jsonStorage;
private final Graphs graphs;
private final GraphJSONCreator graphJSONCreator; private final GraphJSONCreator graphJSONCreator;
private final Locale locale; private final Locale locale;
private final Formatters formatters; private final Formatters formatters;
@ -91,7 +95,9 @@ public class QueryJSONResolver implements Resolver {
QueryFilters filters, QueryFilters filters,
PlanConfig config, PlanConfig config,
DBSystem dbSystem, DBSystem dbSystem,
ServerInfo serverInfo, JSONStorage jsonStorage, ServerInfo serverInfo,
JSONStorage jsonStorage,
Graphs graphs,
GraphJSONCreator graphJSONCreator, GraphJSONCreator graphJSONCreator,
Locale locale, Locale locale,
Formatters formatters, Formatters formatters,
@ -102,6 +108,7 @@ public class QueryJSONResolver implements Resolver {
this.dbSystem = dbSystem; this.dbSystem = dbSystem;
this.serverInfo = serverInfo; this.serverInfo = serverInfo;
this.jsonStorage = jsonStorage; this.jsonStorage = jsonStorage;
this.graphs = graphs;
this.graphJSONCreator = graphJSONCreator; this.graphJSONCreator = graphJSONCreator;
this.locale = locale; this.locale = locale;
this.formatters = formatters; this.formatters = formatters;
@ -111,7 +118,9 @@ public class QueryJSONResolver implements Resolver {
@Override @Override
public boolean canAccess(Request request) { public boolean canAccess(Request request) {
WebUser user = request.getUser().orElse(new WebUser("")); 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 @GET
@ -213,12 +222,21 @@ public class QueryJSONResolver implements Resolver {
long before = view.getBeforeEpochMs(); long before = view.getBeforeEpochMs();
List<ServerUUID> serverUUIDs = view.getServerUUIDs(); List<ServerUUID> serverUUIDs = view.getServerUUIDs();
return Maps.builder(String.class, Object.class) Maps.Builder<String, Object> data = Maps.builder(String.class, Object.class);
.put("players", getPlayersTableData(userIds, serverUUIDs, after, before))
.put("activity", getActivityGraphData(userIds, serverUUIDs, after, before)) if (view.isWanted("players")) data.put("players", getPlayersTableData(userIds, serverUUIDs, after, before));
.put("geolocation", getGeolocationData(userIds)) if (view.isWanted("activity")) data.put("activity", getActivityGraphData(userIds, serverUUIDs, after, before));
.put("sessions", getSessionSummaryData(userIds, serverUUIDs, after, before)) if (view.isWanted("geolocation")) data.put("geolocation", getGeolocationData(userIds));
.build(); 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) { private Map<String, String> getSessionSummaryData(Set<Integer> userIds, List<ServerUUID> serverUUIDs, long after, long before) {

View File

@ -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) { 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 uuidsInSet = " IN (" + new TextStringBuilder().appendWithSeparators(userIds, ",") + ")";
String selectServerIds = SELECT + ServerTable.ID + String selectServerIds = SELECT + ServerTable.ID +

View File

@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {Card} from "react-bootstrap"; import {Card} from "react-bootstrap";
import CardHeader from "../CardHeader"; import CardHeader from "../CardHeader";
import {faChevronLeft, faChevronRight, faHandHoldingHeart} from "@fortawesome/free-solid-svg-icons"; 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 {CardLoader} from "../../navigation/Loader";
import XRangeGraph from "../../graphs/XRangeGraph"; import XRangeGraph from "../../graphs/XRangeGraph";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
@ -10,50 +10,65 @@ import {tooltip} from "../../../util/graphs";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import Graph from "../../graphs/Graph"; import Graph from "../../graphs/Graph";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; 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 FirstMomentsCard = ({identifier}) => {
const {t} = useTranslation(); 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 [sessionPlots, setSessionPlots] = useState([]);
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
const loaded = await fetchFirstMoments(0, 0, identifier); const startOfDay = selectedDay - (selectedDay + timeZoneOffsetMinutes * 60 * 1000) % dayMs;
setData(loaded); const endOfDay = startOfDay + dayMs;
const {
data: loaded,
error
} = await fetchFirstMoments(startOfDay, endOfDay, networkMetadata?.servers.find(s => s.serverUUID === identifier));
console.log(loaded);
const sessionsByPlayer = {}; const sessionsByPlayer = {};
for (const session of loaded.sessions) { if (loaded?.data) {
const player = session.player_name; for (const session of loaded.data.sessionList) {
if (!sessionsByPlayer[player]) sessionsByPlayer[player] = []; const player = session.player_name;
sessionsByPlayer[player].push(session); if (!sessionsByPlayer[player]) sessionsByPlayer[player] = [];
sessionsByPlayer[player].push(session);
}
} }
const sessionPlots = []; const sessionPlots = [];
let i = 1; let i = 1;
for (const entry of Object.entries(sessionsByPlayer)) { for (const entry of Object.entries(sessionsByPlayer)) {
sessionPlots.push({ sessionPlots.push({
name: "Player " + i, name: entry[1][0].player_name,
uuid: entry[1][0].player_uuid, uuid: entry[1][0].player_uuid,
points: entry[1].map(session => { points: entry[1].map(session => {
const dayMs = 24 * 60 * 60 * 1000; const start = session.startMillis;
const addStart = Math.floor(Math.random() * dayMs); const end = session.endMillis;
const start = Date.now() - (Date.now() % dayMs) + addStart;
const end = start + Math.floor(Math.random() * (dayMs - addStart));
return {x: start, x2: end, color: session.first_session ? '#4caf50' : '#4ab4de'}; 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++; i++;
} }
setSessionPlots(sessionPlots.sort((a, b) => a.points[0].x - b.points[0].x > 0 ? 1 : -1)); setSessionPlots(sessionPlots.sort((a, b) => a.points[0].x - b.points[0].x > 0 ? 1 : -1));
}, [setData, setSessionPlots, identifier]); }, [selectedDay, setSessionPlots, identifier]);
useEffect(() => { useEffect(() => {
loadData() loadData()
}, [loadData]); }, [loadData]);
const playersOnlineOptions = useMemo(() => { 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 startOfDay = selectedDay + timeZoneOffsetMinutes;
const endOfDay = startOfDay + (24 * 60 * 60 * 1000); const endOfDay = startOfDay + dayMs;
return { return {
yAxis: { yAxis: {
title: { title: {
@ -80,22 +95,25 @@ const FirstMomentsCard = ({identifier}) => {
name: t('html.label.playersOnline'), name: t('html.label.playersOnline'),
type: 'spline', type: 'spline',
tooltip: tooltip.zeroDecimals, tooltip: tooltip.zeroDecimals,
data: data ? data.graphs[0].points : [], data: playersOnline ? playersOnline.playersOnline : [],
color: "#90b7f3", color: "#90b7f3",
yAxis: 0 yAxis: 0
}] }]
} }
}, [data, t, sessionPlots]); }, [playersOnline, t, sessionPlots]);
if (!data) return <CardLoader/> if (!sessionPlots) return <CardLoader/>;
return ( return (
<Card> <Card>
<CardHeader icon={faHandHoldingHeart} color="light-green" label={"First moments"}> <CardHeader icon={faHandHoldingHeart} color="light-green" label={"First moments"}>
<div className={"float-end"}> <div className={"float-end"}>
<span style={{marginRight: '0.5rem'}}>on 2023-04-10</span> <span style={{marginRight: '0.5rem'}}>on {new Date(selectedDay).toISOString().split("T")[0]}</span>
<button style={{marginRight: '0.5rem'}}><FontAwesomeIcon icon={faChevronLeft}/></button> <button style={{marginRight: '0.5rem'}} onClick={() => setSelectedDay(selectedDay - dayMs)}>
<button><FontAwesomeIcon icon={faChevronRight}/></button> <FontAwesomeIcon icon={faChevronLeft}/></button>
<button onClick={() => setSelectedDay(selectedDay + dayMs)}>
<FontAwesomeIcon icon={faChevronRight}/>
</button>
</div> </div>
</CardHeader> </CardHeader>
{/*<ExtendableCardBody id={"card-body-first-moments"} style={{marginTop: "-0.5rem"}}>*/} {/*<ExtendableCardBody id={"card-body-first-moments"} style={{marginTop: "-0.5rem"}}>*/}
@ -105,25 +123,28 @@ const FirstMomentsCard = ({identifier}) => {
<table className={"table table-striped"}> <table className={"table table-striped"}>
<thead> <thead>
<tr style={{position: 'sticky', top: 0, backgroundColor: "white", zIndex: 1}}> <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>Player</th>
<th>Sessions</th> <th>Sessions</th>
<th>Playtime</th> <th>Playtime</th>
</tr> </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> </thead>
<tbody> <tbody>
{sessionPlots.map((plot, i) => <tr key={plot.name}> {sessionPlots.map((plot, i) => <tr key={plot.name}>
<td><Link to={`/player/${plot.uuid}`}>{plot.name}</Link></td> <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>
<td>0s</td> <td><FormattedTime timeMs={plot.playtime}/></td>
</tr>)} </tr>)}
</tbody> </tbody>
</table> </table>

View File

@ -121,7 +121,8 @@ const QueryOptionsCard = () => {
afterTime: fromTime ? fromTime : options.view.afterTime, afterTime: fromTime ? fromTime : options.view.afterTime,
beforeDate: toDate ? toDate : options.view.beforeDate, beforeDate: toDate ? toDate : options.view.beforeDate,
beforeTime: toTime ? toTime : options.view.beforeTime, 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 filters
} }

View File

@ -94,7 +94,8 @@ const NetworkCalendarTab = () => {
view: { view: {
afterDate: start, afterTime: "00:00", afterDate: start, afterTime: "00:00",
beforeDate: end, beforeTime: "00:00", beforeDate: end, beforeTime: "00:00",
servers: [] servers: [],
wantedData: ["players"]
} }
} }
setQueryData(undefined); setQueryData(undefined);

View File

@ -71,7 +71,8 @@ const ServerCalendarTab = () => {
view: { view: {
afterDate: start, afterTime: "00:00", afterDate: start, afterTime: "00:00",
beforeDate: end, beforeTime: "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); setQueryData(undefined);

View File

@ -1,5 +1,6 @@
import {doGetRequest, staticSite} from "./backendConfiguration"; import {doGetRequest, staticSite} from "./backendConfiguration";
import {firstMoments} from "./mockData"; import Highcharts from "highcharts/highstock";
import {postQuery} from "./queryService";
export const fetchServerIdentity = async (timestamp, identifier) => { export const fetchServerIdentity = async (timestamp, identifier) => {
let url = `/v1/serverIdentity?server=${identifier}`; let url = `/v1/serverIdentity?server=${identifier}`;
@ -341,6 +342,23 @@ export const fetchPluginHistory = async (timestamp, identifier) => {
return doGetRequest(url, timestamp); return doGetRequest(url, timestamp);
} }
export const fetchFirstMoments = async (timestamp, after, before, identifier) => { export const fetchFirstMoments = async (after, before, server) => {
return firstMoments; 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);
} }

View File

@ -5,6 +5,7 @@ import LoadIn from "../../components/animation/LoadIn";
import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard"; import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard";
import {useParams} from "react-router-dom"; import {useParams} from "react-router-dom";
import {useAuth} from "../../hooks/authenticationHook"; import {useAuth} from "../../hooks/authenticationHook";
import FirstMomentsCard from "../../components/cards/common/FirstMomentsCard";
const ServerPlayerRetention = () => { const ServerPlayerRetention = () => {
const {hasPermission} = useAuth(); const {hasPermission} = useAuth();
@ -19,6 +20,11 @@ const ServerPlayerRetention = () => {
<PlayerRetentionGraphCard identifier={identifier}/> <PlayerRetentionGraphCard identifier={identifier}/>
</Col> </Col>
</ExtendableRow>} </ExtendableRow>}
<ExtendableRow id={'row-server-retention-1'}>
<Col lg={12}>
<FirstMomentsCard identifier={identifier}/>
</Col>
</ExtendableRow>
</section> </section>
</LoadIn> </LoadIn>
) )