mirror of
https://github.com/plan-player-analytics/Plan.git
synced 2025-03-10 05:39:19 +01:00
Began work on Query page
- Implemented the view time range selector input fields - Fixed geolocation filter query returning more options than supposed to
This commit is contained in:
parent
027c63fd84
commit
428a0c5fde
@ -160,7 +160,7 @@ public class GeoInfoQueries {
|
||||
}
|
||||
|
||||
public static Query<List<String>> 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));
|
||||
|
@ -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 (<Navigate to={"overview"} replace={true}/>)
|
||||
}
|
||||
const NewRedirect = () => {
|
||||
return (<Navigate to={"new"} replace={true}/>)
|
||||
}
|
||||
|
||||
const ContextProviders = ({children}) => (
|
||||
<AuthenticationContextProvider>
|
||||
@ -101,7 +107,7 @@ function App() {
|
||||
<Route path="*" element={<Lazy><AllPlayers/></Lazy>}/>
|
||||
</Route>
|
||||
<Route path="/server/:identifier" element={<Lazy><ServerPage/></Lazy>}>
|
||||
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
|
||||
<Route path="" element={<OverviewRedirect/>}/>
|
||||
<Route path="overview" element={<Lazy><ServerOverview/></Lazy>}/>
|
||||
<Route path="online-activity" element={<Lazy><OnlineActivity/></Lazy>}/>
|
||||
<Route path="sessions" element={<Lazy><ServerSessions/></Lazy>}/>
|
||||
@ -121,7 +127,7 @@ function App() {
|
||||
}}/>}/>
|
||||
</Route>
|
||||
<Route path="/network" element={<Lazy><NetworkPage/></Lazy>}>
|
||||
<Route path="" element={<Lazy><OverviewRedirect/></Lazy>}/>
|
||||
<Route path="" element={<OverviewRedirect/>}/>
|
||||
<Route path="overview" element={<Lazy><NetworkOverview/></Lazy>}/>
|
||||
<Route path="serversOverview" element={<Lazy><NetworkServers/></Lazy>}/>
|
||||
<Route path="sessions" element={<Lazy><NetworkSessions/></Lazy>}/>
|
||||
@ -138,6 +144,10 @@ function App() {
|
||||
icon: faMapSigns
|
||||
}}/>}/>
|
||||
</Route>
|
||||
<Route path="/query" element={<Lazy><QueryPage/></Lazy>}>
|
||||
<Route path="" element={<NewRedirect/>}/>
|
||||
<Route path="new" element={<Lazy><NewQueryView/></Lazy>}/>
|
||||
</Route>
|
||||
<Route path="/errors" element={<Lazy><ErrorsPage/></Lazy>}/>
|
||||
<Route path="/docs" element={<Lazy><SwaggerView/></Lazy>}/>
|
||||
</Routes>
|
||||
|
@ -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 <ErrorViewCard error={loadingError}/>
|
||||
if (!options) return (<Card>
|
||||
<Card.Body>
|
||||
<ChartLoader/>
|
||||
</Card.Body>
|
||||
</Card>)
|
||||
|
||||
const view = options.view;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<label>{t('html.query.label.view')}</label>
|
||||
<Row className={"my-2 justify-content-start justify-content-md-center"}>
|
||||
<Col className={"my-2"}>
|
||||
<label>{t('html.query.label.from')
|
||||
.replace('</label>', '')
|
||||
.replace('>', '')}</label>
|
||||
</Col>
|
||||
<Col md={3}>
|
||||
<DateInputField id={"viewFromDateField"}
|
||||
value={fromDate}
|
||||
setValue={setFromDate}
|
||||
placeholder={view.afterDate}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={2}>
|
||||
<TimeInputField id={"viewFromTimeField"}
|
||||
value={fromTime}
|
||||
setValue={setFromTime}
|
||||
placeholder={view.afterTime}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={1} className={"my-2 text-center"}>
|
||||
<label>{t('html.query.label.to')
|
||||
.replace('</label>', '')
|
||||
.replace('>', '')}</label>
|
||||
</Col>
|
||||
<Col md={3}>
|
||||
<DateInputField id={"viewToDateField"}
|
||||
value={toDate}
|
||||
setValue={setToDate}
|
||||
placeholder={view.beforeDate}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={2}>
|
||||
<TimeInputField id={"viewToTimeField"}
|
||||
value={toTime}
|
||||
setValue={setToTime}
|
||||
placeholder={view.beforeTime}
|
||||
setAsInvalid={setAsInvalid} setAsValid={setAsValid}
|
||||
/>
|
||||
</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')}
|
||||
</button>
|
||||
</Card>
|
||||
)
|
||||
};
|
||||
|
||||
export default QueryOptionsCard
|
74
Plan/react/dashboard/src/components/input/DateInputField.js
Normal file
74
Plan/react/dashboard/src/components/input/DateInputField.js
Normal file
@ -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 (
|
||||
<InputGroup>
|
||||
<div className={"input-group-text"}>
|
||||
<FontAwesomeIcon icon={faCalendar}/>
|
||||
</div>
|
||||
<input type="text" className={"form-control" + (invalid ? " is-invalid" : '')}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</InputGroup>
|
||||
)
|
||||
};
|
||||
|
||||
export default DateInputField
|
53
Plan/react/dashboard/src/components/input/TimeInputField.js
Normal file
53
Plan/react/dashboard/src/components/input/TimeInputField.js
Normal file
@ -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 (
|
||||
<InputGroup>
|
||||
<div className={"input-group-text"}>
|
||||
<FontAwesomeIcon icon={faClock}/>
|
||||
</div>
|
||||
<input type="text" className={"form-control" + (invalid ? " is-invalid" : '')}
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</InputGroup>
|
||||
)
|
||||
};
|
||||
|
||||
export default TimeInputField
|
@ -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,6 +56,7 @@ const Header = ({page, tab}) => {
|
||||
{tab ? <>{' '}· {t(tab)}</> : ''}</h1>
|
||||
</div>
|
||||
|
||||
{!hideUpdater && <>
|
||||
<span className="topbar-divider"/>
|
||||
<div className="refresh-element">
|
||||
<button onClick={requestUpdate}>
|
||||
@ -64,6 +65,7 @@ const Header = ({page, tab}) => {
|
||||
{' '}
|
||||
<span className="refresh-time">{lastUpdate.formatted}</span>
|
||||
</div>
|
||||
</>}
|
||||
|
||||
<div className="ms-auto">
|
||||
<LanguageSelector/>
|
||||
|
6
Plan/react/dashboard/src/service/queryService.js
Normal file
6
Plan/react/dashboard/src/service/queryService.js
Normal file
@ -0,0 +1,6 @@
|
||||
import {doGetRequest} from "./backendConfiguration";
|
||||
|
||||
export const fetchFilters = async () => {
|
||||
const url = `/v1/filters`;
|
||||
return doGetRequest(url);
|
||||
}
|
57
Plan/react/dashboard/src/views/layout/QueryPage.js
Normal file
57
Plan/react/dashboard/src/views/layout/QueryPage.js
Normal file
@ -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 <ErrorPage error={error}/>;
|
||||
|
||||
const displayedServerName = !isProxy && serverName && serverName.startsWith('Server') ? "Plan" : serverName;
|
||||
return (
|
||||
<>
|
||||
<NightModeCss/>
|
||||
<Sidebar items={sidebarItems} showBackButton={showBackButton}/>
|
||||
<div className="d-flex flex-column" id="content-wrapper">
|
||||
<Header page={displayedServerName} tab={currentTab} hideUpdater/>
|
||||
<div id="content" style={{display: 'flex'}}>
|
||||
<main className="container-fluid mt-4">
|
||||
<Outlet context={{}}/>
|
||||
</main>
|
||||
<aside>
|
||||
<ColorSelectorModal/>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default QueryPage;
|
20
Plan/react/dashboard/src/views/query/NewQueryView.js
Normal file
20
Plan/react/dashboard/src/views/query/NewQueryView.js
Normal file
@ -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 (
|
||||
<LoadIn>
|
||||
<section className={"query-options-view"}>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<QueryOptionsCard/>
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
</LoadIn>
|
||||
)
|
||||
};
|
||||
|
||||
export default NewQueryView
|
Loading…
Reference in New Issue
Block a user