mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-01-21 23:51:29 +01:00
Implemented rest of the graphs on Online Activity Overview
This commit is contained in:
parent
538eff10d9
commit
e77a9ee8b5
@ -37,6 +37,7 @@ public enum HtmlLang implements Lang {
|
|||||||
SIDE_PERFORMANCE("html.label.performance", "Performance"),
|
SIDE_PERFORMANCE("html.label.performance", "Performance"),
|
||||||
QUERY_MAKE("html.label.query", "Make a query"),
|
QUERY_MAKE("html.label.query", "Make a query"),
|
||||||
UNIT_NO_DATA("generic.noData", "No Data"), // Generic
|
UNIT_NO_DATA("generic.noData", "No Data"), // Generic
|
||||||
|
GRAPH_NO_DATA("html.label.noDataToDisplay", "No Data to Display"),
|
||||||
// Modals
|
// Modals
|
||||||
TITLE_THEME_SELECT("html.label.themeSelect", "Theme Select"),
|
TITLE_THEME_SELECT("html.label.themeSelect", "Theme Select"),
|
||||||
LINK_NIGHT_MODE("html.button.nightMode", "Night Mode"),
|
LINK_NIGHT_MODE("html.button.nightMode", "Night Mode"),
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react";
|
||||||
|
import FullCalendar from '@fullcalendar/react'
|
||||||
|
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||||
|
|
||||||
|
const ServerCalendar = ({series, firstDay}) => {
|
||||||
|
return (
|
||||||
|
<FullCalendar
|
||||||
|
plugins={[dayGridPlugin]}
|
||||||
|
timeZone="UTC"
|
||||||
|
themeSystem='bootstrap'
|
||||||
|
eventColor='#2196F3'
|
||||||
|
// dayMaxEventRows={4}
|
||||||
|
firstDay={firstDay}
|
||||||
|
initialView='dayGridMonth'
|
||||||
|
navLinks={true}
|
||||||
|
height={800}
|
||||||
|
contentHeight={800}
|
||||||
|
headerToolbar={{
|
||||||
|
left: 'title',
|
||||||
|
center: '',
|
||||||
|
right: 'dayGridMonth dayGridWeek dayGridDay today prev next'
|
||||||
|
}}
|
||||||
|
events={(_fetchInfo, successCallback) => successCallback(series)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerCalendar
|
@ -0,0 +1,85 @@
|
|||||||
|
import {useParams} from "react-router-dom";
|
||||||
|
import {useDataRequest} from "../../../hooks/dataFetchHook";
|
||||||
|
import {
|
||||||
|
fetchDayByDayGraph,
|
||||||
|
fetchHourByHourGraph,
|
||||||
|
fetchPunchCardGraph,
|
||||||
|
fetchServerCalendarGraph
|
||||||
|
} from "../../../service/serverService";
|
||||||
|
import {ErrorViewBody} from "../../../views/ErrorView";
|
||||||
|
import PunchCard from "../../graphs/PunchCard";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {Card} from "react-bootstrap-v5";
|
||||||
|
import CardTabs from "../../CardTabs";
|
||||||
|
import {faBraille, faChartArea} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import React from "react";
|
||||||
|
import TimeByTimeGraph from "../../graphs/TimeByTimeGraph";
|
||||||
|
import ServerCalendar from "../../calendar/ServerCalendar";
|
||||||
|
|
||||||
|
const DayByDayTab = () => {
|
||||||
|
const {identifier} = useParams();
|
||||||
|
|
||||||
|
const {data, loadingError} = useDataRequest(fetchDayByDayGraph, [identifier])
|
||||||
|
|
||||||
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
|
if (!data) return <></>;
|
||||||
|
|
||||||
|
return <TimeByTimeGraph data={data}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
const HourByHourTab = () => {
|
||||||
|
const {identifier} = useParams();
|
||||||
|
|
||||||
|
const {data, loadingError} = useDataRequest(fetchHourByHourGraph, [identifier])
|
||||||
|
|
||||||
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
|
if (!data) return <></>;
|
||||||
|
|
||||||
|
return <TimeByTimeGraph data={data}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerCalendarTab = () => {
|
||||||
|
const {identifier} = useParams();
|
||||||
|
|
||||||
|
const {data, loadingError} = useDataRequest(fetchServerCalendarGraph, [identifier])
|
||||||
|
|
||||||
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
|
if (!data) return <></>;
|
||||||
|
|
||||||
|
return <ServerCalendar series={data.data} firstDay={data.firstDay}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PunchCardTab = () => {
|
||||||
|
const {identifier} = useParams();
|
||||||
|
|
||||||
|
const {data, loadingError} = useDataRequest(fetchPunchCardGraph, [identifier])
|
||||||
|
|
||||||
|
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||||
|
if (!data) return <></>;
|
||||||
|
|
||||||
|
return <PunchCard series={data.punchCard}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnlineActivityGraphsCard = () => {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
return <Card>
|
||||||
|
<CardTabs tabs={[
|
||||||
|
{
|
||||||
|
name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day',
|
||||||
|
element: <DayByDayTab/>
|
||||||
|
}, {
|
||||||
|
name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour',
|
||||||
|
element: <HourByHourTab/>
|
||||||
|
}, {
|
||||||
|
name: t('html.label.serverCalendar'), icon: faCalendar, color: 'teal', href: 'server-calendar',
|
||||||
|
element: <ServerCalendarTab/>
|
||||||
|
}, {
|
||||||
|
name: t('html.label.punchcard30days'), icon: faBraille, color: 'black', href: 'punchcard',
|
||||||
|
element: <PunchCardTab/>
|
||||||
|
},
|
||||||
|
]}/>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OnlineActivityGraphsCard;
|
42
Plan/react/dashboard/src/components/graphs/LineGraph.js
Normal file
42
Plan/react/dashboard/src/components/graphs/LineGraph.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {useTheme} from "../../hooks/themeHook";
|
||||||
|
import React, {useEffect} from "react";
|
||||||
|
import {linegraphButtons} from "../../util/graphs";
|
||||||
|
import Highcharts from "highcharts/highstock";
|
||||||
|
import NoDataDisplay from "highcharts/modules/no-data-to-display"
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
|
||||||
|
const LineGraph = ({id, series}) => {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
const {graphTheming} = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
NoDataDisplay(Highcharts);
|
||||||
|
Highcharts.setOptions({lang: {noData: t('html.labels.noDataToDisplay')}})
|
||||||
|
Highcharts.setOptions(graphTheming);
|
||||||
|
Highcharts.stockChart(id, {
|
||||||
|
rangeSelector: {
|
||||||
|
selected: 2,
|
||||||
|
buttons: linegraphButtons
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
softMax: 2,
|
||||||
|
softMin: 0
|
||||||
|
},
|
||||||
|
title: {text: ''},
|
||||||
|
plotOptions: {
|
||||||
|
areaspline: {
|
||||||
|
fillOpacity: 0.4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: series
|
||||||
|
})
|
||||||
|
}, [series, graphTheming])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chart-area" id={id}>
|
||||||
|
<span className="loader"/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LineGraph
|
@ -1,12 +1,11 @@
|
|||||||
import React, {useEffect} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {useTheme} from "../../hooks/themeHook";
|
import {tooltip} from "../../util/graphs";
|
||||||
import Highcharts from "highcharts/highstock";
|
import LineGraph from "./LineGraph";
|
||||||
import {linegraphButtons, tooltip} from "../../util/graphs";
|
|
||||||
|
|
||||||
const PingGraph = ({data}) => {
|
const PingGraph = ({data}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {graphTheming} = useTheme();
|
const [series, setSeries] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const avgPingSeries = {
|
const avgPingSeries = {
|
||||||
@ -30,25 +29,11 @@ const PingGraph = ({data}) => {
|
|||||||
data: data.min_ping_series,
|
data: data.min_ping_series,
|
||||||
color: data.colors.min
|
color: data.colors.min
|
||||||
}
|
}
|
||||||
Highcharts.setOptions(graphTheming);
|
setSeries([avgPingSeries, maxPingSeries, minPingSeries]);
|
||||||
Highcharts.stockChart("ping-graph", {
|
}, [data, t])
|
||||||
rangeSelector: {
|
|
||||||
selected: 2,
|
|
||||||
buttons: linegraphButtons
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
softMax: 2,
|
|
||||||
softMin: 0
|
|
||||||
},
|
|
||||||
title: {text: ''},
|
|
||||||
series: [avgPingSeries, maxPingSeries, minPingSeries]
|
|
||||||
})
|
|
||||||
}, [data, graphTheming, t])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chart-area" id="ping-graph">
|
<LineGraph id="ping-graph" series={series}/>
|
||||||
<span className="loader"/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import React, {useEffect} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import {useTheme} from "../../hooks/themeHook";
|
import {tooltip} from "../../util/graphs";
|
||||||
import Highcharts from "highcharts/highstock";
|
import LineGraph from "./LineGraph";
|
||||||
import {linegraphButtons, tooltip} from "../../util/graphs";
|
|
||||||
|
|
||||||
const PlayersOnlineGraph = ({data}) => {
|
const PlayersOnlineGraph = ({data}) => {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const {graphTheming} = useTheme();
|
const [series, setSeries] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const playersOnlineSeries = {
|
const playersOnlineSeries = {
|
||||||
@ -17,30 +16,11 @@ const PlayersOnlineGraph = ({data}) => {
|
|||||||
color: data.color,
|
color: data.color,
|
||||||
yAxis: 0
|
yAxis: 0
|
||||||
}
|
}
|
||||||
Highcharts.setOptions(graphTheming);
|
setSeries([playersOnlineSeries]);
|
||||||
Highcharts.stockChart("online-activity-graph", {
|
}, [data, t])
|
||||||
rangeSelector: {
|
|
||||||
selected: 2,
|
|
||||||
buttons: linegraphButtons
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
softMax: 2,
|
|
||||||
softMin: 0
|
|
||||||
},
|
|
||||||
title: {text: ''},
|
|
||||||
plotOptions: {
|
|
||||||
areaspline: {
|
|
||||||
fillOpacity: 0.4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [playersOnlineSeries]
|
|
||||||
})
|
|
||||||
}, [data, graphTheming, t])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chart-area" id="online-activity-graph">
|
<LineGraph id="players-online-graph" series={series}/>
|
||||||
<span className="loader"/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
import React, {useEffect, useState} from "react";
|
||||||
|
import {tooltip} from "../../util/graphs";
|
||||||
|
import LineGraph from "./LineGraph";
|
||||||
|
|
||||||
|
const TimeByTimeGraph = ({data}) => {
|
||||||
|
const {t} = useTranslation();
|
||||||
|
const [series, setSeries] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const uniquePlayers = {
|
||||||
|
name: t('html.label.uniquePlayers'),
|
||||||
|
type: 'spline',
|
||||||
|
tooltip: tooltip.zeroDecimals,
|
||||||
|
data: data.uniquePlayers,
|
||||||
|
color: data.colors.playersOnline
|
||||||
|
};
|
||||||
|
const newPlayers = {
|
||||||
|
name: t('html.label.newPlayers'),
|
||||||
|
type: 'spline',
|
||||||
|
tooltip: tooltip.zeroDecimals,
|
||||||
|
data: data.newPlayers,
|
||||||
|
color: data.colors.newPlayers
|
||||||
|
};
|
||||||
|
setSeries([uniquePlayers, newPlayers]);
|
||||||
|
}, [data, t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LineGraph id="day-by-day-graph" series={series}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeByTimeGraph
|
@ -172,7 +172,7 @@ const SidebarCollapse = ({item, open, setOpen}) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderItem = (item, i, openCollapse, setOpenCollapse) => {
|
const renderItem = (item, i, openCollapse, setOpenCollapse, t) => {
|
||||||
if (item.contents) {
|
if (item.contents) {
|
||||||
return <SidebarCollapse key={i}
|
return <SidebarCollapse key={i}
|
||||||
item={item}
|
item={item}
|
||||||
@ -191,7 +191,7 @@ const renderItem = (item, i, openCollapse, setOpenCollapse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.name) {
|
if (item.name) {
|
||||||
return <div key={i} className="sidebar-heading">{item.name}</div>
|
return <div key={i} className="sidebar-heading">{t(item.name)}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <hr key={i} className="sidebar-divider"/>
|
return <hr key={i} className="sidebar-divider"/>
|
||||||
@ -227,7 +227,7 @@ const Sidebar = ({items, showBackButton}) => {
|
|||||||
<Item active={false} href="/" icon={faArrowLeft} name={t('html.label.toMainPage')}/>
|
<Item active={false} href="/" icon={faArrowLeft} name={t('html.label.toMainPage')}/>
|
||||||
<Divider/>
|
<Divider/>
|
||||||
</> : ''}
|
</> : ''}
|
||||||
{items.map((item, i) => renderItem(item, i, openCollapse, toggleCollapse))}
|
{items.map((item, i) => renderItem(item, i, openCollapse, toggleCollapse, t))}
|
||||||
<Divider/>
|
<Divider/>
|
||||||
<FooterButtons/>
|
<FooterButtons/>
|
||||||
</ul>}
|
</ul>}
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
|
import {useNavigation} from "./navigationHook";
|
||||||
|
|
||||||
export const useDataRequest = (fetchMethod, parameters) => {
|
export const useDataRequest = (fetchMethod, parameters) => {
|
||||||
const [data, setData] = useState(undefined);
|
const [data, setData] = useState(undefined);
|
||||||
const [loadingError, setLoadingError] = useState(undefined);
|
const [loadingError, setLoadingError] = useState(undefined);
|
||||||
|
const {updateRequested, finishUpdate} = useNavigation();
|
||||||
|
|
||||||
/*eslint-disable react-hooks/exhaustive-deps */
|
/*eslint-disable react-hooks/exhaustive-deps */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchMethod(...parameters).then(({data: json, error}) => {
|
fetchMethod(...parameters, updateRequested).then(({data: json, error}) => {
|
||||||
if (json) {
|
if (json) {
|
||||||
setData(json)
|
setData(json);
|
||||||
|
finishUpdate(json.timestamp, json.timestamp_f);
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
setLoadingError(error);
|
setLoadingError(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [fetchMethod, ...parameters])
|
}, [fetchMethod, ...parameters, updateRequested])
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
return {data, loadingError};
|
return {data, loadingError};
|
||||||
|
@ -18,6 +18,7 @@ export const NavigationContextProvider = ({children}) => {
|
|||||||
}, [updating, setUpdateRequested, setUpdating]);
|
}, [updating, setUpdateRequested, setUpdating]);
|
||||||
|
|
||||||
const finishUpdate = useCallback((date, formatted) => {
|
const finishUpdate = useCallback((date, formatted) => {
|
||||||
|
// TODO Logic to retry if received data is too old
|
||||||
setLastUpdate({date, formatted});
|
setLastUpdate({date, formatted});
|
||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
}, [setLastUpdate, setUpdating]);
|
}, [setLastUpdate, setUpdating]);
|
||||||
|
@ -14,13 +14,13 @@ export const fetchPlayersOnlineGraph = async (identifier) => {
|
|||||||
|
|
||||||
export const fetchDayByDayGraph = async (identifier) => {
|
export const fetchDayByDayGraph = async (identifier) => {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const url = `/v1/graph?type=dayByDay&server=${identifier}×tamp=${timestamp}`;
|
const url = `/v1/graph?type=uniqueAndNew&server=${identifier}×tamp=${timestamp}`;
|
||||||
return doGetRequest(url);
|
return doGetRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchHourByHourGraph = async (identifier) => {
|
export const fetchHourByHourGraph = async (identifier) => {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const url = `/v1/graph?type=hourByHour&server=${identifier}×tamp=${timestamp}`;
|
const url = `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}×tamp=${timestamp}`;
|
||||||
return doGetRequest(url);
|
return doGetRequest(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,65 +1,13 @@
|
|||||||
import {useParams} from "react-router-dom";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {fetchPunchCardGraph} from "../service/serverService";
|
import {Col, Row} from "react-bootstrap-v5";
|
||||||
import {Card, Col, Row} from "react-bootstrap-v5";
|
import OnlineActivityGraphsCard from "../components/cards/server/OnlineActivityGraphsCard";
|
||||||
import CardTabs from "../components/CardTabs";
|
|
||||||
import {useTranslation} from "react-i18next";
|
|
||||||
import {faBraille, faChartArea} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import PunchCard from "../components/graphs/PunchCard";
|
|
||||||
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
|
|
||||||
import {useDataRequest} from "../hooks/dataFetchHook";
|
|
||||||
import {ErrorViewBody} from "./ErrorView";
|
|
||||||
|
|
||||||
const DayByDayGraph = () => {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
const HourByHourGraph = () => {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServerCalendar = () => {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServerPunchCard = () => {
|
|
||||||
const {identifier} = useParams();
|
|
||||||
|
|
||||||
const {data, loadingError} = useDataRequest(fetchPunchCardGraph, [identifier])
|
|
||||||
|
|
||||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
|
||||||
if (!data) return <></>;
|
|
||||||
|
|
||||||
return <PunchCard series={data.punchCard}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
const GraphsTabbedCard = () => {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
return <Card><CardTabs
|
|
||||||
tabs={[
|
|
||||||
{
|
|
||||||
name: t('html.label.dayByDay'), icon: faChartArea, color: 'blue', href: 'day-by-day',
|
|
||||||
element: <DayByDayGraph/>
|
|
||||||
}, {
|
|
||||||
name: t('html.label.hourByHour'), icon: faChartArea, color: 'blue', href: 'hour-by-hour',
|
|
||||||
element: <HourByHourGraph/>
|
|
||||||
}, {
|
|
||||||
name: t('html.label.serverCalendar'), icon: faCalendar, color: 'teal', href: 'server-calendar',
|
|
||||||
element: <ServerCalendar/>
|
|
||||||
}, {
|
|
||||||
name: t('html.label.punchcard30days'), icon: faBraille, color: 'black', href: 'punchcard',
|
|
||||||
element: <ServerPunchCard/>
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/></Card>
|
|
||||||
}
|
|
||||||
|
|
||||||
const ServerOnlineActivity = () => {
|
const ServerOnlineActivity = () => {
|
||||||
return (
|
return (
|
||||||
<section className="server_online_activity_overview">
|
<section className="server_online_activity_overview">
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={12}>
|
<Col lg={12}>
|
||||||
<GraphsTabbedCard/>
|
<OnlineActivityGraphsCard/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
Loading…
Reference in New Issue
Block a user