Use local cache to optimize display

This commit is contained in:
Emmy D'Anello 2024-02-02 00:50:50 +01:00
parent edbc01122d
commit 311a29cf4b
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
5 changed files with 179 additions and 85 deletions

View File

@ -12,6 +12,9 @@
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.6", "@mui/material": "^5.15.6",
"@mui/x-date-pickers": "^6.19.2", "@mui/x-date-pickers": "^6.19.2",
"@tanstack/query-sync-storage-persister": "^5.18.0",
"@tanstack/react-query": "^5.18.0",
"@tanstack/react-query-persist-client": "^5.18.0",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
@ -4181,6 +4184,71 @@
"url": "https://github.com/sponsors/gregberge" "url": "https://github.com/sponsors/gregberge"
} }
}, },
"node_modules/@tanstack/query-core": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.18.0.tgz",
"integrity": "sha512-8c6nxeAnGHxIDZIyDmHdmgFt4D+LprAQaJmjsnM4szcIjsWOyFlzxdqQUuQ/XuQRvUgqYaqlJTtDADlSS7pTPQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-persist-client-core": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.18.0.tgz",
"integrity": "sha512-CXmUP8GYW1LL9tQC8vmuaK3XKWR3GaP8Bhy2bpgbIX5aOTN0qWL4zVema4hclgNahpTlY13P67Biab8ieB6frA==",
"dependencies": {
"@tanstack/query-core": "5.18.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-sync-storage-persister": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.18.0.tgz",
"integrity": "sha512-a+ztQhpnWRmQN8kn/IVscgxmSnBZER2TbrLkocZGzowecxT+Lm0RzbX+Dl2lVz92XXHFmhAjAXhWG61rbqQTng==",
"dependencies": {
"@tanstack/query-core": "5.18.0",
"@tanstack/query-persist-client-core": "5.18.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.18.0.tgz",
"integrity": "sha512-7FKxNfxxKEL7n3ADpwp81Fy4FX85hNkYVzQQVQsF0JAPl93c3d1gmNZMIbEtOqgYfom1/ontGh3FiZGYj3xyWA==",
"dependencies": {
"@tanstack/query-core": "5.18.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/@tanstack/react-query-persist-client": {
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.18.0.tgz",
"integrity": "sha512-XS3C3tMcnBosptRhn4kN5RBs6j8iRiE7tVr7XN73We4o3VNo86+zEe4iOwO+ziTq0Dawnc4kCjUVS3tZ2DiKhg==",
"dependencies": {
"@tanstack/query-persist-client-core": "5.18.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^5.18.0",
"react": "^18.0.0"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "9.3.4", "version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",

View File

@ -7,6 +7,9 @@
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/material": "^5.15.6", "@mui/material": "^5.15.6",
"@mui/x-date-pickers": "^6.19.2", "@mui/x-date-pickers": "^6.19.2",
"@tanstack/query-sync-storage-persister": "^5.18.0",
"@tanstack/react-query": "^5.18.0",
"@tanstack/react-query-persist-client": "^5.18.0",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",

View File

@ -6,6 +6,9 @@ import {frFR, LocalizationProvider} from "@mui/x-date-pickers"
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs" import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"
import 'dayjs/locale/fr' import 'dayjs/locale/fr'
import './App.css' import './App.css'
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {createSyncStoragePersister} from "@tanstack/query-sync-storage-persister";
import {persistQueryClient} from "@tanstack/react-query-persist-client";
function App() { function App() {
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -40,11 +43,32 @@ function App() {
[prefersDarkMode], [prefersDarkMode],
); );
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 3 hours
staleTime: 1000 * 60 * 60 * 3, // 3 hours
notifyOnChangeProps: ['data', 'error'],
},
},
})
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister: localStoragePersister,
})
return <> return <>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<LocalizationProvider dateAdapter={AdapterDayjs} localeText={frFR.components.MuiLocalizationProvider.defaultProps.localeText} adapterLocale="fr"> <LocalizationProvider dateAdapter={AdapterDayjs} localeText={frFR.components.MuiLocalizationProvider.defaultProps.localeText} adapterLocale="fr">
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} /> <RouterProvider router={router} />
</QueryClientProvider>
</LocalizationProvider> </LocalizationProvider>
</ThemeProvider> </ThemeProvider>
</> </>

View File

@ -1,9 +1,10 @@
import {useParams, useSearchParams} from "react-router-dom" import {useParams, useSearchParams} from "react-router-dom"
import TrainsTable from "./TrainsTable" import TrainsTable from "./TrainsTable"
import {useEffect, useState} from "react"; import {useState} from "react";
import {Box, Button, FormControl, FormControlLabel, FormGroup, FormLabel} from "@mui/material"; import {Box, Button, FormLabel} from "@mui/material";
import {DatePicker, DateTimePicker, TimePicker} from "@mui/x-date-pickers"; import {DatePicker, TimePicker} from "@mui/x-date-pickers";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {useQuery, useQueryClient} from "@tanstack/react-query";
function DateTimeSelector({date, time}) { function DateTimeSelector({date, time}) {
return <> return <>
@ -20,7 +21,6 @@ function DateTimeSelector({date, time}) {
function Station() { function Station() {
let {stopId} = useParams() let {stopId} = useParams()
let [stop, setStop] = useState({'name': "Chargement…"})
let [searchParams, _setSearchParams] = useSearchParams() let [searchParams, _setSearchParams] = useSearchParams()
const now = new Date() const now = new Date()
let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
@ -28,13 +28,14 @@ function Station() {
let [date, setDate] = useState(searchParams.get('date') || dateNow) let [date, setDate] = useState(searchParams.get('date') || dateNow)
let [time, setTime] = useState(searchParams.get('time') || timeNow) let [time, setTime] = useState(searchParams.get('time') || timeNow)
useEffect(() => { useQueryClient()
fetch(`http://localhost:8000/api/gtfs/stop/${stopId}/`) const stopQuery = useQuery({
.then(response => response.json()) queryKey: ['stop', stopId],
.then(data => { queryFn: () => fetch(`http://localhost:8000/api/gtfs/stop/${stopId}/`)
setStop(data) .then(response => response.json()),
enabled: !!stopId,
}) })
}, [stopId]) const stop = stopQuery.data ?? {name: "Chargement…"}
if (time === timeNow) { if (time === timeNow) {
setInterval(() => { setInterval(() => {
@ -43,7 +44,7 @@ function Station() {
let timeNow = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}` let timeNow = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
setDate(dateNow) setDate(dateNow)
setTime(timeNow) setTime(timeNow)
}, 60000) }, 5000)
} }
return ( return (

View File

@ -1,4 +1,3 @@
import {useEffect, useState} from "react"
import { import {
Box, Box,
styled, styled,
@ -11,6 +10,8 @@ import {
Typography Typography
} from "@mui/material" } from "@mui/material"
import {CSSTransition, TransitionGroup} from 'react-transition-group' import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {useQueries, useQuery, useQueryClient} from "@tanstack/react-query";
import {useEffect, useState} from "react";
const StyledTableRow = styled(TableRow)(({ theme, tableType }) => ({ const StyledTableRow = styled(TableRow)(({ theme, tableType }) => ({
'tbody &:nth-of-type(odd)': { 'tbody &:nth-of-type(odd)': {
@ -49,27 +50,37 @@ function TrainsTableHeader({tableType}) {
} }
function TrainsTableBody({stop, date, time, tableType}) { function TrainsTableBody({stop, date, time, tableType}) {
const [trains, setTrains] = useState([]) const queryClient = useQueryClient()
useEffect(() => { let filterTime = (train) => {
if (stop.id !== undefined) { if (train.departure_time === "04:56:00")
let validTrains = trains.filter(train => { return false
if (tableType === "departures") if (tableType === "departures")
return `${train.departure_date}T${train.departure_time_24h}` >= `${date}T${time}` return `${train.departure_date}T${train.departure_time_24h}` >= `${date}T${time}`
else else
return `${train.arrival_date}T${train.arrival_time_24h}` >= `${date}T${time}` return `${train.arrival_date}T${train.arrival_time_24h}` >= `${date}T${time}`
}) }
if (trains.length > 0 && validTrains.length === trains.length)
return function updateTrains() {
console.log(`${trains.length - validTrains.length} trains deleted`) return fetch(`http://localhost:8000/api/station/next_${tableType}/?stop_id=${stop.id}&date=${date}&time=${time}&offset=${0}&limit=${20}`)
fetch(`http://localhost:8000/api/station/next_${tableType}/?stop_id=${stop.id}&date=${date}&time=${time}&offset=${validTrains.length}&limit=${20 - validTrains.length}`)
.then(response => response.json()) .then(response => response.json())
.then(data => data.results) .then(data => data.results)
.then(data => { .then(data => [...data])
setTrains(trains => [...validTrains, ...data])
})
} }
}, [stop, tableType, date, time])
const trainsQuery = useQuery({
queryKey: ['trains', stop.id, tableType],
queryFn: updateTrains,
enabled: !!stop.id,
})
const trains = trainsQuery.data ?? []
useEffect(() => {
let validTrains = trains?.filter(filterTime) ?? []
console.log(validTrains.length)
if (trains?.length > 0 && validTrains.length <= trains?.length)
queryClient.invalidateQueries({queryKey: ['trains', stop.id, tableType]})
}, [stop.id, tableType, date, time])
let table_rows = trains.map((train) => <CSSTransition key={train.id} timeout={500} classNames="shrink"> let table_rows = trains.map((train) => <CSSTransition key={train.id} timeout={500} classNames="shrink">
<TrainRow train={train} tableType={tableType} /> <TrainRow train={train} tableType={tableType} />
@ -85,64 +96,51 @@ function TrainsTableBody({stop, date, time, tableType}) {
} }
function TrainRow({train, tableType}) { function TrainRow({train, tableType}) {
const [trip, setTrip] = useState({}) const tripQuery = useQuery({
const [route, setRoute] = useState({}) queryKey: ['trip', train.trip],
const [stopTimes, setStopTimes] = useState([]) queryFn: () => fetch(`http://localhost:8000/api/gtfs/trip/${train.trip}/`)
const [trainType, setTrainType] = useState("") .then(response => response.json()),
enabled: !!train.trip,
})
const trip = tripQuery.data ?? {}
useEffect(() => { const routeQuery = useQuery({
if (train.trip !== undefined) { queryKey: ['route', trip.route],
fetch(`http://localhost:8000/api/gtfs/trip/${train.trip}/`) queryFn: () => fetch(`http://localhost:8000/api/gtfs/route/${trip.route}/`)
.then(response => response.json()),
enabled: !!trip.route,
})
const route = routeQuery.data ?? {}
const trainType = getTrainType(train, route)
const stopTimesQuery = useQuery({
queryKey: ['stop_times', trip.id],
queryFn: () => fetch(`http://localhost:8000/api/gtfs/stop_time/?trip=${trip.id}&order=stop_sequence&limit=1000`)
.then(response => response.json()) .then(response => response.json())
.then(t => { .then(data => data.results),
t.stop_times = [] enabled: !!trip.id,
setTrip(t)
}) })
} const stopTimes = stopTimesQuery.data ?? []
}, [train.trip]) const stopIds = stopTimes.map(stop_time => stop_time.stop)
useEffect(() => { const stopQueries = useQueries({
if (trip.route !== undefined) { queries: stopIds.map(stopId => ({
fetch(`http://localhost:8000/api/gtfs/route/${trip.route}/`) queryKey: ['stop', stopId],
.then(response => response.json()) queryFn: () => fetch(`http://localhost:8000/api/gtfs/stop/${stopId}/`)
.then(data => { .then(response => response.json()),
setRoute(data) enabled: !!stopId,
})),
}) })
} const stops = stopTimes.map(((stopTime, i) => ({...stopTime, stop: stopQueries[i]?.data ?? {"name": "…"}}))) ?? []
}, [trip.route])
useEffect(() => { let headline = stops[tableType === "departures" ? stops.length - 1 : 0]?.stop ?? {name: "Chargement…"}
if (route !== undefined) {
setTrainType(getTrainType(train, route))
}
}, [train, route]);
useEffect(() => { let stopsFilter
if (trip.route !== undefined) {
fetch(`http://localhost:8000/api/gtfs/stop_time/?trip=${trip.id}&order=stop_sequence&limit=1000`)
.then(response => response.json())
.then(data => data.results)
.then(stop_times => {
Promise.all(stop_times.map(stop_time =>
fetch(`http://localhost:8000/api/gtfs/stop/${stop_time.stop}/`).then(response => response.json())
)).then(stops => {
setStopTimes(stop_times.map((stop_time, index) => {
stop_time.stop = stops[index]
return stop_time
}))
})
})
}
}, [trip.route])
let headline = stopTimes[tableType === "departures" ? stopTimes.length - 1 : 0]?.stop ?? {name: "Chargement…"}
let stops_filter
if (tableType === "departures") if (tableType === "departures")
stops_filter = (stop_time) => stop_time.stop_sequence > train.stop_sequence && stop_time.drop_off_type === 0 stopsFilter = (stop_time) => stop_time.stop_sequence > train.stop_sequence && stop_time.drop_off_type === 0
else else
stops_filter = (stop_time) => stop_time.stop_sequence < train.stop_sequence && stop_time.pickup_type === 0 stopsFilter = (stop_time) => stop_time.stop_sequence < train.stop_sequence && stop_time.pickup_type === 0
let stops_names = stopTimes.filter(stops_filter).map(stop_time => stop_time?.stop.name ?? "").join(", ") let stopsNames = stops.filter(stopsFilter).map(stopTime => stopTime?.stop.name ?? "").join(", ") ?? ""
return <> return <>
<StyledTableRow tableType={tableType}> <StyledTableRow tableType={tableType}>
@ -176,8 +174,8 @@ function TrainRow({train, tableType}) {
</Box> </Box>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Typography fontSize={24} fontWeight="bold">{headline.name}</Typography> <Typography fontSize={24} fontWeight="bold" data-stop-id={headline.id}>{headline.name}</Typography>
<span className="stops">{stops_names}</span> <span className="stops">{stopsNames}</span>
</TableCell> </TableCell>
</StyledTableRow> </StyledTableRow>
</> </>