Click & Drag on server calendar to query player data

Clicking and dragging on server calendar will select start and end time to use for
a Query Page View and Played Between filter.
This data is then displayed in a modal for easy viewing.

This allows viewing who was online on specific day(s), how much they played, and other information about the players.

Affects issues:
- Close #1531
This commit is contained in:
Aurora Lahtela 2023-10-08 11:12:47 +03:00
parent a937d64ca1
commit 653ea6d481
9 changed files with 191 additions and 48 deletions

View File

@ -12,9 +12,11 @@
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@fullcalendar/bootstrap": "^5.11.5",
"@fullcalendar/daygrid": "^5.11.5",
"@fullcalendar/react": "^5.11.5",
"@fullcalendar/bootstrap": "^6.1.9",
"@fullcalendar/core": "^6.1.9",
"@fullcalendar/daygrid": "^6.1.9",
"@fullcalendar/interaction": "^6.1.9",
"@fullcalendar/react": "^6.1.9",
"@highcharts/map-collection": "^2.1.0",
"@testing-library/jest-dom": "^6.1.3",
"@testing-library/react": "^14.0.0",

View File

@ -1,12 +1,13 @@
import React from "react";
import FullCalendar from '@fullcalendar/react'
import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction'
const ServerCalendar = ({series, firstDay}) => {
const ServerCalendar = ({series, firstDay, onSelect}) => {
return (
<div id={'server-calendar'}>
<FullCalendar
plugins={[dayGridPlugin]}
plugins={[interactionPlugin, dayGridPlugin]}
timeZone="UTC"
themeSystem='bootstrap'
eventColor='#2196F3'
@ -21,6 +22,10 @@ const ServerCalendar = ({series, firstDay}) => {
center: '',
right: 'dayGridMonth dayGridWeek dayGridDay today prev next'
}}
editable={Boolean(onSelect)}
selectable={Boolean(onSelect)}
select={onSelect}
unselectAuto={true}
events={(_fetchInfo, successCallback) => successCallback(series)}
/>
</div>

View File

@ -30,7 +30,7 @@ const getActivityGroup = value => {
}
}
const PlayerListCard = ({data, title}) => {
const PlayerListCard = ({data, title, justList, orderBy}) => {
const {t} = useTranslation();
const [options, setOptions] = useState(undefined);
@ -95,9 +95,9 @@ const PlayerListCard = ({data, title}) => {
deferRender: true,
columns: columns,
data: rows,
order: [[5, "desc"]]
order: [[orderBy !== undefined ? orderBy : 5, "desc"]]
});
}, [data, t]);
}, [data, orderBy, t]);
const rowKeyFunction = useCallback((row, column) => {
return row.uuid + "-" + (column ? JSON.stringify(column.data) : '');
@ -105,6 +105,12 @@ const PlayerListCard = ({data, title}) => {
if (!options) return <CardLoader/>
if (justList) {
return (
<DataTablesTable id={"players-table"} rowKeyFunction={rowKeyFunction} options={options}/>
);
}
return (
<Card>
<Card.Header>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, {useCallback, useState} from 'react';
import {useTranslation} from "react-i18next";
import {Card} from "react-bootstrap";
import CardTabs from "../../../CardTabs";
@ -19,6 +19,9 @@ import StackedPlayersOnlineGraph from "../../../graphs/StackedPlayersOnlineGraph
import {useAuth} from "../../../../hooks/authenticationHook";
import {faCalendar} from "@fortawesome/free-regular-svg-icons";
import ServerCalendar from "../../../calendar/ServerCalendar";
import {postQuery} from "../../../../service/queryService";
import Highcharts from "highcharts/highstock";
import QueryPlayerListModal from "../../../modal/QueryPlayerListModal";
const SingleProxyPlayersOnlineGraph = ({serverUUID}) => {
const {data, loadingError} = useDataRequest(fetchPlayersOnlineGraph, [serverUUID]);
@ -69,13 +72,47 @@ const HourByHourTab = () => {
}
const NetworkCalendarTab = () => {
const {data, loadingError} = useDataRequest(fetchNetworkCalendarGraph, []);
const [modalOpen, setModalOpen] = useState(false);
const [queryData, setQueryData] = useState(undefined);
const {data, loadingError} = useDataRequest(fetchNetworkCalendarGraph, [])
const closeModal = useCallback(() => {
setModalOpen(false);
}, [setModalOpen]);
const onSelect = useCallback(async selectionInfo => {
const start = Highcharts.dateFormat('%d/%m/%Y', selectionInfo.start);
const end = Highcharts.dateFormat('%d/%m/%Y', selectionInfo.end);
const query = {
filters: [{
kind: "playedBetween",
parameters: {
afterDate: start, afterTime: "00:00",
beforeDate: end, beforeTime: "00:00"
}
}],
view: {
afterDate: start, afterTime: "00:00",
beforeDate: end, beforeTime: "00:00",
servers: []
}
}
setQueryData(undefined);
setModalOpen(true);
const data = await postQuery(query);
const loaded = data?.data;
if (loaded) {
setQueryData(loaded);
}
}, [setQueryData, setModalOpen]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader/>;
return <ServerCalendar series={data.data} firstDay={data.firstDay}/>
return <>
<ServerCalendar series={data.data} firstDay={data.firstDay} onSelect={onSelect}/>
<QueryPlayerListModal open={modalOpen} toggle={closeModal} queryData={queryData}/>
</>
}
const NetworkOnlineActivityGraphsCard = () => {

View File

@ -13,11 +13,15 @@ import {Card} from "react-bootstrap";
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 React, {useCallback, useState} from "react";
import TimeByTimeGraph from "../../../graphs/TimeByTimeGraph";
import ServerCalendar from "../../../calendar/ServerCalendar";
import {ChartLoader} from "../../../navigation/Loader";
import {useAuth} from "../../../../hooks/authenticationHook";
import Highcharts from "highcharts/highstock";
import {postQuery} from "../../../../service/queryService";
import QueryPlayerListModal from "../../../modal/QueryPlayerListModal";
import {useMetadata} from "../../../../hooks/metadataHook";
const DayByDayTab = () => {
const {identifier} = useParams();
@ -38,18 +42,54 @@ const HourByHourTab = () => {
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader/>;
return <TimeByTimeGraph id={"hour-by-hour-graph"}data={data}/>
return <TimeByTimeGraph id={"hour-by-hour-graph"} data={data}/>
}
const ServerCalendarTab = () => {
const {identifier} = useParams();
const {data, loadingError} = useDataRequest(fetchServerCalendarGraph, [identifier]);
const {networkMetadata} = useMetadata();
const {data, loadingError} = useDataRequest(fetchServerCalendarGraph, [identifier])
const [modalOpen, setModalOpen] = useState(false);
const [queryData, setQueryData] = useState(undefined);
const closeModal = useCallback(() => {
setModalOpen(false);
}, [setModalOpen]);
const onSelect = useCallback(async selectionInfo => {
const start = Highcharts.dateFormat('%d/%m/%Y', selectionInfo.start);
const end = Highcharts.dateFormat('%d/%m/%Y', selectionInfo.end);
const query = {
filters: [{
kind: "playedBetween",
parameters: {
afterDate: start, afterTime: "00:00",
beforeDate: end, beforeTime: "00:00"
}
}],
view: {
afterDate: start, afterTime: "00:00",
beforeDate: end, beforeTime: "00:00",
servers: networkMetadata?.servers.filter(server => server.serverUUID === identifier) || []
}
}
setQueryData(undefined);
setModalOpen(true);
const data = await postQuery(query);
const loaded = data?.data;
if (loaded) {
setQueryData(loaded);
}
}, [setQueryData, setModalOpen, networkMetadata, identifier]);
if (loadingError) return <ErrorViewBody error={loadingError}/>
if (!data) return <ChartLoader/>;
return <ServerCalendar series={data.data} firstDay={data.firstDay}/>
return <>
<ServerCalendar series={data.data} firstDay={data.firstDay} onSelect={onSelect}/>
<QueryPlayerListModal open={modalOpen} toggle={closeModal} queryData={queryData}/>
</>
}
const PunchCardTab = () => {

View File

@ -0,0 +1,31 @@
import React from 'react';
import {useTranslation} from "react-i18next";
import {Modal} from "react-bootstrap";
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
import {faSearch} from "@fortawesome/free-solid-svg-icons";
import PlayerListCard from "../cards/common/PlayerListCard";
import {getViewTitle} from "../../views/query/QueryResultView";
import {ChartLoader} from "../navigation/Loader";
const QueryPlayerListModal = ({open, toggle, queryData}) => {
const {t} = useTranslation();
return (
<Modal id="queryModal" aria-labelledby="queryModalLabel" show={open} onHide={toggle} size="xl">
<Modal.Header>
<Modal.Title id="queryModalLabel">
<Fa icon={faSearch}/> {queryData ? getViewTitle(queryData, t, true) : t('html.query.title.text').replace('<', '')}
</Modal.Title>
<button aria-label="Close" className="btn-close" type="button" onClick={toggle}/>
</Modal.Header>
{!queryData && <ChartLoader/>}
{queryData &&
<PlayerListCard justList data={queryData?.data?.players || {players: [], extensionDescriptors: []}}
orderBy={2}/>}
<Modal.Footer>
<button className="btn bg-theme" onClick={toggle}>OK</button>
</Modal.Footer>
</Modal>
)
};
export default QueryPlayerListModal

View File

@ -218,6 +218,11 @@ const DataTablesTable = ({id, rowKeyFunction, options}) => {
</tr>
</thead>
<tbody id={id + '-body'}>
{!rows.length && <tr>
{visibleColumns.map((column, i) => <td key={"col-" + rowKeyFunction(0, column)}>
{i === 0 && t('html.label.noDataToDisplay')}
</td>)}
</tr>}
{rows.map(row => <React.Fragment key={"frag-" + rowKeyFunction(row, null)}>
<tr key={"row-" + rowKeyFunction(row, null)}>
{visibleColumns.map((column, i) => {

View File

@ -13,6 +13,27 @@ import GeolocationsCard from "../../components/cards/common/GeolocationsCard";
import SessionsWithinViewCard from "../../components/cards/query/SessionsWithinViewCard";
import {useNavigation} from "../../hooks/navigationHook";
const serverCount = (count, t) => {
if (count === 0) {
return t('html.query.label.servers.all');
} else if (count === 1) {
return t('html.query.label.servers.single');
} else if (count === 2) {
return t('html.query.label.servers.two');
} else {
return t('html.query.label.servers.many').replace('{number}', count);
}
}
export const getViewTitle = (result, t, showTime) => {
if (!result) return '';
return 'View: ' + result.view.afterDate + (showTime ? ', ' + result.view.afterTime : '') +
" - " + result.view.beforeDate + (showTime ? ', ' + result.view.beforeTime : '') + ', ' +
serverCount(result.view.servers.length, t) +
(result.view.servers.length ? ': ' + result.view.servers.map(server => server.serverName).join(', ') : '')
}
const QueryResultView = () => {
const {t} = useTranslation();
const navigate = useNavigate();
@ -49,11 +70,6 @@ const QueryResultView = () => {
return <></>
}
const getViewTitle = () => {
return 'View: ' + result.view.afterDate + " - " + result.view.beforeDate + ', ' +
(result.view.servers.len ? 'using data of servers: ' + result.view.servers.map(server => server.name).join(', ') : "using data of all servers")
}
return (
<LoadIn>
<section className={"query-results-view"}>
@ -62,7 +78,7 @@ const QueryResultView = () => {
<QueryPath/>
<PlayerListCard
data={result.data.players}
title={getViewTitle()}
title={getViewTitle(result)}
/>
</Col>
</Row>

View File

@ -1363,36 +1363,32 @@
dependencies:
prop-types "^15.8.1"
"@fullcalendar/bootstrap@^5.11.5":
version "5.11.5"
resolved "https://registry.yarnpkg.com/@fullcalendar/bootstrap/-/bootstrap-5.11.5.tgz#97de88d5b7821d575fd6259e826d8f8b9a3f3e9b"
integrity sha512-btsVVJuLQ9UNTj0SeHNHxwM3uzfHi6RsjtMQs13sXzp3tu2d6FTI849VJHs81GfTZp+fwK0RD2tZ/eswN58A5Q==
dependencies:
"@fullcalendar/common" "~5.11.5"
tslib "^2.1.0"
"@fullcalendar/bootstrap@^6.1.9":
version "6.1.9"
resolved "https://registry.yarnpkg.com/@fullcalendar/bootstrap/-/bootstrap-6.1.9.tgz#53a45ff0fc1090155caa99d94a6065b77dbe7813"
integrity sha512-NcXc6uH2cqeB3YaCHDut6Krjl9tSN0zbH/PFuKpslbA4FMZOPKtxbF5uh9Vs+B7HyCY4sBrKzPAQWGAcYf1exQ==
"@fullcalendar/common@~5.11.5":
version "5.11.5"
resolved "https://registry.yarnpkg.com/@fullcalendar/common/-/common-5.11.5.tgz#1a30a852b33ab5c1b04f4ee558941bed3c72d07f"
integrity sha512-3iAYiUbHXhjSVXnYWz27Od2cslztUPsOwiwKlfGvQxBixv2Kl6a8IPwaijKFYJHXdwYmfPoEgK7rvqAGVoIYwA==
"@fullcalendar/core@^6.1.9":
version "6.1.9"
resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.9.tgz#ea735b0dd0a0a487969ebbb6c99b0967e07568c0"
integrity sha512-eeG+z9BWerdsU9Ac6j16rpYpPnE0wxtnEHiHrh/u/ADbGTR3hCOjCD9PxQOfhOTHbWOVs7JQunGcksSPu5WZBQ==
dependencies:
tslib "^2.1.0"
preact "~10.12.1"
"@fullcalendar/daygrid@^5.11.5":
version "5.11.5"
resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-5.11.5.tgz#2825ed691eadf72c6a2979bcde871de74681fc7a"
integrity sha512-hMpq0U3Nucys2jDD+crbkJCr+tVt3fDw04OE3fbpisuzqtrHxIzRmnUOdbWUjJQyToAAkt7UVUQ9E7hYdmvyGA==
dependencies:
"@fullcalendar/common" "~5.11.5"
tslib "^2.1.0"
"@fullcalendar/daygrid@^6.1.9":
version "6.1.9"
resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-6.1.9.tgz#efb8aabb2f928ac0b05a77c5443accb546ae5818"
integrity sha512-o/6joH/7lmVHXAkbaa/tUbzWYnGp/LgfdiFyYPkqQbjKEeivNZWF1WhHqFbhx0zbFONSHtrvkjY2bjr+Ef2quQ==
"@fullcalendar/react@^5.11.5":
version "5.11.5"
resolved "https://registry.yarnpkg.com/@fullcalendar/react/-/react-5.11.5.tgz#36e2f7b6d92dd1d8120003c6822323c32cb7223a"
integrity sha512-PbBlDyKJ8IQYf5mBdD1mjDas2v3eEU1UfWYLv0e6uGCktH+g4mgaG/LCDOwE65V5VH5FH8+kVkFjIScwA54WwA==
dependencies:
"@fullcalendar/common" "~5.11.5"
tslib "^2.1.0"
"@fullcalendar/interaction@^6.1.9":
version "6.1.9"
resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-6.1.9.tgz#9023922df24c296cb7f4671887f1731f5d5a5db2"
integrity sha512-I3FGnv0kKZpIwujg3HllbKrciNjTqeTYy3oJG226oAn7lV6wnrrDYMmuGmA0jPJAGN46HKrQqKN7ItxQRDec4Q==
"@fullcalendar/react@^6.1.9":
version "6.1.9"
resolved "https://registry.yarnpkg.com/@fullcalendar/react/-/react-6.1.9.tgz#280fd543901d792c19b50f363c55cc3068917299"
integrity sha512-ioxu0V++pYz2u/N1LL1V8DkMyiKGRun0gMAll2tQz3Kzi3r9pTwncGKRb1zO8h0e+TrInU08ywk/l5lBwp7eog==
"@highcharts/map-collection@^2.1.0":
version "2.1.0"
@ -8335,6 +8331,11 @@ postcss@^8.3.5, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.4:
picocolors "^1.0.0"
source-map-js "^1.0.2"
preact@~10.12.1:
version "10.12.1"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21"
integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==
prebuild-install@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
@ -10183,7 +10184,7 @@ tslib@^2.0.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
tslib@^2.3.0, tslib@^2.4.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==