diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java index 53e411aa0..9a096c492 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/GeoInfoQueries.java @@ -160,7 +160,7 @@ public class GeoInfoQueries { } public static Query> uniqueGeolocations() { - String sql = SELECT + GeoInfoTable.GEOLOCATION + FROM + GeoInfoTable.TABLE_NAME + + String sql = SELECT + DISTINCT + GeoInfoTable.GEOLOCATION + FROM + GeoInfoTable.TABLE_NAME + ORDER_BY + GeoInfoTable.GEOLOCATION + " ASC"; return db -> db.queryList(sql, RowExtractors.getString(GeoInfoTable.GEOLOCATION)); diff --git a/Plan/react/dashboard/src/App.js b/Plan/react/dashboard/src/App.js index 699f7f45b..cc7b1c5f2 100644 --- a/Plan/react/dashboard/src/App.js +++ b/Plan/react/dashboard/src/App.js @@ -46,12 +46,18 @@ const NetworkPerformance = React.lazy(() => import("./views/network/NetworkPerfo const PlayersPage = React.lazy(() => import("./views/layout/PlayersPage")); 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 ErrorsPage = React.lazy(() => import("./views/layout/ErrorsPage")); const SwaggerView = React.lazy(() => import("./views/SwaggerView")); const OverviewRedirect = () => { return () } +const NewRedirect = () => { + return () +} const ContextProviders = ({children}) => ( @@ -101,7 +107,7 @@ function App() { }/> }> - }/> + }/> }/> }/> }/> @@ -121,7 +127,7 @@ function App() { }}/>}/> }> - }/> + }/> }/> }/> }/> @@ -138,6 +144,10 @@ function App() { icon: faMapSigns }}/>}/> + }> + }/> + }/> + }/> }/> diff --git a/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js new file mode 100644 index 000000000..33a9de590 --- /dev/null +++ b/Plan/react/dashboard/src/components/cards/query/QueryOptionsCard.js @@ -0,0 +1,92 @@ +import React, {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 {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 {faSearch} from "@fortawesome/free-solid-svg-icons"; + +const QueryOptionsCard = () => { + const {t} = useTranslation(); + + const [fromDate, setFromDate] = useState(undefined); + const [fromTime, setFromTime] = useState(undefined); + const [toDate, setToDate] = useState(undefined); + const [toTime, setToTime] = useState(undefined); + + const [invalidFields, setInvalidFields] = useState([]); + const setAsInvalid = id => setInvalidFields([...invalidFields, id]); + const setAsValid = id => setInvalidFields(invalidFields.filter(invalid => id !== invalid)); + + const {data: options, loadingError} = useDataRequest(fetchFilters, []); + + if (loadingError) return + if (!options) return ( + + + + ) + + const view = options.view; + + return ( + + + + + + ', '') + .replace('>', '')} + + + + + + + + + ', '') + .replace('>', '')} + + + + + + + + + + + + ) +}; + +export default QueryOptionsCard \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/input/DateInputField.js b/Plan/react/dashboard/src/components/input/DateInputField.js new file mode 100644 index 000000000..33362c26f --- /dev/null +++ b/Plan/react/dashboard/src/components/input/DateInputField.js @@ -0,0 +1,74 @@ +import React, {useState} from 'react'; +import {InputGroup} from "react-bootstrap-v5"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faCalendar} from "@fortawesome/free-regular-svg-icons"; + +const isValidDate = value => { + if (!value) return true; + const d = value.match( + /^(0\d|\d{2})[/|-]?(0\d|\d{2})[/|-]?(\d{4,5})$/ + ); + if (!d) return false; + // 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]); + return new Date(parsedYear, parsedMonth, parsedDay); +}; + +const correctDate = value => { + const d = value.match( + /^(0\d|\d{2})[/|-]?(0\d|\d{2})[/|-]?(\d{4,5})$/ + ); + if (!d) return value; + + // 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]); + const date = d ? new Date(parsedYear, parsedMonth, parsedDay) : null; + + const day = `${date.getDate()}`; + const month = `${date.getMonth() + 1}`; + const year = `${date.getFullYear()}`; + return ( + (day.length === 1 ? `0${day}` : day) + + "/" + + (month.length === 1 ? `0${month}` : month) + + "/" + + year + ); +}; + +const DateInputField = ({id, setValue, value, placeholder, setAsInvalid, setAsValid}) => { + const [invalid, setInvalid] = useState(false); + + const onChange = (event) => { + const value = correctDate(event.target.value); + const invalid = !isValidDate(value); + setInvalid(invalid); + + if (invalid) { + setAsInvalid(id); + } else { + setAsValid(id); + } + setValue(value); + } + + return ( + +
+ +
+ +
+ ) +}; + +export default DateInputField \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/input/TimeInputField.js b/Plan/react/dashboard/src/components/input/TimeInputField.js new file mode 100644 index 000000000..b0908596f --- /dev/null +++ b/Plan/react/dashboard/src/components/input/TimeInputField.js @@ -0,0 +1,53 @@ +import React, {useState} from 'react'; +import {InputGroup} from "react-bootstrap-v5"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faClock} from "@fortawesome/free-regular-svg-icons"; + +const isValidTime = value => { + if (!value) return true; + const regex = /^[0-2][0-9]:[0-5][0-9]$/; + return regex.test(value); +}; + +const correctTime = value => { + const d = value.match(/^(0\d|\d{2}):?(0\d|\d{2})$/); + if (!d) return value; + let hour = Number(d[1]); + while (hour > 23) hour--; + let minute = Number(d[2]); + while (minute > 59) minute--; + return (hour < 10 ? "0" + hour : hour) + ":" + (minute < 10 ? "0" + minute : minute); +}; + +const TimeInputField = ({id, setValue, value, placeholder, setAsInvalid, setAsValid}) => { + const [invalid, setInvalid] = useState(false); + + const onChange = (event) => { + const value = correctTime(event.target.value); + const invalid = !isValidTime(value); + setInvalid(invalid); + + if (invalid) { + setAsInvalid(id); + } else { + setAsValid(id); + } + setValue(value); + } + + return ( + +
+ +
+ +
+ ) +}; + +export default TimeInputField \ No newline at end of file diff --git a/Plan/react/dashboard/src/components/navigation/Header.js b/Plan/react/dashboard/src/components/navigation/Header.js index 65b7d118f..f16925afa 100644 --- a/Plan/react/dashboard/src/components/navigation/Header.js +++ b/Plan/react/dashboard/src/components/navigation/Header.js @@ -31,7 +31,7 @@ const LanguageSelector = () => { ) } -const Header = ({page, tab}) => { +const Header = ({page, tab, hideUpdater}) => { const {authRequired, user} = useAuth(); const {toggleColorChooser} = useTheme(); const {t} = useTranslation(); @@ -56,14 +56,16 @@ const Header = ({page, tab}) => { {tab ? <>{' '}· {t(tab)} : ''} - -
- - {' '} - {lastUpdate.formatted} -
+ {!hideUpdater && <> + +
+ + {' '} + {lastUpdate.formatted} +
+ }
diff --git a/Plan/react/dashboard/src/service/queryService.js b/Plan/react/dashboard/src/service/queryService.js new file mode 100644 index 000000000..ac5750fe1 --- /dev/null +++ b/Plan/react/dashboard/src/service/queryService.js @@ -0,0 +1,6 @@ +import {doGetRequest} from "./backendConfiguration"; + +export const fetchFilters = async () => { + const url = `/v1/filters`; + return doGetRequest(url); +} diff --git a/Plan/react/dashboard/src/views/layout/QueryPage.js b/Plan/react/dashboard/src/views/layout/QueryPage.js new file mode 100644 index 000000000..565678855 --- /dev/null +++ b/Plan/react/dashboard/src/views/layout/QueryPage.js @@ -0,0 +1,57 @@ +import React, {useEffect, useState} from "react"; +import {useTranslation} from "react-i18next"; +import {Outlet} from "react-router-dom"; +import {useNavigation} from "../../hooks/navigationHook"; +import {faUndo} from "@fortawesome/free-solid-svg-icons"; +import {NightModeCss} from "../../hooks/themeHook"; +import Sidebar from "../../components/navigation/Sidebar"; +import Header from "../../components/navigation/Header"; +import ColorSelectorModal from "../../components/modal/ColorSelectorModal"; +import {useMetadata} from "../../hooks/metadataHook"; +import ErrorPage from "./ErrorPage"; + +const QueryPage = () => { + const {t, i18n} = useTranslation(); + const {isProxy, serverName} = useMetadata(); + + const [error] = useState(undefined); + const {sidebarItems, setSidebarItems} = useNavigation(); + + const {currentTab, setCurrentTab} = useNavigation(); + + useEffect(() => { + const items = [ + {name: 'html.label.links'}, + {name: 'html.query.label.makeAnother', icon: faUndo, href: "/query"}, + ] + + setSidebarItems(items); + window.document.title = `Plan | Query`; + setCurrentTab('html.label.query') + }, [t, i18n, setCurrentTab, setSidebarItems]) + + const showBackButton = true; + + if (error) return ; + + const displayedServerName = !isProxy && serverName && serverName.startsWith('Server') ? "Plan" : serverName; + return ( + <> + + +
+
+
+
+ +
+ +
+
+ + ) +} + +export default QueryPage; \ No newline at end of file diff --git a/Plan/react/dashboard/src/views/query/NewQueryView.js b/Plan/react/dashboard/src/views/query/NewQueryView.js new file mode 100644 index 000000000..40c4e418f --- /dev/null +++ b/Plan/react/dashboard/src/views/query/NewQueryView.js @@ -0,0 +1,20 @@ +import React from 'react'; +import LoadIn from "../../components/animation/LoadIn"; +import {Col, Row} from "react-bootstrap-v5"; +import QueryOptionsCard from "../../components/cards/query/QueryOptionsCard"; + +const NewQueryView = () => { + return ( + +
+ + + + + +
+
+ ) +}; + +export default NewQueryView \ No newline at end of file