Implemented Query page filter options

This commit is contained in:
Aurora Lahtela 2022-10-22 11:18:24 +03:00
parent 8c1a357323
commit f4bd580840
10 changed files with 357 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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