Implement more of server page

- Insights that were unfinished
- Playerbase graphs
- Pvp & PvE tab
This commit is contained in:
Aurora Lahtela 2022-06-02 16:43:32 +03:00
parent b7d90d2b89
commit e3c1ca37eb
17 changed files with 243 additions and 20 deletions

View File

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

View File

@ -7,6 +7,9 @@ import React from "react";
const PvpKillsTableCard = ({player_kills}) => {
const {t} = useTranslation();
if (!player_kills) return <></>;
return (
<Card>
<Card.Header>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

@ -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}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPvpPve = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/playerVersus?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchSessions = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/sessions?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchKills = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/kills?server=${identifier}&timestamp=${timestamp}`;
return doGetRequest(url);
}
export const fetchPlayersOnlineGraph = async (identifier) => {
const timestamp = Date.now();
const url = `/v1/graph?type=playersOnline&server=${identifier}&timestamp=${timestamp}`;

View File

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