mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-23 01:27:42 +01:00
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:
parent
a937d64ca1
commit
653ea6d481
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 = () => {
|
||||
|
@ -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 = () => {
|
||||
|
@ -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
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user