First mockup

# Conflicts:
#	Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/HtmlLang.java
#	Plan/react/dashboard/src/service/serverService.js
#	Plan/react/dashboard/src/views/network/NetworkPlayerRetention.js
This commit is contained in:
Aurora Lahtela 2023-10-10 15:26:54 +03:00
parent f9d28489c2
commit 482f43a8c6
13 changed files with 27623 additions and 12 deletions

View File

@ -282,7 +282,9 @@ public class SessionsMutator {
sessionMap.put("name", nameFunction.apply(sessionMap));
sessionMap.put("start", formatters.yearLong().apply(session.getStart()) +
(session.getExtraData(ActiveSession.class).isPresent() ? " (Online)" : ""));
sessionMap.put("startMillis", session.getStart());
sessionMap.put("end", formatters.yearLong().apply(session.getEnd()));
sessionMap.put("endMillis", session.getEnd());
sessionMap.put("most_used_world", worldAliasSettings.getLongestWorldPlayed(session));
sessionMap.put("length", formatters.timeAmount().apply(session.getLength()));
sessionMap.put("afk_time", formatters.timeAmount().apply(session.getAfkTime()));

View File

@ -427,6 +427,8 @@ public enum HtmlLang implements Lang {
MANAGE_ALERT_SAVE_FAIL("html.label.managePage.alert.saveFail", "Failed to save changes: {{error}}"),
MANAGE_ALERT_SAVE_SUCCESS("html.label.managePage.alert.saveSuccess", "Changes saved successfully!"),
FIRST_MOMENTS("html.label.firstMoments", "First Moments"),
INFO_NO_UPTIME("html.description.noUptimeCalculation", "Server is offline, or has never restarted with Plan installed."),
WARNING_NO_GAME_SERVERS("html.description.noGameServers", "Some data requires Plan to be installed on game servers."),
WARNING_PERFORMANCE_NO_GAME_SERVERS("html.description.performanceNoGameServers", "TPS, Entity or Chunk data is not gathered from proxy servers since they don't have game tick loop."),

View File

@ -0,0 +1,135 @@
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 {CardLoader} from "../../navigation/Loader";
import XRangeGraph from "../../graphs/XRangeGraph";
import {Link} from "react-router-dom";
import {tooltip} from "../../../util/graphs";
import {useTranslation} from "react-i18next";
import Graph from "../../graphs/Graph";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
const FirstMomentsCard = ({identifier}) => {
const {t} = useTranslation();
const [data, setData] = useState(undefined);
const [sessionPlots, setSessionPlots] = useState([]);
const loadData = useCallback(async () => {
const loaded = await fetchFirstMoments(0, 0, identifier);
setData(loaded);
const sessionsByPlayer = {};
for (const session of loaded.sessions) {
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,
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));
return {x: start, x2: end, color: session.first_session ? '#4caf50' : '#4ab4de'};
}).sort((a, b) => a.x - b.x > 0 ? 1 : -1)
})
i++;
}
setSessionPlots(sessionPlots.sort((a, b) => a.points[0].x - b.points[0].x > 0 ? 1 : -1));
}, [setData, setSessionPlots, identifier]);
useEffect(() => {
loadData()
}, [loadData]);
const playersOnlineOptions = useMemo(() => {
if (!data || !sessionPlots) 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);
return {
yAxis: {
title: {
text: ''
},
opposite: true,
softMax: 1,
softMin: 0
},
xAxis: {
visible: false,
type: 'datetime',
min: startOfDay,
max: endOfDay
},
title: {text: ''},
legend: {
enabled: false
},
time: {
timezoneOffset: 0
},
series: [{
name: t('html.label.playersOnline'),
type: 'spline',
tooltip: tooltip.zeroDecimals,
data: data ? data.graphs[0].points : [],
color: "#90b7f3",
yAxis: 0
}]
}
}, [data, t, sessionPlots]);
if (!data) 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>
</div>
</CardHeader>
{/*<ExtendableCardBody id={"card-body-first-moments"} style={{marginTop: "-0.5rem"}}>*/}
{/* <Filter index={0} filter={filter} setFilterOptions={setFilterOptions}/>*/}
{/*</ExtendableCardBody>*/}
<div style={{overflowY: "scroll", maxHeight: "700px"}}>
<table className={"table table-striped"}>
<thead>
<tr style={{position: 'sticky', top: 0, 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>
<td>0s</td>
</tr>)}
</tbody>
</table>
</div>
</Card>
)
};
export default FirstMomentsCard

View File

@ -67,11 +67,11 @@ const BetweenDatesFilter = ({index, label, filter, removeFilter, setFilterOption
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
/>
</Col>
<Col md={"auto"} sm={12} className={"my-1 my-md-auto"}>
{removeFilter && <Col md={"auto"} sm={12} className={"my-1 my-md-auto"}>
<button className="filter-remover btn btn-outline-secondary float-end"
onClick={removeFilter}><FontAwesomeIcon icon={faTrashAlt}/>
</button>
</Col>
</Col>}
</Row>
</div>
)

View File

@ -3,10 +3,13 @@ import MultipleChoiceFilter from "./MultipleChoiceFilter";
import {useTranslation} from "react-i18next";
import PluginGroupsFilter from "./PluginGroupsFilter";
import BetweenDatesFilter from "./BetweenDatesFilter";
import Loader from "../../../navigation/Loader";
const Filter = ({index, filter, setFilterOptions, removeFilter, setAsInvalid, setAsValid}) => {
const {t} = useTranslation();
if (!filter) return <Loader/>;
if (filter.kind.startsWith("pluginGroups-")) {
return <PluginGroupsFilter index={index} filter={filter}
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>;

View File

@ -28,11 +28,11 @@ const MultipleChoiceFilter = ({index, label, filter, removeFilter, setFilterOpti
setSelectedIndexes={setSelectedIndexes}
selectedIndexes={selectedIndexes}/>
</Col>
<Col md={"auto"}>
{removeFilter && <Col md={"auto"}>
<button className="filter-remover btn btn-outline-secondary float-end"
onClick={removeFilter}><FontAwesomeIcon icon={faTrashAlt}/>
</button>
</Col>
</Col>}
</Row>
</div>
)

View File

@ -8,10 +8,12 @@ import Accessibility from "highcharts/modules/accessibility"
import {useTranslation} from "react-i18next";
const Graph = ({
id,
options,
tall,
}) => {
id,
options,
className,
style,
tall,
}) => {
const {t} = useTranslation()
const {graphTheming, nightModeEnabled} = useTheme();
@ -26,10 +28,11 @@ const Graph = ({
}, [options, id, t,
graphTheming, nightModeEnabled]);
const style = tall ? {height: "450px"} : undefined;
const tallStyle = tall ? {height: "450px"} : undefined;
const givenStyle = style ? style : tallStyle;
return (
<div className="chart-area" style={style} id={id}>
<div className={className || "chart-area"} style={givenStyle} id={id}>
<span className="loader"/>
</div>
)

View File

@ -0,0 +1,73 @@
import React, {useEffect} from "react";
import Highcharts from 'highcharts';
import XRange from "highcharts/modules/xrange";
import {useTheme} from "../../hooks/themeHook";
import {useTranslation} from "react-i18next";
import Accessibility from "highcharts/modules/accessibility";
import {withReducedSaturation} from "../../util/colors";
const XRangeGraph = ({id, pointsByAxis, height}) => {
const {t} = useTranslation();
const {graphTheming, nightModeEnabled} = useTheme();
useEffect(() => {
const data = [];
const categories = [];
for (let i = 0; i < pointsByAxis.length; i++) {
const axis = pointsByAxis[i];
categories.push(axis.name);
data.push(...axis.points.map(point => {
return {x: point.x, x2: point.x2, y: i, color: point.color};
}));
}
const startOfDay = pointsByAxis[0].points[0].x - pointsByAxis[0].points[0].x % (24 * 60 * 60 * 1000);
const endOfDay = startOfDay + (24 * 60 * 60 * 1000);
const series = {
name: t('html.label.sessions'),
color: nightModeEnabled ? withReducedSaturation('#222') : '#222',
data: data,
animation: false,
pointWidth: 20,
colorByPoint: true
};
Accessibility(Highcharts);
XRange(Highcharts);
Highcharts.setOptions(graphTheming);
setTimeout(() => Highcharts.chart(id, {
chart: {
backgroundColor: 'transparent',
plotBackgroundColor: 'transparent',
type: 'xrange'
},
title: {text: ''},
xAxis: {
type: 'datetime',
min: startOfDay,
max: endOfDay
},
time: {
timezoneOffset: 0
},
legend: {
enabled: false
},
yAxis: {
title: {
text: ''
},
categories: categories,
reversed: true,
visible: false
},
series: [series]
}), 25)
}, [pointsByAxis, graphTheming, t, nightModeEnabled, id])
return (
<div style={{height}} id={id}>
<span className="loader"/>
</div>
)
}
export default XRangeGraph

View File

@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react';
import {Card} from "react-bootstrap";
import {usePageExtension} from "../../../hooks/pageExtensionHook";
const ExtendableCardBody = ({id, className, children}) => {
const ExtendableCardBody = ({id, className, style, children}) => {
const [elementsBefore, setElementsBefore] = useState([]);
const [elementsAfter, setElementsAfter] = useState([]);
const {onRender, onUnmount, context} = usePageExtension();
@ -27,7 +27,7 @@ const ExtendableCardBody = ({id, className, children}) => {
return (
<>
<div dangerouslySetInnerHTML={{__html: elementsBefore.join('')}}/>
<Card.Body id={id} className={className ? "extendable " + className : "extendable"}>
<Card.Body id={id} className={className ? "extendable " + className : "extendable"} style={style}>
{children}
</Card.Body>
<div dangerouslySetInnerHTML={{__html: elementsAfter.join('')}}/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import {doGetRequest, staticSite} from "./backendConfiguration";
import {firstMoments} from "./mockData";
export const fetchServerIdentity = async (timestamp, identifier) => {
let url = `/v1/serverIdentity?server=${identifier}`;
@ -338,4 +339,8 @@ export const fetchPluginHistory = async (timestamp, identifier) => {
let url = `/v1/pluginHistory?server=${identifier}`;
if (staticSite) url = `/data/pluginHistory-${identifier}.json`;
return doGetRequest(url, timestamp);
}
export const fetchFirstMoments = async (timestamp, after, before, identifier) => {
return firstMoments;
}

View File

@ -3,6 +3,7 @@ import ExtendableRow from "../../components/layout/extension/ExtendableRow";
import {Col} from "react-bootstrap";
import LoadIn from "../../components/animation/LoadIn";
import PlayerRetentionGraphCard from "../../components/cards/common/PlayerRetentionGraphCard";
import FirstMomentsCard from "../../components/cards/common/FirstMomentsCard";
import {useAuth} from "../../hooks/authenticationHook";
const NetworkPlayerRetention = () => {
@ -17,6 +18,11 @@ const NetworkPlayerRetention = () => {
<PlayerRetentionGraphCard identifier={null}/>
</Col>
</ExtendableRow>
<ExtendableRow id={'row-network-retention-1'}>
<Col lg={12}>
<FirstMomentsCard identifier={null}/>
</Col>
</ExtendableRow>
</section>}
</LoadIn>
)