From 4ac11551c91b345e303689fbde5c3867bdf9b958 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:10:32 +0100 Subject: [PATCH] feat: add auto-refresh toggle wired to all polling queries Upgrade @cameleer/design-system to ^0.1.3 which adds LIVE/PAUSED toggle to TopBar backed by autoRefresh state in GlobalFilterProvider. Add useRefreshInterval() hook that returns the polling interval when auto-refresh is on, or false when paused. Wire it into all query hooks that use refetchInterval (executions, catalog, agents, metrics, admin database/opensearch). Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/package-lock.json | 61 +++++++++++----------- ui/package.json | 2 +- ui/src/api/queries/admin/database.ts | 13 +++-- ui/src/api/queries/admin/opensearch.ts | 10 ++-- ui/src/api/queries/agent-metrics.ts | 4 +- ui/src/api/queries/agents.ts | 7 ++- ui/src/api/queries/catalog.ts | 7 ++- ui/src/api/queries/executions.ts | 10 ++-- ui/src/api/queries/processor-metrics.ts | 4 +- ui/src/api/queries/use-refresh-interval.ts | 10 ++++ 10 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 ui/src/api/queries/use-refresh-interval.ts diff --git a/ui/package-lock.json b/ui/package-lock.json index 2083b354..935d9817 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8,7 +8,7 @@ "name": "ui", "version": "0.0.0", "dependencies": { - "@cameleer/design-system": "^0.1.1", + "@cameleer/design-system": "file:../../design-system", "@tanstack/react-query": "^5.90.21", "openapi-fetch": "^0.17.0", "react": "^19.2.4", @@ -35,6 +35,34 @@ "vite": "^8.0.0" } }, + "../../design-system": { + "name": "@cameleer/design-system", + "version": "0.1.3", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.58.2", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "happy-dom": "^20.8.4", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vite-plugin-dts": "^4.5.4", + "vitest": "^3.0.0" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -276,19 +304,8 @@ } }, "node_modules/@cameleer/design-system": { - "version": "0.1.1", - "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.1/design-system-0.1.1.tgz", - "integrity": "sha512-5PNIDuw9vcM0BVQIdiJQalWoMJPUZj01f6rX3unHDCF1gE1mBHCFZ41RPy2QsuKmjgZ1azenXXdnHL/XGVostQ==", - "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "^7.0.0" - }, - "peerDependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "^7.0.0" - } + "resolved": "../../design-system", + "link": true }, "node_modules/@emnapi/core": { "version": "1.9.1", @@ -2955,22 +2972,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", - "license": "MIT", - "dependencies": { - "react-router": "7.13.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 4bbd4cca..e297d277 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,7 +14,7 @@ "generate-api:live": "curl -s http://localhost:8081/api/v1/api-docs -o src/api/openapi.json && openapi-typescript src/api/openapi.json -o src/api/schema.d.ts" }, "dependencies": { - "@cameleer/design-system": "^0.1.1", + "@cameleer/design-system": "^0.1.3", "@tanstack/react-query": "^5.90.21", "openapi-fetch": "^0.17.0", "react": "^19.2.4", diff --git a/ui/src/api/queries/admin/database.ts b/ui/src/api/queries/admin/database.ts index ff9173d2..44cae732 100644 --- a/ui/src/api/queries/admin/database.ts +++ b/ui/src/api/queries/admin/database.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { adminFetch } from './admin-api'; +import { useRefreshInterval } from '../use-refresh-interval'; // ── Types ────────────────────────────────────────────────────────────── @@ -38,34 +39,38 @@ export interface ActiveQuery { // ── Query Hooks ──────────────────────────────────────────────────────── export function useDatabaseStatus() { + const refetchInterval = useRefreshInterval(30_000); return useQuery({ queryKey: ['admin', 'database', 'status'], queryFn: () => adminFetch('/database/status'), - refetchInterval: 30_000, + refetchInterval, }); } export function useConnectionPool() { + const refetchInterval = useRefreshInterval(10_000); return useQuery({ queryKey: ['admin', 'database', 'pool'], queryFn: () => adminFetch('/database/pool'), - refetchInterval: 10_000, + refetchInterval, }); } export function useDatabaseTables() { + const refetchInterval = useRefreshInterval(60_000); return useQuery({ queryKey: ['admin', 'database', 'tables'], queryFn: () => adminFetch('/database/tables'), - refetchInterval: 60_000, + refetchInterval, }); } export function useActiveQueries() { + const refetchInterval = useRefreshInterval(5_000); return useQuery({ queryKey: ['admin', 'database', 'queries'], queryFn: () => adminFetch('/database/queries'), - refetchInterval: 5_000, + refetchInterval, }); } diff --git a/ui/src/api/queries/admin/opensearch.ts b/ui/src/api/queries/admin/opensearch.ts index a242968d..1482b9cf 100644 --- a/ui/src/api/queries/admin/opensearch.ts +++ b/ui/src/api/queries/admin/opensearch.ts @@ -1,5 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { adminFetch } from './admin-api'; +import { useRefreshInterval } from '../use-refresh-interval'; // ── Types ────────────────────────────────────────────────────────────── @@ -53,18 +54,20 @@ export interface PerformanceStats { // ── Query Hooks ──────────────────────────────────────────────────────── export function useOpenSearchStatus() { + const refetchInterval = useRefreshInterval(30_000); return useQuery({ queryKey: ['admin', 'opensearch', 'status'], queryFn: () => adminFetch('/opensearch/status'), - refetchInterval: 30_000, + refetchInterval, }); } export function usePipelineStats() { + const refetchInterval = useRefreshInterval(10_000); return useQuery({ queryKey: ['admin', 'opensearch', 'pipeline'], queryFn: () => adminFetch('/opensearch/pipeline'), - refetchInterval: 10_000, + refetchInterval, }); } @@ -83,10 +86,11 @@ export function useOpenSearchIndices(page = 0, size = 20, search = '') { } export function useOpenSearchPerformance() { + const refetchInterval = useRefreshInterval(30_000); return useQuery({ queryKey: ['admin', 'opensearch', 'performance'], queryFn: () => adminFetch('/opensearch/performance'), - refetchInterval: 30_000, + refetchInterval, }); } diff --git a/ui/src/api/queries/agent-metrics.ts b/ui/src/api/queries/agent-metrics.ts index c1774c67..b105015f 100644 --- a/ui/src/api/queries/agent-metrics.ts +++ b/ui/src/api/queries/agent-metrics.ts @@ -1,8 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { config } from '../../config'; import { useAuthStore } from '../../auth/auth-store'; +import { useRefreshInterval } from './use-refresh-interval'; export function useAgentMetrics(agentId: string | null, names: string[], buckets = 60) { + const refetchInterval = useRefreshInterval(30_000); return useQuery({ queryKey: ['agent-metrics', agentId, names.join(','), buckets], queryFn: async () => { @@ -21,6 +23,6 @@ export function useAgentMetrics(agentId: string | null, names: string[], buckets return res.json() as Promise<{ metrics: Record> }>; }, enabled: !!agentId && names.length > 0, - refetchInterval: 30_000, + refetchInterval, }); } diff --git a/ui/src/api/queries/agents.ts b/ui/src/api/queries/agents.ts index 56763c41..710c7839 100644 --- a/ui/src/api/queries/agents.ts +++ b/ui/src/api/queries/agents.ts @@ -2,8 +2,10 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; import { config } from '../../config'; import { useAuthStore } from '../../auth/auth-store'; +import { useRefreshInterval } from './use-refresh-interval'; export function useAgents(status?: string, application?: string) { + const refetchInterval = useRefreshInterval(10_000); return useQuery({ queryKey: ['agents', status, application], queryFn: async () => { @@ -13,11 +15,12 @@ export function useAgents(status?: string, application?: string) { if (error) throw new Error('Failed to load agents'); return data!; }, - refetchInterval: 10_000, + refetchInterval, }); } export function useAgentEvents(appId?: string, agentId?: string, limit = 50) { + const refetchInterval = useRefreshInterval(15_000); return useQuery({ queryKey: ['agents', 'events', appId, agentId, limit], queryFn: async () => { @@ -35,6 +38,6 @@ export function useAgentEvents(appId?: string, agentId?: string, limit = 50) { if (!res.ok) throw new Error('Failed to load agent events'); return res.json(); }, - refetchInterval: 15_000, + refetchInterval, }); } diff --git a/ui/src/api/queries/catalog.ts b/ui/src/api/queries/catalog.ts index 66f167aa..2b9184da 100644 --- a/ui/src/api/queries/catalog.ts +++ b/ui/src/api/queries/catalog.ts @@ -1,8 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { config } from '../../config'; import { useAuthStore } from '../../auth/auth-store'; +import { useRefreshInterval } from './use-refresh-interval'; export function useRouteCatalog() { + const refetchInterval = useRefreshInterval(15_000); return useQuery({ queryKey: ['routes', 'catalog'], queryFn: async () => { @@ -16,11 +18,12 @@ export function useRouteCatalog() { if (!res.ok) throw new Error('Failed to load route catalog'); return res.json(); }, - refetchInterval: 15_000, + refetchInterval, }); } export function useRouteMetrics(from?: string, to?: string, appId?: string) { + const refetchInterval = useRefreshInterval(30_000); return useQuery({ queryKey: ['routes', 'metrics', from, to, appId], queryFn: async () => { @@ -38,6 +41,6 @@ export function useRouteMetrics(from?: string, to?: string, appId?: string) { if (!res.ok) throw new Error('Failed to load route metrics'); return res.json(); }, - refetchInterval: 30_000, + refetchInterval, }); } diff --git a/ui/src/api/queries/executions.ts b/ui/src/api/queries/executions.ts index d898ef21..c008193a 100644 --- a/ui/src/api/queries/executions.ts +++ b/ui/src/api/queries/executions.ts @@ -1,6 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; import type { SearchRequest } from '../types'; +import { useRefreshInterval } from './use-refresh-interval'; export function useExecutionStats( timeFrom: string | undefined, @@ -8,6 +9,7 @@ export function useExecutionStats( routeId?: string, application?: string, ) { + const refetchInterval = useRefreshInterval(10_000); return useQuery({ queryKey: ['executions', 'stats', timeFrom, timeTo, routeId, application], queryFn: async () => { @@ -26,11 +28,12 @@ export function useExecutionStats( }, enabled: !!timeFrom, placeholderData: (prev) => prev, - refetchInterval: 10_000, + refetchInterval, }); } export function useSearchExecutions(filters: SearchRequest, live = false) { + const refetchInterval = useRefreshInterval(5_000); return useQuery({ queryKey: ['executions', 'search', filters], queryFn: async () => { @@ -41,7 +44,7 @@ export function useSearchExecutions(filters: SearchRequest, live = false) { return data!; }, placeholderData: (prev) => prev, - refetchInterval: live ? 5_000 : false, + refetchInterval: live ? refetchInterval : false, }); } @@ -51,6 +54,7 @@ export function useStatsTimeseries( routeId?: string, application?: string, ) { + const refetchInterval = useRefreshInterval(30_000); return useQuery({ queryKey: ['executions', 'timeseries', timeFrom, timeTo, routeId, application], queryFn: async () => { @@ -70,7 +74,7 @@ export function useStatsTimeseries( }, enabled: !!timeFrom, placeholderData: (prev) => prev, - refetchInterval: 30_000, + refetchInterval, }); } diff --git a/ui/src/api/queries/processor-metrics.ts b/ui/src/api/queries/processor-metrics.ts index e9cc39f4..fc576cf9 100644 --- a/ui/src/api/queries/processor-metrics.ts +++ b/ui/src/api/queries/processor-metrics.ts @@ -1,8 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { config } from '../../config'; import { useAuthStore } from '../../auth/auth-store'; +import { useRefreshInterval } from './use-refresh-interval'; export function useProcessorMetrics(routeId: string | null, appId?: string) { + const refetchInterval = useRefreshInterval(30_000); return useQuery({ queryKey: ['processor-metrics', routeId, appId], queryFn: async () => { @@ -20,6 +22,6 @@ export function useProcessorMetrics(routeId: string | null, appId?: string) { return res.json(); }, enabled: !!routeId, - refetchInterval: 30_000, + refetchInterval, }); } diff --git a/ui/src/api/queries/use-refresh-interval.ts b/ui/src/api/queries/use-refresh-interval.ts new file mode 100644 index 00000000..d3a9c6d3 --- /dev/null +++ b/ui/src/api/queries/use-refresh-interval.ts @@ -0,0 +1,10 @@ +import { useGlobalFilters } from '@cameleer/design-system'; + +/** + * Returns the given interval when auto-refresh is enabled, or `false` when paused. + * Use as `refetchInterval` in React Query hooks. + */ +export function useRefreshInterval(intervalMs: number): number | false { + const { autoRefresh } = useGlobalFilters(); + return autoRefresh ? intervalMs : false; +}