diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java index 7999d6290..e9a1931b8 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/locale/lang/JSLang.java @@ -56,6 +56,10 @@ public enum JSLang implements Lang { QUERY_ACTIVITY_ON("html.query.title.activityOnDate", "Activity on "), 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"), diff --git a/Plan/react/dashboard/src/components/cards/query/FilterDropdown.js b/Plan/react/dashboard/src/components/cards/query/FilterDropdown.js new file mode 100644 index 000000000..e15fb2006 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/query/FilterDropdown.js @@ -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 ( + + + {t('html.query.filters.add')} + + + +
{t('html.query.filters.add')}
+ + {filterOptions.map((option, i) => ( + addFilter(option)}> + {getReadableFilterName(option)} + + ))} + +
+
+ ) +}; + +export default FilterDropdown \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/query/FilterList.js b/Plan/react/dashboard/src/components/cards/query/FilterList.js new file mode 100644 index 000000000..6314276e4 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/query/FilterList.js @@ -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 ( + + ) +}; + +export default FilterList \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js index 582c31abf..3e5c772bc 100644 --- a/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js +++ b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js @@ -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 = () => { -
+
+ + + + + + + + + + + + + + ) +}; + +export default BetweenDatesFilter \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/query/filter/Filter.js b/Plan/react/dashboard/src/components/cards/query/filter/Filter.js new file mode 100644 index 000000000..2eb6fb9b5 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/query/filter/Filter.js @@ -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 ; + } + + const are = t('html.query.generic.are') + .replaceAll("`", ""); + switch (filter.kind) { + case "activityIndexNow": + return ; + case "allPlayers": + case "banned": + case "operators": + return + case "joinAddresses": + return + case "geolocations": + return + case "playedOnServer": + return + case "pluginsBooleanGroups": + return + case "playedBetween": + return + case "registeredBetween": + return + default: + return ( +
+

Unknown filter {filter.kind}

+
+ ) + } + +}; + +export default Filter \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/query/filter/MultipleChoiceFilter.js b/Plan/react/dashboard/src/components/cards/query/filter/MultipleChoiceFilter.js new file mode 100644 index 000000000..2e220317a --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/query/filter/MultipleChoiceFilter.js @@ -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 ( +
+ + + + + + + + + +
+ ) +}; + +export default MultipleChoiceFilter \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/query/filter/PluginGroupsFilter.js b/Plan/react/dashboard/src/components/cards/query/filter/PluginGroupsFilter.js new file mode 100644 index 000000000..db951854a --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/query/filter/PluginGroupsFilter.js @@ -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 ( + + ) +}; + +export default PluginGroupsFilter \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/graphs/LineGraph.js b/Plan/react/dashboard/src/components/graphs/LineGraph.js index 1a206ee04..17129ad4a 100644 --- a/Plan/react/dashboard/src/components/graphs/LineGraph.js +++ b/Plan/react/dashboard/src/components/graphs/LineGraph.js @@ -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]); diff --git a/Plan/react/dashboard/src/style/style.css b/Plan/react/dashboard/src/style/style.css index 49e510ffc..5a369851e 100644 --- a/Plan/react/dashboard/src/style/style.css +++ b/Plan/react/dashboard/src/style/style.css @@ -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; } \ No newline at end of file