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:
parent
f9d28489c2
commit
482f43a8c6
|
@ -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()));
|
||||
|
|
|
@ -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."),
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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}/>;
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue