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.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;

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.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) {

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

View File

@ -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
}

View File

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

View File

@ -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);

View File

@ -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);
}

View File

@ -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>
)