mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-01-03 15:08:12 +01:00
Implement more of server page
- Insights that were unfinished - Playerbase graphs - Pvp & PvE tab
This commit is contained in:
parent
b7d90d2b89
commit
e3c1ca37eb
@ -4,7 +4,7 @@ import End from "./layout/End";
|
||||
|
||||
const Datapoint = ({icon, color, name, value, valueLabel, bold, boldTitle, title, trend}) => {
|
||||
const displayedValue = bold ? <b>{value}</b> : value;
|
||||
const extraLabel = valueLabel instanceof String ? ` (${valueLabel})` : '';
|
||||
const extraLabel = typeof valueLabel === 'string' ? ` (${valueLabel})` : '';
|
||||
const colorClass = color && color.startsWith("col-") ? color : "col-" + color;
|
||||
return (
|
||||
<p title={title ? title : name + " is " + value}>
|
||||
|
@ -7,6 +7,9 @@ import React from "react";
|
||||
|
||||
const PvpKillsTableCard = ({player_kills}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (!player_kills) return <></>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
|
@ -7,6 +7,7 @@ import {useTranslation} from "react-i18next";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import PlayerbasePie from "../../../graphs/PlayerbasePie";
|
||||
|
||||
const CurrentPlayerbaseCard = () => {
|
||||
const {t} = useTranslation();
|
||||
@ -24,7 +25,7 @@ const CurrentPlayerbaseCard = () => {
|
||||
<Fa icon={faUsers} className="col-amber"/> {t('html.label.currentPlayerbase')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
|
||||
<PlayerbasePie series={data.activity_pie_series}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faChartArea} from "@fortawesome/free-solid-svg-icons";
|
||||
import PlayersOnlineGraph from "../../../graphs/PlayersOnlineGraph";
|
||||
import React from "react";
|
||||
import {CardLoader} from "../../../navigation/Loader";
|
||||
|
||||
const OnlineActivityCard = () => {
|
||||
const {t} = useTranslation();
|
||||
@ -17,8 +18,8 @@ const OnlineActivityCard = () => {
|
||||
fetchPlayersOnlineGraph,
|
||||
[identifier])
|
||||
|
||||
if (!data) return <></>;
|
||||
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@ -16,6 +16,7 @@ import {faCalendar} from "@fortawesome/free-regular-svg-icons";
|
||||
import React from "react";
|
||||
import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph";
|
||||
import ServerCalendar from "../../../calendar/ServerCalendar";
|
||||
import {ChartLoader} from "../../../navigation/Loader";
|
||||
|
||||
const DayByDayTab = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -23,7 +24,7 @@ const DayByDayTab = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchDayByDayGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <></>;
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <TimeByTimeGraph data={data}/>
|
||||
}
|
||||
@ -34,7 +35,7 @@ const HourByHourTab = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchHourByHourGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <></>;
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <TimeByTimeGraph data={data}/>
|
||||
}
|
||||
@ -45,7 +46,7 @@ const ServerCalendarTab = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchServerCalendarGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <></>;
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <ServerCalendar series={data.data} firstDay={data.firstDay}/>
|
||||
}
|
||||
@ -56,7 +57,7 @@ const PunchCardTab = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchPunchCardGraph, [identifier])
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <></>;
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return <PunchCard series={data.punchCard}/>
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import {ErrorViewCard} from "../../../../views/ErrorView";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import {faChartLine} from "@fortawesome/free-solid-svg-icons";
|
||||
import PlayersOnlineGraph from "../../../graphs/PlayersOnlineGraph";
|
||||
import React from "react";
|
||||
import PlayerbaseGraph from "../../../graphs/PlayerbaseGraph";
|
||||
|
||||
const PlayerbaseDevelopmentCard = () => {
|
||||
const {t} = useTranslation();
|
||||
@ -27,7 +27,7 @@ const PlayerbaseDevelopmentCard = () => {
|
||||
<Fa className="col-amber" icon={faChartLine}/> {t('html.label.playerbaseDevelopment')}
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<PlayersOnlineGraph data={data}/>
|
||||
<PlayerbaseGraph data={data}/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchWorldPie} from "../../../../service/serverService";
|
||||
import {ErrorViewBody} from "../../../../views/ErrorView";
|
||||
import {CardLoader} from "../../../navigation/Loader";
|
||||
|
||||
const ServerWorldPieCard = () => {
|
||||
const {identifier} = useParams();
|
||||
@ -11,7 +12,7 @@ const ServerWorldPieCard = () => {
|
||||
const {data, loadingError} = useDataRequest(fetchWorldPie, [identifier]);
|
||||
|
||||
if (loadingError) return <ErrorViewBody error={loadingError}/>
|
||||
if (!data) return <></>;
|
||||
if (!data) return <CardLoader/>;
|
||||
|
||||
return (
|
||||
<WorldPieCard
|
||||
|
@ -1,10 +1,20 @@
|
||||
import React from "react";
|
||||
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
|
||||
import Datapoint from "../../../Datapoint";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {faKhanda} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const PvpPveInsightsCard = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const PvpPveInsightsCard = () => {
|
||||
return (
|
||||
<InsightsFor30DaysCard>
|
||||
TODO
|
||||
<Datapoint name={t('html.label.deadliestWeapon')} icon={faKhanda} color="amber"
|
||||
value={data.weapon_1st}/>
|
||||
<Datapoint name={t('html.label.secondDeadliestWeapon')} icon={faKhanda} color="gray"
|
||||
value={data.weapon_2nd}/>
|
||||
<Datapoint name={t('html.label.thirdDeadliestWeapon')} icon={faKhanda} color="brown"
|
||||
value={data.weapon_3rd}/>
|
||||
</InsightsFor30DaysCard>
|
||||
)
|
||||
}
|
||||
|
@ -1,10 +1,37 @@
|
||||
import React from "react";
|
||||
import InsightsFor30DaysCard from "../../common/InsightsFor30DaysCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../../../hooks/dataFetchHook";
|
||||
import {fetchSessionOverview} from "../../../../service/serverService";
|
||||
import ErrorView from "../../../../views/ErrorView";
|
||||
import Datapoint from "../../../Datapoint";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {faGamepad, faUsers} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faClock} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const SessionInsightsCard = () => {
|
||||
const {identifier} = useParams();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchSessionOverview, [identifier]);
|
||||
|
||||
if (!data) return <></>;
|
||||
if (loadingError) return <ErrorView error={loadingError}/>
|
||||
|
||||
return (
|
||||
<InsightsFor30DaysCard>
|
||||
TODO
|
||||
<Datapoint name={t('html.label.mostActiveGamemode')} icon={faGamepad} color="teal" bold
|
||||
value={data.insights.most_active_gamemode} valueLabel={data.insights.most_active_gamemode_perc}
|
||||
/>
|
||||
<Datapoint name={t('html.label.serverOccupied')} icon={faUsers} color="teal"
|
||||
value={'~' + data.insights.server_occupied} valueLabel={data.insights.server_occupied_perc}
|
||||
/>
|
||||
<Datapoint name={t('html.label.playtime')} icon={faClock} color="green"
|
||||
value={data.insights.total_playtime}
|
||||
/>
|
||||
<Datapoint name={t('html.label.afkTime')} icon={faClock} color="grey"
|
||||
value={data.insights.afk_time} valueLabel={data.insights.afk_time_perc}
|
||||
/>
|
||||
</InsightsFor30DaysCard>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
import {useTranslation} from "react-i18next";
|
||||
import React, {useEffect} from "react";
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import NoDataDisplay from "highcharts/modules/no-data-to-display";
|
||||
import Highcharts from "highcharts/highstock";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
|
||||
const PlayerbaseGraph = ({data}) => {
|
||||
const {t} = useTranslation()
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
const id = 'playerbase-graph';
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (series) => series.map(slice => {
|
||||
return {...slice, color: withReducedSaturation(slice.color)}
|
||||
});
|
||||
|
||||
NoDataDisplay(Highcharts);
|
||||
Highcharts.setOptions({lang: {noData: t('html.label.noDataToDisplay')}})
|
||||
Highcharts.setOptions(graphTheming);
|
||||
|
||||
const labels = data?.activity_labels;
|
||||
const series = data?.activity_series;
|
||||
|
||||
Highcharts.chart(id, {
|
||||
chart: {
|
||||
type: "area"
|
||||
},
|
||||
xAxis: {
|
||||
categories: labels,
|
||||
tickmarkPlacement: 'on',
|
||||
title: {
|
||||
enabled: false
|
||||
},
|
||||
ordinal: false
|
||||
},
|
||||
yAxis: {
|
||||
softMax: 2,
|
||||
softMin: 0
|
||||
},
|
||||
title: {text: ''},
|
||||
plotOptions: {
|
||||
area: {
|
||||
stacking: 'normal',
|
||||
lineWidth: 1
|
||||
}
|
||||
},
|
||||
series: nightModeEnabled ? reduceColors(series) : series
|
||||
})
|
||||
}, [data, graphTheming, id, t])
|
||||
|
||||
return (
|
||||
<div className="chart-area" id={id}>
|
||||
<span className="loader"/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayerbaseGraph;
|
48
Plan/react/dashboard/src/components/graphs/PlayerbasePie.js
Normal file
48
Plan/react/dashboard/src/components/graphs/PlayerbasePie.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React, {useEffect} from "react";
|
||||
import Highcharts from 'highcharts';
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const PlayerbasePie = ({series}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (series) => series.map(slice => {
|
||||
return {...slice, color: withReducedSaturation(slice.color)}
|
||||
});
|
||||
|
||||
const pieSeries = {
|
||||
name: t('html.label.players'),
|
||||
colorByPoint: true,
|
||||
data: nightModeEnabled ? reduceColors(series) : series
|
||||
};
|
||||
|
||||
Highcharts.setOptions(graphTheming);
|
||||
Highcharts.chart('playerbase-pie', {
|
||||
chart: {
|
||||
backgroundColor: 'transparent',
|
||||
plotBorderWidth: null,
|
||||
plotShadow: false,
|
||||
type: 'pie'
|
||||
},
|
||||
title: {text: ''},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
allowPointSelect: true,
|
||||
cursor: 'pointer',
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
showInLegend: true
|
||||
}
|
||||
},
|
||||
series: [pieSeries]
|
||||
});
|
||||
}, [series, graphTheming, nightModeEnabled]);
|
||||
|
||||
return (<div className="chart-area" id="playerbase-pie"/>);
|
||||
}
|
||||
|
||||
export default PlayerbasePie;
|
@ -2,6 +2,7 @@ import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {tooltip} from "../../util/graphs";
|
||||
import LineGraph from "./LineGraph";
|
||||
import {ChartLoader} from "../navigation/Loader";
|
||||
|
||||
const PlayersOnlineGraph = ({data}) => {
|
||||
const {t} = useTranslation();
|
||||
@ -19,6 +20,8 @@ const PlayersOnlineGraph = ({data}) => {
|
||||
setSeries([playersOnlineSeries]);
|
||||
}, [data, t])
|
||||
|
||||
if (!data) return <ChartLoader/>;
|
||||
|
||||
return (
|
||||
<LineGraph id="players-online-graph" series={series}/>
|
||||
)
|
||||
|
@ -4,15 +4,17 @@ import Highcharts from 'highcharts';
|
||||
import {formatTimeAmount} from '../../util/formatters'
|
||||
import {useTheme} from "../../hooks/themeHook";
|
||||
import {withReducedSaturation} from "../../util/colors";
|
||||
import {useTranslation} from "react-i18next";
|
||||
|
||||
const ServerPie = ({colors, series}) => {
|
||||
const {t} = useTranslation();
|
||||
const {nightModeEnabled, graphTheming} = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const reduceColors = (colorsToReduce) => colorsToReduce.map(color => withReducedSaturation(color));
|
||||
|
||||
const pieSeries = {
|
||||
name: 'Server Playtime',
|
||||
name: t('html.label.serverPlaytime'),
|
||||
colorByPoint: true,
|
||||
colors: nightModeEnabled ? reduceColors(colors) : colors,
|
||||
data: series
|
||||
|
29
Plan/react/dashboard/src/components/navigation/Loader.js
Normal file
29
Plan/react/dashboard/src/components/navigation/Loader.js
Normal file
@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import {Card} from "react-bootstrap-v5";
|
||||
|
||||
export const CardLoader = () => {
|
||||
return (
|
||||
<Card className="loading">
|
||||
<Card.Header>
|
||||
<h6 className="col-black">
|
||||
...
|
||||
</h6>
|
||||
</Card.Header>
|
||||
<ChartLoader/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChartLoader = () => {
|
||||
return <div className="chart-area loading">
|
||||
<Loader/>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Loader = () => {
|
||||
return (
|
||||
<span className="loader"/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loader;
|
@ -11,9 +11,9 @@ const ServerPvpPveAsNumbersTable = ({killData}) => {
|
||||
headers={[t('html.label.allTime'), t('html.label.last30days'), t('html.label.last7days')]}
|
||||
>
|
||||
<TableRow icon={faCrosshairs} color="red" text={t('html.label.averageKdr')}
|
||||
values={[killData.player_kdr_total,
|
||||
killData.player_kdr_30d,
|
||||
killData.player_kdr_7d]}/>
|
||||
values={[killData.player_kdr_avg,
|
||||
killData.player_kdr_avg_30d,
|
||||
killData.player_kdr_avg_7d]}/>
|
||||
<TableRow icon={faCrosshairs} color="red" text={t('html.label.playerKills')}
|
||||
values={[killData.player_kills_total,
|
||||
killData.player_kills_30d,
|
||||
|
@ -18,12 +18,30 @@ export const fetchPlayerbaseOverview = async (identifier) => {
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchSessionOverview = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/sessionsOverview?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPvpPve = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/playerVersus?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchSessions = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/sessions?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchKills = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/kills?server=${identifier}×tamp=${timestamp}`;
|
||||
return doGetRequest(url);
|
||||
}
|
||||
|
||||
export const fetchPlayersOnlineGraph = async (identifier) => {
|
||||
const timestamp = Date.now();
|
||||
const url = `/v1/graph?type=playersOnline&server=${identifier}×tamp=${timestamp}`;
|
||||
|
@ -3,17 +3,36 @@ import PvpPveAsNumbersCard from "../../components/cards/server/tables/PvpPveAsNu
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import PvpKillsTableCard from "../../components/cards/common/PvpKillsTableCard";
|
||||
import PvpPveInsightsCard from "../../components/cards/server/insights/PvpPveInsightsCard";
|
||||
import {useParams} from "react-router-dom";
|
||||
import {useDataRequest} from "../../hooks/dataFetchHook";
|
||||
import {fetchKills, fetchPvpPve} from "../../service/serverService";
|
||||
import ErrorView from "../ErrorView";
|
||||
|
||||
const ServerPvpPve = () => {
|
||||
const {identifier} = useParams();
|
||||
|
||||
const {data, loadingError} = useDataRequest(fetchPvpPve, [identifier]);
|
||||
const {data: killsData, loadingError: killsLoadingError} = useDataRequest(fetchKills, [identifier]);
|
||||
|
||||
console.log(killsData)
|
||||
|
||||
if (!data || !killsData) return <></>;
|
||||
if (loadingError) return <ErrorView error={loadingError}/>
|
||||
if (killsLoadingError) return <ErrorView error={killsLoadingError}/>
|
||||
|
||||
return (
|
||||
<section className="server_pvp_pve">
|
||||
<Row>
|
||||
<Col lg={8}>
|
||||
<PvpPveAsNumbersCard kill_data={{}}/>
|
||||
<PvpKillsTableCard player_kills={[]}/>
|
||||
<PvpPveAsNumbersCard kill_data={data?.numbers}/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<PvpPveInsightsCard/>
|
||||
<PvpPveInsightsCard data={data?.insights}/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col lg={8}>
|
||||
<PvpKillsTableCard player_kills={killsData?.player_kills}/>
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
|
Loading…
Reference in New Issue
Block a user