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:
Aurora Lahtela 2022-10-15 15:41:06 +03:00
parent 027c63fd84
commit 428a0c5fde
9 changed files with 326 additions and 12 deletions

View File

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

View File

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

View File

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

View 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

View 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

View File

@ -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 ? <>{' '}&middot; {t(tab)}</> : ''}</h1>
</div>
<span className="topbar-divider"/>
<div className="refresh-element">
<button onClick={requestUpdate}>
<Fa icon={faSyncAlt} spin={Boolean(updating)}/>
</button>
{' '}
<span className="refresh-time">{lastUpdate.formatted}</span>
</div>
{!hideUpdater && <>
<span className="topbar-divider"/>
<div className="refresh-element">
<button onClick={requestUpdate}>
<Fa icon={faSyncAlt} spin={Boolean(updating)}/>
</button>
{' '}
<span className="refresh-time">{lastUpdate.formatted}</span>
</div>
</>}
<div className="ms-auto">
<LanguageSelector/>

View File

@ -0,0 +1,6 @@
import {doGetRequest} from "./backendConfiguration";
export const fetchFilters = async () => {
const url = `/v1/filters`;
return doGetRequest(url);
}

View 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;

View 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