mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2024-12-27 05:58:55 +08:00
Implemented Query page filter options
This commit is contained in:
parent
8c1a357323
commit
f4bd580840
@ -56,6 +56,10 @@ public enum JSLang implements Lang {
|
||||
QUERY_ACTIVITY_ON("html.query.title.activityOnDate", "Activity on <span id=\"activity-date\"></span>"),
|
||||
QUERY_ARE("html.query.generic.are", "`are`"),
|
||||
QUERY_SESSIONS_WITHIN_VIEW("html.query.title.sessionsWithinView", "Sessions within view"),
|
||||
QUERY_HAS_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.name", "Has plugin boolean value"),
|
||||
QUERY_HAVE_PLUGIN_BOOLEAN_VALUE("html.query.filter.hasPluginBooleanValue.text", "have Plugin boolean value"),
|
||||
QUERY_HAS_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.name", "Has played on one of servers"),
|
||||
QUERY_HAVE_PLAYED_ON_SERVERS("html.query.filter.hasPlayedOnServers.text", "have played on at least one of"),
|
||||
|
||||
FILTER_GROUP("html.query.filter.pluginGroup.name", "Group: "),
|
||||
FILTER_ALL_PLAYERS("html.query.filter.generic.allPlayers", "All players"),
|
||||
|
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import DropdownToggle from "react-bootstrap-v5/lib/esm/DropdownToggle";
|
||||
import {FontAwesomeIcon as Fa} from "@fortawesome/react-fontawesome";
|
||||
import DropdownMenu from "react-bootstrap-v5/lib/esm/DropdownMenu";
|
||||
import DropdownItem from "react-bootstrap-v5/lib/esm/DropdownItem";
|
||||
import {Dropdown} from "react-bootstrap-v5";
|
||||
import {faPlus} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import Scrollable from "../../Scrollable";
|
||||
|
||||
const FilterDropdown = ({filterOptions, filters, setFilters}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
const addFilter = filter => {
|
||||
setFilters([...filters, filter])
|
||||
}
|
||||
|
||||
const getReadableFilterName = filter => {
|
||||
if (filter.kind.startsWith("pluginGroups-")) {
|
||||
return t('html.query.filter.pluginGroup.name') + filter.kind.substring(13);
|
||||
}
|
||||
switch (filter.kind) {
|
||||
case "allPlayers":
|
||||
return t('html.query.filter.generic.allPlayers')
|
||||
case "activityIndexNow":
|
||||
return t('html.query.filter.title.activityGroup');
|
||||
case "banned":
|
||||
return t('html.query.filter.banStatus.name');
|
||||
case "operators":
|
||||
return t('html.query.filter.operatorStatus.name');
|
||||
case "joinAddresses":
|
||||
return t('html.label.joinAddresses');
|
||||
case "geolocations":
|
||||
return t('html.label.geolocations');
|
||||
case "playedBetween":
|
||||
return t('html.query.filter.playedBetween.text');
|
||||
case "registeredBetween":
|
||||
return t('html.query.filter.registeredBetween.text');
|
||||
case "pluginsBooleanGroups":
|
||||
return t('html.query.filter.hasPluginBooleanValue.name');
|
||||
case "playedOnServer":
|
||||
return t('html.query.filter.hasPlayedOnServers.name');
|
||||
default:
|
||||
return filter.kind;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownToggle variant=''>
|
||||
<Fa icon={faPlus}/> {t('html.query.filters.add')}
|
||||
</DropdownToggle>
|
||||
|
||||
<DropdownMenu popperConfig={{strategy: "absolute"}}>
|
||||
<h6 className="dropdown-header">{t('html.query.filters.add')}</h6>
|
||||
<Scrollable>
|
||||
{filterOptions.map((option, i) => (
|
||||
<DropdownItem key={i} onClick={() => addFilter(option)}>
|
||||
{getReadableFilterName(option)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Scrollable>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
};
|
||||
|
||||
export default FilterDropdown
|
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import Filter from "./filter/Filter";
|
||||
|
||||
const FilterList = ({filters, setFilters, setAsInvalid, setAsValid}) => {
|
||||
const updateFilterOptions = (index, newOptions) => {
|
||||
filters[index] = newOptions;
|
||||
setFilters(filters);
|
||||
}
|
||||
|
||||
const removeFilter = index => {
|
||||
setFilters(filters.filter((f, i) => i !== index));
|
||||
}
|
||||
|
||||
const moveUp = index => {
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
[filters[index - 1], filters[index]] = [filters[index], filters[index - 1]];
|
||||
setFilters(filters);
|
||||
}
|
||||
|
||||
const moveDown = index => {
|
||||
if (index === filters.length - 1) {
|
||||
return;
|
||||
}
|
||||
[filters[index], filters[index + 1]] = [filters[index + 1], filters[index]];
|
||||
setFilters(filters);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul id={"filters"} className={"filters"}>
|
||||
{filters.map((filter, i) => <li className={"filter"}>
|
||||
<Filter filter={filter} key={i} index={i}
|
||||
setFilterOptions={newOptions => updateFilterOptions(i, newOptions)}
|
||||
removeFilter={() => removeFilter(i)}
|
||||
moveUp={() => moveUp(i)}
|
||||
moveDown={() => moveDown(i)}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</li>)}
|
||||
</ul>
|
||||
)
|
||||
};
|
||||
|
||||
export default FilterList
|
@ -13,10 +13,12 @@ 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";
|
||||
|
||||
const parseTime = (dateString, timeString) => {
|
||||
const d = dateString.match(
|
||||
/^(0\d|\d{2})[\/|\-]?(0\d|\d{2})[\/|\-]?(\d{4,5})$/
|
||||
/^(0\d|\d{2})[/|-]?(0\d|\d{2})[/|-]?(\d{4,5})$/
|
||||
);
|
||||
const t = timeString.match(/^(0\d|\d{2}):?(0\d|\d{2})$/);
|
||||
|
||||
@ -40,6 +42,17 @@ const QueryOptionsCard = () => {
|
||||
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) {
|
||||
console.log("Graph data loaded")
|
||||
setGraphData({playersOnline: options.viewPoints, color: '#9E9E9E'})
|
||||
}
|
||||
}, [options, setGraphData]);
|
||||
|
||||
// View state handling
|
||||
const [invalidFields, setInvalidFields] = useState([]);
|
||||
@ -47,8 +60,12 @@ const QueryOptionsCard = () => {
|
||||
const setAsValid = id => setInvalidFields(invalidFields.filter(invalid => id !== invalid));
|
||||
|
||||
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
|
||||
@ -61,11 +78,12 @@ const QueryOptionsCard = () => {
|
||||
min: newMin,
|
||||
max: newMax
|
||||
});
|
||||
}, [fromDate, fromTime, toDate, toTime, invalidFields]);
|
||||
useEffect(updateExtremes, [invalidFields]);
|
||||
}, [invalidFields, options]);
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
useEffect(updateExtremes, [invalidFields, updateExtremes]);
|
||||
|
||||
const onSetExtremes = useCallback((event) => {
|
||||
if (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);
|
||||
@ -77,15 +95,6 @@ const QueryOptionsCard = () => {
|
||||
}
|
||||
}, [setFromTime, setFromDate, setToTime, setToDate]);
|
||||
|
||||
// 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]);
|
||||
|
||||
const getServerSelectorMessage = () => {
|
||||
const selected = selectedServers.length;
|
||||
const available = options.view.servers.length;
|
||||
@ -176,7 +185,18 @@ const QueryOptionsCard = () => {
|
||||
</CollapseWithButton>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr/>
|
||||
<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-plan m-2"} disabled={Boolean(invalidFields.length)}>
|
||||
<FontAwesomeIcon icon={faSearch}/> {t('html.query.performQuery')}
|
||||
|
@ -0,0 +1,80 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import DateInputField from "../../../input/DateInputField";
|
||||
import TimeInputField from "../../../input/TimeInputField";
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faTrashAlt} from "@fortawesome/free-regular-svg-icons";
|
||||
|
||||
const BetweenDatesFilter = ({index, label, filter, removeFilter, setFilterOptions, setAsInvalid, setAsValid}) => {
|
||||
const {t} = useTranslation();
|
||||
const select = index === 0 ? t('html.query.filter.generic.start') : t('html.query.filter.generic.and');
|
||||
|
||||
const options = filter.options;
|
||||
|
||||
const [fromDate, setFromDate] = useState(options.after[0]);
|
||||
const [fromTime, setFromTime] = useState(options.after[1]);
|
||||
const [toDate, setToDate] = useState(options.before[0]);
|
||||
const [toTime, setToTime] = useState(options.before[1]);
|
||||
useEffect(() => {
|
||||
setFilterOptions({
|
||||
...filter,
|
||||
parameters: {
|
||||
afterDate: fromDate,
|
||||
afterTime: fromTime,
|
||||
beforeDate: toDate,
|
||||
beforeTime: toTime
|
||||
}
|
||||
})
|
||||
}, [setFilterOptions, fromDate, fromTime, toDate, toTime, filter]);
|
||||
|
||||
return (
|
||||
<div id={'filter-' + index} className="mt-2">
|
||||
<label>{select}{label}:</label>
|
||||
<Row className={"my-2 justify-content-start"}>
|
||||
<Col md={3} sm={6}>
|
||||
<DateInputField id={"filter-" + index + "-from-date"}
|
||||
value={fromDate}
|
||||
setValue={setFromDate}
|
||||
placeholder={options.after[0]}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={2} sm={6}>
|
||||
<TimeInputField id={"filter-" + index + "-from-time"}
|
||||
value={fromTime}
|
||||
setValue={setFromTime}
|
||||
placeholder={options.after[1]}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={1} sm={12} className={"text-center my-1 my-md-2 flex-fill"}>
|
||||
<label htmlFor="inlineFormCustomSelectPref">&</label>
|
||||
</Col>
|
||||
<Col md={3} sm={6}>
|
||||
<DateInputField id={"filter-" + index + "-to-date"}
|
||||
value={toDate}
|
||||
setValue={setToDate}
|
||||
placeholder={options.before[0]}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={2} sm={6}>
|
||||
<TimeInputField id={"filter-" + index + "-to-time"}
|
||||
value={toTime}
|
||||
setValue={setToTime}
|
||||
placeholder={options.before[1]}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={"auto"} sm={12} className={"my-1 my-md-auto"}>
|
||||
<button className="filter-remover btn btn-outline-secondary float-end"
|
||||
onClick={removeFilter}><FontAwesomeIcon icon={faTrashAlt}/>
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default BetweenDatesFilter
|
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import MultipleChoiceFilter from "./MultipleChoiceFilter";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import PluginGroupsFilter from "./PluginGroupsFilter";
|
||||
import BetweenDatesFilter from "./BetweenDatesFilter";
|
||||
|
||||
const Filter = ({index, filter, setFilterOptions, removeFilter, setAsInvalid, setAsValid}) => {
|
||||
const {t} = useTranslation();
|
||||
|
||||
if (filter.kind.startsWith("pluginGroups-")) {
|
||||
return <PluginGroupsFilter index={index} filter={filter}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>;
|
||||
}
|
||||
|
||||
const are = t('html.query.generic.are')
|
||||
.replaceAll("`", "");
|
||||
switch (filter.kind) {
|
||||
case "activityIndexNow":
|
||||
return <MultipleChoiceFilter index={index} filter={filter} label={t('html.query.filter.activity.text')}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>;
|
||||
case "allPlayers":
|
||||
case "banned":
|
||||
case "operators":
|
||||
return <MultipleChoiceFilter index={index} filter={filter} label={are}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>
|
||||
case "joinAddresses":
|
||||
return <MultipleChoiceFilter index={index} filter={filter} label={t('html.query.filter.joinAddress.text')}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>
|
||||
case "geolocations":
|
||||
return <MultipleChoiceFilter index={index} filter={filter} label={t('html.query.filter.country.text')}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>
|
||||
case "playedOnServer":
|
||||
return <MultipleChoiceFilter index={index} filter={filter}
|
||||
label={t('html.query.filter.hasPlayedOnServers.text')}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>
|
||||
case "pluginsBooleanGroups":
|
||||
return <MultipleChoiceFilter index={index} filter={filter}
|
||||
label={t('html.query.filter.hasPluginBooleanValue.text')}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}/>
|
||||
case "playedBetween":
|
||||
return <BetweenDatesFilter index={index} filter={filter}
|
||||
label={t('html.query.filter.playedBetween.text')}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}/>
|
||||
case "registeredBetween":
|
||||
return <BetweenDatesFilter index={index} filter={filter}
|
||||
label={t('html.query.filter.registeredBetween.text')}
|
||||
setFilterOptions={setFilterOptions} removeFilter={removeFilter}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}/>
|
||||
default:
|
||||
return (
|
||||
<div className={"my-2"}>
|
||||
<p>Unknown filter {filter.kind}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default Filter
|
@ -0,0 +1,41 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useTranslation} from "react-i18next";
|
||||
import MultiSelect from "../../../input/MultiSelect";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faTrashAlt} from "@fortawesome/free-regular-svg-icons";
|
||||
import {Col, Row} from "react-bootstrap-v5";
|
||||
|
||||
const MultipleChoiceFilter = ({index, label, filter, removeFilter, setFilterOptions}) => {
|
||||
const {t} = useTranslation();
|
||||
const select = index === 0 ? t('html.query.filter.generic.start') : t('html.query.filter.generic.and');
|
||||
|
||||
const [selectedIndexes, setSelectedIndexes] = useState([]);
|
||||
useEffect(() => {
|
||||
setFilterOptions({
|
||||
...filter,
|
||||
parameters: {
|
||||
selected: JSON.stringify(selectedIndexes.map(index => filter.options.options[index]))
|
||||
}
|
||||
})
|
||||
}, [setFilterOptions, selectedIndexes, filter]);
|
||||
|
||||
return (
|
||||
<div id={'filter-' + index} className="mt-2">
|
||||
<label className="form-label" htmlFor={'filter-' + index}>{select}{t(label)}:</label>
|
||||
<Row>
|
||||
<Col md={11} className={"flex-fill"}>
|
||||
<MultiSelect options={filter.options.options}
|
||||
setSelectedIndexes={setSelectedIndexes}
|
||||
selectedIndexes={selectedIndexes}/>
|
||||
</Col>
|
||||
<Col md={"auto"}>
|
||||
<button className="filter-remover btn btn-outline-secondary float-end"
|
||||
onClick={removeFilter}><FontAwesomeIcon icon={faTrashAlt}/>
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default MultipleChoiceFilter
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import MultipleChoiceFilter from "./MultipleChoiceFilter";
|
||||
|
||||
const PluginGroupsFilter = ({index, filter, removeFilter, setFilterOptions}) => {
|
||||
const label = `are in ${filter.options.plugin}'s ${filter.options.group} Groups`
|
||||
|
||||
return (
|
||||
<MultipleChoiceFilter
|
||||
index={index}
|
||||
label={label}
|
||||
filter={filter}
|
||||
removeFilter={removeFilter}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export default PluginGroupsFilter
|
@ -41,9 +41,9 @@ const LineGraph = ({id, series, legendEnabled, tall, yAxis, selectedRange, extre
|
||||
},
|
||||
series: series
|
||||
}));
|
||||
}, [series, graphTheming, id, t, nightModeEnabled, legendEnabled, yAxis, onSetExtremes, setGraph, onSetExtremes, selectedRange])
|
||||
}, [series, graphTheming, id, t, nightModeEnabled, legendEnabled, yAxis, onSetExtremes, setGraph, selectedRange])
|
||||
useEffect(() => {
|
||||
if (graph && extremes) {
|
||||
if (graph && graph.xAxis && graph.xAxis.length && extremes) {
|
||||
graph.xAxis[0].setExtremes(extremes.min, extremes.max);
|
||||
}
|
||||
}, [graph, extremes]);
|
||||
|
@ -1349,4 +1349,9 @@ button, input[type="submit"], input[type="reset"] {
|
||||
.dataTables_filter input {
|
||||
/* Fixes datatables search bar going outside cards */
|
||||
width: calc(100% - 3.7rem) !important;
|
||||
}
|
||||
|
||||
ul.filters {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
Loading…
Reference in New Issue
Block a user