diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java index c0d3bf81f..bcc557253 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java @@ -142,7 +142,7 @@ public class QueryJSONResolver implements Resolver { List resultPath = result.getInverseResultPath(); Collections.reverse(resultPath); - return buildAndStoreResponse(inputQuery.getView(), result, resultPath); + return buildAndStoreResponse(inputQuery, result, resultPath); } private InputQueryDto parseInputQuery(Request request) { @@ -180,16 +180,17 @@ public class QueryJSONResolver implements Resolver { } } - private Response buildAndStoreResponse(ViewDto view, Filter.Result result, List resultPath) { + private Response buildAndStoreResponse(InputQueryDto input, Filter.Result result, List resultPath) { try { long timestamp = System.currentTimeMillis(); Map json = Maps.builder(String.class, Object.class) .put("path", resultPath) - .put("view", view) + .put("view", input.getView()) + .put("filters", input.getFilters()) .put("timestamp", timestamp) .build(); if (!result.isEmpty()) { - json.put("data", getDataFor(result.getResultUserIds(), view)); + json.put("data", getDataFor(result.getResultUserIds(), input.getView())); } JSONStorage.StoredJSON stored = jsonStorage.storeJson("query", json, timestamp); 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 e9a1931b8..2ba8337e6 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 @@ -60,6 +60,7 @@ public enum JSLang implements Lang { 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_SKIPPED("html.query.filter.skipped", "Skipped"), FILTER_GROUP("html.query.filter.pluginGroup.name", "Group: "), FILTER_ALL_PLAYERS("html.query.filter.generic.allPlayers", "All players"), diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/analysis/NetworkActivityIndexQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/analysis/NetworkActivityIndexQueries.java index b7700a319..f027bd24f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/analysis/NetworkActivityIndexQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/analysis/NetworkActivityIndexQueries.java @@ -449,7 +449,7 @@ public class NetworkActivityIndexQueries { Map indexes = new HashMap<>(); while (set.next()) { indexes.put( - set.getInt(UsersTable.ID), + set.getInt("user_id"), new ActivityIndex(set.getDouble("activity_index"), date) ); } diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index cc7b1c5f2..2e25d97b0 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -48,6 +48,7 @@ const AllPlayers = React.lazy(() => import("./views/players/AllPlayers")); const QueryPage = React.lazy(() => import("./views/layout/QueryPage")); const NewQueryView = React.lazy(() => import("./views/query/NewQueryView")); +const QueryResultView = React.lazy(() => import("./views/query/QueryResultView")); const ErrorsPage = React.lazy(() => import("./views/layout/ErrorsPage")); const SwaggerView = React.lazy(() => import("./views/SwaggerView")); @@ -147,6 +148,7 @@ function App() { }> }/> }/> + }/> }/> }/> diff --git a/Plan/react/dashboard/src/components/alert/QueryPath.js b/Plan/react/dashboard/src/components/alert/QueryPath.js new file mode 100644 index 000000000..f7b319b24 --- /dev/null +++ b/Plan/react/dashboard/src/components/alert/QueryPath.js @@ -0,0 +1,59 @@ +import React from 'react'; +import {useQueryResultContext} from "../../hooks/queryResultContext"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faFilter} from "@fortawesome/free-solid-svg-icons"; +import {useTranslation} from "react-i18next"; + +const QueryPath = () => { + const {t} = useTranslation(); + const {result} = useQueryResultContext(); + const hasResults = result && result.data; + const path = result?.path; + if (!path || !path.length) return <>; + + + const getReadableFilterName = kind => { + if (kind.endsWith(" (skip)")) { + return getReadableFilterName(kind.substring(0, kind.length - 5)) + " (" + t('html.query.filter.skipped') + ")"; + } + + if (kind.startsWith("pluginGroups-")) { + return "Group: " + kind.substring(13); + } + switch (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 kind.kind; + } + }; + + return ( + + ) +}; + +export default QueryPath \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/cards/common/PlayerListCard.js b/Plan/react/dashboard/src/components/cards/common/PlayerListCard.js index eea58eada..9fc9b671e 100644 --- a/Plan/react/dashboard/src/components/cards/common/PlayerListCard.js +++ b/Plan/react/dashboard/src/components/cards/common/PlayerListCard.js @@ -7,7 +7,7 @@ import DataTablesTable from "../../table/DataTablesTable"; import {CardLoader} from "../../navigation/Loader"; import {baseAddress} from "../../../service/backendConfiguration"; -const PlayerListCard = ({data}) => { +const PlayerListCard = ({data, title}) => { const {t} = useTranslation(); const [options, setOptions] = useState(undefined); @@ -34,7 +34,7 @@ const PlayerListCard = ({data}) => {
- {t('html.label.playerList')} + {title ? title : t('html.label.playerList')}
diff --git a/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js index f5e37b04b..c28a90379 100644 --- a/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js +++ b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js @@ -2,7 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import {Card, Col, Row} from "react-bootstrap-v5"; import {useTranslation} from "react-i18next"; import {useDataRequest} from "../../../hooks/dataFetchHook"; -import {fetchFilters} from "../../../service/queryService"; +import {fetchFilters, postQuery} from "../../../service/queryService"; import {ErrorViewCard} from "../../../views/ErrorView"; import {ChartLoader} from "../../navigation/Loader"; import DateInputField from "../../input/DateInputField"; @@ -15,6 +15,8 @@ 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( @@ -34,6 +36,8 @@ const parseTime = (dateString, timeString) => { const QueryOptionsCard = () => { const {t} = useTranslation(); + const navigate = useNavigate() + const {setResult} = useQueryResultContext(); // View state const [fromDate, setFromDate] = useState(undefined); @@ -49,7 +53,6 @@ const QueryOptionsCard = () => { const [graphData, setGraphData] = useState(undefined); useEffect(() => { if (options) { - console.log("Graph data loaded") setGraphData({playersOnline: options.viewPoints, color: '#9E9E9E'}) } }, [options, setGraphData]); @@ -109,6 +112,27 @@ const QueryOptionsCard = () => { } } + 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]) + }, + filters + } + + // TODO handle error + const {data} = await postQuery(inputDto); + setResult(data); + window.scrollTo(0, 0); + if (data?.data) { + navigate('../result?timestamp=' + data.timestamp); + } + } + if (loadingError) return if (!options) return ( @@ -198,7 +222,10 @@ const QueryOptionsCard = () => { - diff --git a/Plan/react/dashboard/src/hooks/queryResultContext.js b/Plan/react/dashboard/src/hooks/queryResultContext.js new file mode 100644 index 000000000..cd565844f --- /dev/null +++ b/Plan/react/dashboard/src/hooks/queryResultContext.js @@ -0,0 +1,17 @@ +import {createContext, useContext, useState} from "react"; + +const QueryResultContext = createContext({}); + +export const QueryResultContextProvider = ({children}) => { + const [result, setResult] = useState({}); + + const sharedState = {result, setResult} + return ( + {children} + + ) +} + +export const useQueryResultContext = () => { + return useContext(QueryResultContext); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/service/queryService.js b/Plan/react/dashboard/src/service/queryService.js index ac5750fe1..efcaeb7f9 100644 --- a/Plan/react/dashboard/src/service/queryService.js +++ b/Plan/react/dashboard/src/service/queryService.js @@ -1,6 +1,16 @@ -import {doGetRequest} from "./backendConfiguration"; +import {doGetRequest, doSomePostRequest, standard200option} from "./backendConfiguration"; export const fetchFilters = async () => { const url = `/v1/filters`; return doGetRequest(url); } + +export const postQuery = async (inputDto) => { + const url = `/v1/query`; + return doSomePostRequest(url, [standard200option], inputDto); +} + +export const fetchExistingResults = async (timestamp) => { + const url = `/v1/query?timestamp=${timestamp}`; + return doGetRequest(url); +} \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/layout/QueryPage.js b/Plan/react/dashboard/src/views/layout/QueryPage.js index 565678855..79f32d8c0 100644 --- a/Plan/react/dashboard/src/views/layout/QueryPage.js +++ b/Plan/react/dashboard/src/views/layout/QueryPage.js @@ -9,6 +9,7 @@ import Header from "../../components/navigation/Header"; import ColorSelectorModal from "../../components/modal/ColorSelectorModal"; import {useMetadata} from "../../hooks/metadataHook"; import ErrorPage from "./ErrorPage"; +import {QueryResultContextProvider} from "../../hooks/queryResultContext"; const QueryPage = () => { const {t, i18n} = useTranslation(); @@ -38,18 +39,20 @@ const QueryPage = () => { return ( <> - -
-
-
-
- -
- + + +
+
+
+
+ +
+ +
-
+ ) } diff --git a/Plan/react/dashboard/src/views/query/NewQueryView.js b/Plan/react/dashboard/src/views/query/NewQueryView.js index 40c4e418f..9c8d540ae 100644 --- a/Plan/react/dashboard/src/views/query/NewQueryView.js +++ b/Plan/react/dashboard/src/views/query/NewQueryView.js @@ -2,6 +2,7 @@ import React from 'react'; import LoadIn from "../../components/animation/LoadIn"; import {Col, Row} from "react-bootstrap-v5"; import QueryOptionsCard from "../../components/cards/query/QueryOptionsCard"; +import QueryPath from "../../components/alert/QueryPath"; const NewQueryView = () => { return ( @@ -9,6 +10,7 @@ const NewQueryView = () => {
+ diff --git a/Plan/react/dashboard/src/views/query/QueryResultView.js b/Plan/react/dashboard/src/views/query/QueryResultView.js new file mode 100644 index 000000000..e66848702 --- /dev/null +++ b/Plan/react/dashboard/src/views/query/QueryResultView.js @@ -0,0 +1,81 @@ +import React, {useCallback, useEffect} from 'react'; +import LoadIn from "../../components/animation/LoadIn"; +import {Col, Row} from "react-bootstrap-v5"; +import QueryPath from "../../components/alert/QueryPath"; +import {useQueryResultContext} from "../../hooks/queryResultContext"; +import {useNavigate} from "react-router-dom"; +import PlayerListCard from "../../components/cards/common/PlayerListCard"; +import {fetchExistingResults} from "../../service/queryService"; + +const QueryResultView = () => { + const navigate = useNavigate(); + const {result, setResult} = useQueryResultContext(); + + const getResult = useCallback(async () => { + const urlParams = new URLSearchParams(window.location.search); + const timestamp = urlParams.get('timestamp'); + if (!timestamp) return {}; + + const {data: result} = await fetchExistingResults(timestamp); + if (result) { + return result; + } else { + return {}; + } + }, []) + + useEffect(() => { + if (!result.data) { + getResult().then(data => { + if (data.data) { + setResult(data); + } else { + navigate('../new'); + } + }); + } + }, [result, navigate, getResult, setResult]) + + if (!result.data) { + 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 ( + +
+ + + + + + + + + {/**/} + + + {/**/} + + + + + {/**/} + + + {/**/} + + +
+
+ ) +}; + +export default QueryResultView \ No newline at end of file