241 lines
10 KiB
JavaScript
241 lines
10 KiB
JavaScript
import React, {useCallback, useEffect, useState} from 'react';
|
|
import {Card, Col, Row} from "react-bootstrap";
|
|
import {useTranslation} from "react-i18next";
|
|
import {useDataRequest} from "../../../hooks/dataFetchHook";
|
|
import {fetchFilters, postQuery} from "../../../service/queryService";
|
|
import {ErrorViewCard} from "../../../views/ErrorView";
|
|
import {ChartLoader} from "../../navigation/Loader";
|
|
import DateInputField from "../../input/DateInputField";
|
|
import TimeInputField from "../../input/TimeInputField";
|
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|
import {faGear, faSearch} from "@fortawesome/free-solid-svg-icons";
|
|
import PlayersOnlineGraph from "../../graphs/PlayersOnlineGraph";
|
|
import Highcharts from "highcharts/highstock";
|
|
import MultiSelect from "../../input/MultiSelect";
|
|
import CollapseWithButton from "../../layout/CollapseWithButton";
|
|
import FilterDropdown from "./FilterDropdown";
|
|
import FilterList from "./FilterList";
|
|
import {useQueryResultContext} from "../../../hooks/queryResultContext";
|
|
import {useNavigate} from "react-router-dom";
|
|
|
|
const parseTime = (dateString, timeString) => {
|
|
const d = dateString.match(
|
|
/^(0\d|\d{2})[/|-]?(0\d|\d{2})[/|-]?(\d{4,5})$/
|
|
);
|
|
const t = timeString.match(/^(0\d|\d{2}):?(0\d|\d{2})$/);
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
|
|
const parsedDay = Number(d[1]);
|
|
const parsedMonth = Number(d[2]) - 1; // 0=January, 11=December
|
|
const parsedYear = Number(d[3]);
|
|
let hour = Number(t[1]);
|
|
let minute = Number(t[2]);
|
|
const date = new Date(parsedYear, parsedMonth, parsedDay, hour, minute);
|
|
return date.getTime() - (date.getTimezoneOffset() * 60000);
|
|
};
|
|
|
|
const QueryOptionsCard = () => {
|
|
const {t} = useTranslation();
|
|
const navigate = useNavigate()
|
|
const {setResult} = useQueryResultContext();
|
|
|
|
const [loadingResults, setLoadingResults] = useState(false);
|
|
|
|
// View state
|
|
const [fromDate, setFromDate] = useState(undefined);
|
|
const [fromTime, setFromTime] = useState(undefined);
|
|
const [toDate, setToDate] = useState(undefined);
|
|
const [toTime, setToTime] = useState(undefined);
|
|
|
|
const [selectedServers, setSelectedServers] = useState([]);
|
|
const [filters, setFilters] = useState([]);
|
|
|
|
// View & filter data
|
|
const {data: options, loadingError} = useDataRequest(fetchFilters, []);
|
|
const [graphData, setGraphData] = useState(undefined);
|
|
useEffect(() => {
|
|
if (options) {
|
|
setGraphData({playersOnline: options.viewPoints, color: '#9E9E9E'})
|
|
}
|
|
}, [options, setGraphData]);
|
|
|
|
// View state handling
|
|
const [invalidFields, setInvalidFields] = useState([]);
|
|
const setAsInvalid = useCallback(id => setInvalidFields([...invalidFields, id]), [setInvalidFields, invalidFields]);
|
|
const setAsValid = useCallback(id => setInvalidFields(invalidFields.filter(invalid => id !== invalid)), [setInvalidFields, invalidFields]);
|
|
|
|
const [extremes, setExtremes] = useState(undefined);
|
|
/*eslint-disable react-hooks/exhaustive-deps */
|
|
// Because: Don't update when any of the date/time fields change because that would lead to infinite loop
|
|
const updateExtremes = useCallback(() => {
|
|
if (invalidFields.length || !options) return;
|
|
if (!fromDate && !fromTime && !toDate && !toTime) return;
|
|
|
|
const newMin = parseTime(
|
|
fromDate ? fromDate : options.view.afterDate,
|
|
fromTime ? fromTime : options.view.afterTime
|
|
);
|
|
const newMax = parseTime(
|
|
toDate ? toDate : options.view.beforeDate,
|
|
toTime ? toTime : options.view.beforeTime
|
|
);
|
|
setExtremes({
|
|
min: newMin,
|
|
max: newMax
|
|
});
|
|
}, [invalidFields, options]);
|
|
/* eslint-enable react-hooks/exhaustive-deps */
|
|
useEffect(updateExtremes, [invalidFields, updateExtremes]);
|
|
|
|
const onSetExtremes = useCallback((event) => {
|
|
if (event && event.trigger) {
|
|
const afterDate = Highcharts.dateFormat('%d/%m/%Y', event.min);
|
|
const afterTime = Highcharts.dateFormat('%H:%M', event.min);
|
|
const beforeDate = Highcharts.dateFormat('%d/%m/%Y', event.max);
|
|
const beforeTime = Highcharts.dateFormat('%H:%M', event.max);
|
|
setFromDate(afterDate);
|
|
setFromTime(afterTime);
|
|
setToDate(beforeDate);
|
|
setToTime(beforeTime);
|
|
}
|
|
}, [setFromTime, setFromDate, setToTime, setToDate]);
|
|
|
|
const getServerSelectorMessage = () => {
|
|
const selected = selectedServers.length;
|
|
const available = options.view.servers.length;
|
|
if (selected === 0 || selected === available) {
|
|
return t('html.query.label.servers.all');
|
|
} else if (selected === 1) {
|
|
return t('html.query.label.servers.single');
|
|
} else if (selected === 2) {
|
|
return t('html.query.label.servers.two');
|
|
} else {
|
|
return t('html.query.label.servers.many').replace('{number}', selected);
|
|
}
|
|
}
|
|
|
|
const performQuery = async () => {
|
|
const inputDto = {
|
|
view: {
|
|
afterDate: fromDate ? fromDate : options.view.afterDate,
|
|
afterTime: fromTime ? fromTime : options.view.afterTime,
|
|
beforeDate: toDate ? toDate : options.view.beforeDate,
|
|
beforeTime: toTime ? toTime : options.view.beforeTime,
|
|
servers: selectedServers.map(index => options.view.servers[index]),
|
|
wantedData: ["players", "activity", "geolocations", "sessions"]
|
|
},
|
|
filters
|
|
}
|
|
|
|
// TODO handle error
|
|
setLoadingResults(true);
|
|
const {data} = await postQuery(inputDto);
|
|
setLoadingResults(false);
|
|
setResult(data);
|
|
window.scrollTo(0, 0);
|
|
if (data?.data) {
|
|
navigate('../result?timestamp=' + data.timestamp);
|
|
}
|
|
}
|
|
|
|
if (loadingError) return <ErrorViewCard error={loadingError}/>
|
|
if (!options) return (<Card>
|
|
<Card.Body>
|
|
<ChartLoader/>
|
|
</Card.Body>
|
|
</Card>)
|
|
|
|
const view = options?.view;
|
|
|
|
return (
|
|
<Card>
|
|
<Card.Body>
|
|
<label>{t('html.query.label.view')}</label>
|
|
<Row className={"my-2 justify-content-start justify-content-md-center"}>
|
|
<Col className={"my-2"}>
|
|
<label>{t('html.query.label.from') // TODO Remove locale hack when the old frontend is disabled
|
|
.replace('</label>', '')
|
|
.replace('>', '')}</label>
|
|
</Col>
|
|
<Col md={3}>
|
|
<DateInputField id={"viewFromDateField"}
|
|
value={fromDate}
|
|
setValue={setFromDate}
|
|
placeholder={view.afterDate}
|
|
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
|
/>
|
|
</Col>
|
|
<Col md={2}>
|
|
<TimeInputField id={"viewFromTimeField"}
|
|
value={fromTime}
|
|
setValue={setFromTime}
|
|
placeholder={view.afterTime}
|
|
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
|
/>
|
|
</Col>
|
|
<Col md={1} className={"my-2 text-center"}>
|
|
<label>{t('html.query.label.to') // TODO Remove locale hack when the old frontend is disabled
|
|
.replace('</label>', '')
|
|
.replace('>', '')}</label>
|
|
</Col>
|
|
<Col md={3}>
|
|
<DateInputField id={"viewToDateField"}
|
|
value={toDate}
|
|
setValue={setToDate}
|
|
placeholder={view.beforeDate}
|
|
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
|
/>
|
|
</Col>
|
|
<Col md={2}>
|
|
<TimeInputField id={"viewToTimeField"}
|
|
value={toTime}
|
|
setValue={setToTime}
|
|
placeholder={view.beforeTime}
|
|
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<Row>
|
|
<Col md={12}>
|
|
<PlayersOnlineGraph
|
|
data={graphData}
|
|
selectedRange={3}
|
|
extremes={extremes}
|
|
onSetExtremes={onSetExtremes}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
<Row>
|
|
<Col md={12}>
|
|
<CollapseWithButton title={getServerSelectorMessage()}>
|
|
<MultiSelect options={view.servers.map(server => server.serverName)}
|
|
selectedIndexes={selectedServers}
|
|
setSelectedIndexes={setSelectedServers}/>
|
|
</CollapseWithButton>
|
|
</Col>
|
|
</Row>
|
|
<hr style={{marginBottom: 0}}/>
|
|
<Row>
|
|
<Col md={12}>
|
|
<FilterList filters={filters} setFilters={setFilters}
|
|
setAsInvalid={setAsInvalid} setAsValid={setAsValid}/>
|
|
</Col>
|
|
</Row>
|
|
<Row>
|
|
<Col md={12}>
|
|
<FilterDropdown filterOptions={options.filters} filters={filters} setFilters={setFilters}/>
|
|
</Col>
|
|
</Row>
|
|
</Card.Body>
|
|
<button id={"query-button"}
|
|
className={"btn bg-theme m-2"}
|
|
disabled={Boolean(invalidFields.length) || loadingResults}
|
|
onClick={performQuery}>
|
|
<FontAwesomeIcon icon={loadingResults ? faGear : faSearch}
|
|
spin={loadingResults}/> {t('html.query.performQuery')}
|
|
</button>
|
|
</Card>
|
|
)
|
|
};
|
|
|
|
export default QueryOptionsCard |