From d400588524e271d8c0708ec042442b430b79a8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20PLANCKEEL?= Date: Fri, 30 Oct 2026 14:49:37 +0100 Subject: [PATCH] perf: paginate ListEvents, gzip the HTTP responses, cache the assets - ListEvents honours per_page and page (newest first, total in total_count); without per_page it still returns every event - The dashboard looks for the next event in the next 90 days instead of downloading the whole collection every 30 seconds - gzip on the SPA and the API (random jitter against BREACH), the MCP endpoint is left uncompressed - Hashed /assets/ are cached for a year, index.html is revalidated - One cache key for the catalog list: ['catalogs','list'] fetched the same data a second time and was not invalidated by catalog mutations - Default staleTime of 30s: changing page no longer refetches what was just loaded Co-Authored-By: Claude Opus 5.5 --- cmd/serv.go | 29 +++++++++++++++++++++++++++ go.mod | 2 +- internal/stores/event.go | 22 ++++++++++++++++++++ server/event.go | 13 ++++++++++++ server/event_scope_test.go | 22 ++++++++++++++++++++ web/src/components/LinksSearch.tsx | 5 ++--- web/src/lib/queries.ts | 9 +++++++++ web/src/main.tsx | 3 +++ web/src/pages/CatalogDependencies.tsx | 5 ++--- web/src/pages/CatalogDetail.tsx | 4 ++-- web/src/pages/CatalogTable.tsx | 5 ++--- web/src/pages/CreateCatalog.tsx | 4 ++-- web/src/pages/CreateDrift.tsx | 6 +++--- web/src/pages/CreateEvent.tsx | 6 +++--- web/src/pages/CreateRpaOperation.tsx | 6 +++--- web/src/pages/Dashboard.tsx | 24 +++++++++++++++------- web/src/pages/EventsCalendar.tsx | 6 +++--- web/src/pages/EventsOverlaps.tsx | 6 +++--- web/src/pages/EventsStreamline.tsx | 6 +++--- web/src/pages/EventsTimeline.tsx | 6 +++--- 20 files changed, 147 insertions(+), 42 deletions(-) create mode 100644 web/src/lib/queries.ts diff --git a/cmd/serv.go b/cmd/serv.go index fa2c97b..31a7eb9 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -28,6 +28,7 @@ import ( "github.com/bananaops/tracker/server" "github.com/go-openapi/runtime/server-middleware/docui" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/klauspost/compress/gzhttp" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" @@ -306,6 +307,7 @@ var serv = &cobra.Command{ _, err := os.Stat(path) if os.IsNotExist(err) { // File does not exist, serve index.html for SPA routing + w.Header().Set("Cache-Control", "no-cache") http.ServeFile(w, r, filepath.Join(frontendDir, "index.html")) return } else if err != nil { @@ -314,6 +316,14 @@ var serv = &cobra.Command{ return } + // Vite names the bundled assets after their content: they never + // change, everything else (index.html) is revalidated. + if strings.HasPrefix(r.URL.Path, "/assets/") { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } else { + w.Header().Set("Cache-Control", "no-cache") + } + // File exists, serve it fileServer.ServeHTTP(w, r) }) @@ -322,6 +332,25 @@ var serv = &cobra.Command{ httpHandler = mux } + // Compress the SPA and the API responses. The MCP endpoint streams + // (text/event-stream) and is left as is. The random jitter hides the + // exact compressed size of responses that hold secrets (BREACH). + gzipWrapper, err := gzhttp.NewWrapper( + gzhttp.RandomJitter(32, 0, false), + gzhttp.ExceptContentTypes([]string{"text/event-stream"}), + ) + if err != nil { + log.Fatalf("cannot create the gzip handler: %v", err) + } + uncompressed, compressed := httpHandler, gzipWrapper(httpHandler) + httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == server.MCPPath { + uncompressed.ServeHTTP(w, r) + return + } + compressed.ServeHTTP(w, r) + }) + // Resolve the principal of every HTTP request (SPA, API and custom handlers) httpHandler = auth.HTTPMiddleware(resolver, authCfg)(httpHandler) diff --git a/go.mod b/go.mod index 77b0334..df55e5a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/coreos/go-oidc/v3 v3.21.0 github.com/go-openapi/runtime/server-middleware v0.33.3 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/klauspost/compress v1.20.1 github.com/modelcontextprotocol/go-sdk v1.8.0 golang.org/x/crypto v0.58.0 golang.org/x/oauth2 v0.37.0 @@ -21,7 +22,6 @@ require ( github.com/golang/snappy v1.0.0 // indirect github.com/google/jsonschema-go v0.4.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.20.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/internal/stores/event.go b/internal/stores/event.go index 72e9c07..9bf71bb 100644 --- a/internal/stores/event.go +++ b/internal/stores/event.go @@ -45,6 +45,28 @@ func (c *EventStoreClient) List(ctx context.Context, scope auth.Scope) (results return } +// ListPage returns one page of events, newest first, and the number of +// events in scope. page starts at 1. +func (c *EventStoreClient) ListPage(ctx context.Context, scope auth.Scope, perPage, page int64) (results []*v1alpha1.Event, total int64, err error) { + filter := scopedFilter(bson.D{}, scope, eventServiceField) + total, err = c.collection.CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + opts := options.Find(). + SetSort(bson.D{{Key: "metadata.createdat.seconds", Value: -1}, {Key: "_id", Value: -1}}). + SetSkip((page - 1) * perPage). + SetLimit(perPage) + cursor, err := c.collection.Find(ctx, filter, opts) + if err != nil { + return nil, 0, err + } + if err = cursor.All(ctx, &results); err != nil { + return nil, 0, err + } + return results, total, nil +} + // Create takes the representation of an Event and creates it. Returns the server's representation of the Event, and an error, if there is any. func (c *EventStoreClient) Create(ctx context.Context, eventInsert *v1alpha1.Event) (result *v1alpha1.Event, err error) { result = &v1alpha1.Event{} diff --git a/server/event.go b/server/event.go index 3f028cc..0441556 100644 --- a/server/event.go +++ b/server/event.go @@ -81,6 +81,7 @@ func addChangelogEntry(event *v1alpha1.Event, changeType v1alpha1.ChangeType, us const ( changelogDefaultPerPage = 50 changelogMaxPerPage = 1000 + listEventsMaxPerPage = 1000 ) // shouldCreateLock détermine si un lock doit être créé pour cet événement @@ -359,6 +360,18 @@ func (e *Event) ListEvents( var eventsResult = &v1alpha1.ListEventsResponse{} var err error + // Without per_page the whole collection is returned, as before. + if perPage := i.GetPerPage().GetValue(); perPage > 0 { + page := max(int64(i.GetPage().GetValue()), 1) + var total int64 + eventsResult.Events, total, err = e.store.ListPage(ctx, authz.ScopeFromContext(ctx), int64(min(perPage, listEventsMaxPerPage)), page) + if err != nil { + return nil, err + } + eventsResult.TotalCount = uint32(total) // #nosec G115 -- a collection never holds 2^32 events + return eventsResult, nil + } + eventsResult.Events, err = e.store.List(context.Background(), authz.ScopeFromContext(ctx)) if err != nil { return nil, err diff --git a/server/event_scope_test.go b/server/event_scope_test.go index efdd835..f99d9ad 100644 --- a/server/event_scope_test.go +++ b/server/event_scope_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/wrapperspb" ) const eventSvc = "/tracker.event.v1alpha1.EventService/" @@ -140,6 +141,27 @@ func TestListEventsScope(t *testing.T) { require.Len(t, list(x.all).Events, 3) } +func TestListEventsPagination(t *testing.T) { + x := newScopeEnv(t) + page := func(p auth.Principal, perPage uint32, n int32) *eventv1.ListEventsResponse { + r, err := x.s.events.ListEvents(x.ctx(p, "ListEvents"), &eventv1.ListEventsRequest{ + PerPage: wrapperspb.UInt32(perPage), Page: wrapperspb.Int32(n), + }) + require.NoError(t, err) + return r + } + first, second := page(x.all, 2, 1), page(x.all, 2, 2) + require.Len(t, first.Events, 2) + require.Len(t, second.Events, 1) + require.EqualValues(t, 3, first.TotalCount) + require.ElementsMatch(t, []string{x.a, x.b, x.e}, append(eventIDs(first.Events), eventIDs(second.Events)...)) + require.Empty(t, page(x.all, 2, 3).Events) + + scoped := page(x.pa, 2, 1) + require.ElementsMatch(t, []string{x.a}, eventIDs(scoped.Events)) + require.EqualValues(t, 1, scoped.TotalCount) +} + func TestSearchEventsScope(t *testing.T) { x := newScopeEnv(t) search := func(p auth.Principal, req *eventv1.SearchEventsRequest) []*eventv1.Event { diff --git a/web/src/components/LinksSearch.tsx b/web/src/components/LinksSearch.tsx index d551812..562e3eb 100644 --- a/web/src/components/LinksSearch.tsx +++ b/web/src/components/LinksSearch.tsx @@ -5,7 +5,7 @@ import { load as yamlLoad } from 'js-yaml' import { useNavigate } from 'react-router-dom' import { Dialog, DialogContent } from '@/components/ui/dialog' import { getLinksConfig, getHomerUrl, type LinkItem, type LinkGroup } from '../config' -import { catalogApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import type { Catalog } from '../types/api' interface HomerLink { name: string; url: string; icon?: string } @@ -90,8 +90,7 @@ export default function LinksSearch({ collapsed = false }: { collapsed?: boolean }) const { data: catalogData } = useQuery({ - queryKey: ['catalog', 'list'], - queryFn: () => catalogApi.list({ perPage: 500 }), + ...catalogListQuery, staleTime: 60_000, }) diff --git a/web/src/lib/queries.ts b/web/src/lib/queries.ts new file mode 100644 index 0000000..b29af79 --- /dev/null +++ b/web/src/lib/queries.ts @@ -0,0 +1,9 @@ +import { catalogApi } from './api' + +// The whole catalog, shared by every page under one cache key. The backend +// returns every catalog whatever the page size, and mutations invalidate the +// ['catalog'] prefix. +export const catalogListQuery = { + queryKey: ['catalog', 'list'], + queryFn: () => catalogApi.list({ perPage: 1000 }), +} as const diff --git a/web/src/main.tsx b/web/src/main.tsx index 492b7b5..18baebf 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -43,6 +43,9 @@ const queryClient = new QueryClient({ queries: { refetchOnWindowFocus: false, retry: 1, + // Data fetched less than 30s ago is reused when a page mounts instead of + // refetched; mutations still invalidate it right away. + staleTime: 30_000, }, }, }) diff --git a/web/src/pages/CatalogDependencies.tsx b/web/src/pages/CatalogDependencies.tsx index 3b3ffad..84ea245 100644 --- a/web/src/pages/CatalogDependencies.tsx +++ b/web/src/pages/CatalogDependencies.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' -import { catalogApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { SLALevel, CatalogType, Platform, type Catalog } from '../types/api' import { ArrowLeft, GitBranch, Search, X, Grid, List, Maximize2, ZoomIn, ZoomOut, Server, Zap, Activity, Package } from 'lucide-react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -217,8 +217,7 @@ export default function CatalogDependencies() { } const { data: allCatalogs, isLoading } = useQuery({ - queryKey: ['catalog', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, }) // Build complete dependency graph with improved layouts diff --git a/web/src/pages/CatalogDetail.tsx b/web/src/pages/CatalogDetail.tsx index a7d69e3..41454f7 100644 --- a/web/src/pages/CatalogDetail.tsx +++ b/web/src/pages/CatalogDetail.tsx @@ -1,6 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useParams, useNavigate, Link } from 'react-router-dom' import { catalogApi, eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { SLALevel, CatalogType, Language, Platform, CommunicationType, type Catalog, type InfrastructureType, Status } from '../types/api' import { Package, GitBranch, Activity, ExternalLink, Github, Code, Server, Edit, Trash2, AlertTriangle, X, Mail, Zap, Plus, Database, HardDrive, Network, MessageSquare, Shield, Cloud, Check, Clock, Rocket, Maximize2, Minimize2 } from 'lucide-react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -466,8 +467,7 @@ export default function CatalogDetail({ serviceNameProp, onClose }: CatalogDetai }, []) const { data: allCatalogs, isLoading: catalogsLoading } = useQuery({ - queryKey: ['catalog', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, }) // Fetch deployments for this service (last 30 days) diff --git a/web/src/pages/CatalogTable.tsx b/web/src/pages/CatalogTable.tsx index dae7a14..3bf0ccc 100644 --- a/web/src/pages/CatalogTable.tsx +++ b/web/src/pages/CatalogTable.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query' -import { catalogApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { CatalogType, Language, SLALevel, Platform, CommunicationType, DashboardType, type Catalog } from '../types/api' import { Package, BookOpen, Search, X, Server, Cloud, Zap, Globe, Shield, HardDrive, Activity, Mail, Filter, SlidersHorizontal } from 'lucide-react' import { useState, useMemo } from 'react' @@ -26,8 +26,7 @@ export default function CatalogTable() { const { data, isLoading } = useQuery({ - queryKey: ['catalog', 'list'], - queryFn: () => catalogApi.list({ perPage: 100 }), + ...catalogListQuery, }) diff --git a/web/src/pages/CreateCatalog.tsx b/web/src/pages/CreateCatalog.tsx index 8dd2e71..96b4f0e 100644 --- a/web/src/pages/CreateCatalog.tsx +++ b/web/src/pages/CreateCatalog.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, type FormEvent, type ReactNode } from 're import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query' import { useNavigate, useParams } from 'react-router-dom' import { catalogApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { CatalogType, Language, SLALevel, Platform, type Catalog, type SLA, type UsedDeliverable, type CommunicationChannel, type DashboardLink, type VulnerabilitySummary } from '../types/api' import { Package, X, GitBranch, Shield, Plus } from 'lucide-react' import DependencySelector from '../components/DependencySelector' @@ -55,8 +56,7 @@ export default function CreateCatalog({ asPanel = false, onClose, onSuccess }: C // Fetch all catalogs for dependency selection const { data: allCatalogs } = useQuery({ - queryKey: ['catalog', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, }) // Get list of available service names (excluding current service) diff --git a/web/src/pages/CreateDrift.tsx b/web/src/pages/CreateDrift.tsx index 1bb8ec5..04338b7 100644 --- a/web/src/pages/CreateDrift.tsx +++ b/web/src/pages/CreateDrift.tsx @@ -1,6 +1,7 @@ import { useState, type FormEvent, type ReactNode } from 'react' import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query' -import { eventsApi, catalogApi } from '../lib/api' +import { eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { useNavigate } from 'react-router-dom' import { EventType, Priority, Status, Environment } from '../types/api' import type { CreateEventRequest } from '../types/api' @@ -27,8 +28,7 @@ export default function CreateDrift({ asPanel = false, onClose, onSuccess }: Cre const [showToast, setShowToast] = useState(false) const { data: catalogData, isLoading: catalogLoading } = useQuery({ - queryKey: ['catalogs', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, }) const catalogServices = catalogData?.catalogs.map((c) => c.name).sort() || [] diff --git a/web/src/pages/CreateEvent.tsx b/web/src/pages/CreateEvent.tsx index 236e800..0360f45 100644 --- a/web/src/pages/CreateEvent.tsx +++ b/web/src/pages/CreateEvent.tsx @@ -1,7 +1,8 @@ import { useState, type FormEvent, type ReactNode } from 'react' import { apiErrorMessage } from '../lib/errors' import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query' -import { eventsApi, catalogApi } from '../lib/api' +import { eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { useNavigate } from 'react-router-dom' import { EventType, Priority, Status, Environment } from '../types/api' import type { CreateEventRequest } from '../types/api' @@ -27,8 +28,7 @@ export default function CreateEvent({ asPanel = false, onClose, onSuccess }: Cre const [showToast, setShowToast] = useState(false) const { data: catalogData, isLoading: catalogLoading } = useQuery({ - queryKey: ['catalogs', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, }) const catalogServices = catalogData?.catalogs.map((c) => c.name).sort() || [] diff --git a/web/src/pages/CreateRpaOperation.tsx b/web/src/pages/CreateRpaOperation.tsx index aa3cc1a..ed49c90 100644 --- a/web/src/pages/CreateRpaOperation.tsx +++ b/web/src/pages/CreateRpaOperation.tsx @@ -1,6 +1,7 @@ import { useState, type FormEvent, type ReactNode } from 'react' import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query' -import { eventsApi, catalogApi } from '../lib/api' +import { eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { useNavigate } from 'react-router-dom' import { EventType, Priority, Status, Environment } from '../types/api' import type { CreateEventRequest } from '../types/api' @@ -26,8 +27,7 @@ export default function CreateRpaOperation({ asPanel = false, onClose, onSuccess const [showToast, setShowToast] = useState(false) const { data: catalogData, isLoading: catalogLoading } = useQuery({ - queryKey: ['catalogs', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, }) const catalogServices = catalogData?.catalogs.map((c) => c.name).sort() || [] diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index b4f1a1e..83f5eed 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -49,6 +49,9 @@ const T = Object.fromEntries( ) as Record const a = (key: keyof typeof V, opacity: number) => `rgb(var(${V[key]}) / ${opacity})` +// How far ahead the dashboard looks for the next planned event. +const UPCOMING_HORIZON_DAYS = 90 + const C = { primary: '#1B3575', accent: '#E8580A', @@ -299,9 +302,16 @@ export default function Dashboard() { refetchInterval: 30000, }) - const { data: allEventsData } = useQuery({ - queryKey: ['events', 'dashboard-all'], - queryFn: () => eventsApi.list({ perPage: 1000, page: 1 }), + // Only what is still ahead feeds "next": the whole collection is not needed. + const { data: upcomingEventsData } = useQuery({ + queryKey: ['events', 'dashboard-upcoming'], + queryFn: () => { + const now = new Date() + return eventsApi.search({ + startDate: now.toISOString(), + endDate: new Date(now.getTime() + UPCOMING_HORIZON_DAYS * 86_400_000).toISOString(), + }) + }, refetchInterval: 30000, }) @@ -309,7 +319,7 @@ export default function Dashboard() { useEffect(() => { setLastUpdated(new Date()) setCountdown(30) - }, [todayEvents, allEventsData]) + }, [todayEvents, upcomingEventsData]) // Countdown timer useEffect(() => { @@ -320,7 +330,7 @@ export default function Dashboard() { }, []) const events: Event[] = useMemo(() => (todayEvents?.events || []) as Event[], [todayEvents]) - const allEvents: Event[] = useMemo(() => (allEventsData?.events || []) as Event[], [allEventsData]) + const upcomingEvents: Event[] = useMemo(() => (upcomingEventsData?.events || []) as Event[], [upcomingEventsData]) const derived = useMemo(() => { const toDerivedRow = (event: Event): DerivedRow => { @@ -344,7 +354,7 @@ export default function Dashboard() { }, {}) const now = new Date() - const nextRows: DerivedRow[] = (allEvents.length > 0 ? allEvents : events) + const nextRows: DerivedRow[] = (upcomingEvents.length > 0 ? upcomingEvents : events) .map((event: Event) => toDerivedRow(event)) .filter((row: DerivedRow) => row.start >= now) .sort((aRow: DerivedRow, bRow: DerivedRow) => { @@ -403,7 +413,7 @@ export default function Dashboard() { services: (Object.values(serviceCount) as ServiceSummary[]).sort((aRow: ServiceSummary, bRow: ServiceSummary) => bRow.count - aRow.count).slice(0, 5), timeline, } - }, [events, allEvents]) + }, [events, upcomingEvents]) const nowHour = new Date().getHours() + new Date().getMinutes() / 60 diff --git a/web/src/pages/EventsCalendar.tsx b/web/src/pages/EventsCalendar.tsx index 24d76c9..93948a3 100644 --- a/web/src/pages/EventsCalendar.tsx +++ b/web/src/pages/EventsCalendar.tsx @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query' -import { eventsApi, catalogApi } from '../lib/api' +import { eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { useEffect, useMemo, useRef, useState } from 'react' import type { CSSProperties } from 'react' import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isToday, isWithinInterval, startOfDay, endOfDay } from 'date-fns' @@ -46,8 +47,7 @@ export default function EventsCalendar() { }) const { data: catalogData, isLoading: catalogLoading } = useQuery({ - queryKey: ['catalogs', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, refetchInterval: 30_000, }) diff --git a/web/src/pages/EventsOverlaps.tsx b/web/src/pages/EventsOverlaps.tsx index cca8a76..787ae8a 100644 --- a/web/src/pages/EventsOverlaps.tsx +++ b/web/src/pages/EventsOverlaps.tsx @@ -1,6 +1,7 @@ import { useState, useMemo } from 'react' import { useQuery } from '@tanstack/react-query' -import { eventsApi, catalogApi } from '../lib/api' +import { eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { format, startOfDay, endOfDay, addDays, subDays } from 'date-fns' import { fr } from 'date-fns/locale' import { AlertTriangle, Calendar as CalendarIcon, ChevronLeft, ChevronRight, Users, Layers, Globe, CheckCircle } from 'lucide-react' @@ -114,8 +115,7 @@ export default function EventsOverlaps() { }) const { data: catalogData, isLoading: catalogLoading } = useQuery({ - queryKey: ['catalogs', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, refetchInterval: 30_000, }) diff --git a/web/src/pages/EventsStreamline.tsx b/web/src/pages/EventsStreamline.tsx index 863dea8..01e3c73 100644 --- a/web/src/pages/EventsStreamline.tsx +++ b/web/src/pages/EventsStreamline.tsx @@ -1,7 +1,8 @@ import { useState, useMemo, useRef, useEffect } from 'react' import type { CSSProperties } from 'react' import { useQuery } from '@tanstack/react-query' -import { eventsApi, catalogApi } from '../lib/api' +import { eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { format, addDays, isWithinInterval, isSameDay, startOfDay, endOfDay, addHours, getHours, subDays, subHours } from 'date-fns' import { enUS } from 'date-fns/locale' import { Package, Globe, Calendar, Clock, ChevronDown, AlertTriangle } from 'lucide-react' @@ -58,8 +59,7 @@ export default function EventsStreamline() { }) const { data: catalogData, isLoading: catalogLoading } = useQuery({ - queryKey: ['catalogs', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, refetchInterval: 30_000, }) diff --git a/web/src/pages/EventsTimeline.tsx b/web/src/pages/EventsTimeline.tsx index 85a81d5..e439949 100644 --- a/web/src/pages/EventsTimeline.tsx +++ b/web/src/pages/EventsTimeline.tsx @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query' import type { CSSProperties } from 'react' -import { eventsApi, catalogApi } from '../lib/api' +import { eventsApi } from '../lib/api' +import { catalogListQuery } from '../lib/queries' import { format, subDays, startOfDay, endOfDay, subHours, isToday, isYesterday } from 'date-fns' import { enUS } from 'date-fns/locale' import type { Event } from '../types/api' @@ -79,8 +80,7 @@ export default function EventsTimeline() { }) const { data: catalogData, isLoading: catalogLoading } = useQuery({ - queryKey: ['catalogs', 'list'], - queryFn: () => catalogApi.list({ perPage: 1000 }), + ...catalogListQuery, refetchInterval: 30_000, })