feat: add auto-refresh toggle wired to all polling queries
Some checks failed
CI / build (push) Failing after 51s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-24 18:10:32 +01:00
parent 6fea5f2c5b
commit 4ac11551c9
10 changed files with 81 additions and 47 deletions

61
ui/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "ui", "name": "ui",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@cameleer/design-system": "^0.1.1", "@cameleer/design-system": "file:../../design-system",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
"react": "^19.2.4", "react": "^19.2.4",
@@ -35,6 +35,34 @@
"vite": "^8.0.0" "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": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -276,19 +304,8 @@
} }
}, },
"node_modules/@cameleer/design-system": { "node_modules/@cameleer/design-system": {
"version": "0.1.1", "resolved": "../../design-system",
"resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.1/design-system-0.1.1.tgz", "link": true
"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"
}
}, },
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.9.1", "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": { "node_modules/require-from-string": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",

View File

@@ -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" "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": { "dependencies": {
"@cameleer/design-system": "^0.1.1", "@cameleer/design-system": "^0.1.3",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"openapi-fetch": "^0.17.0", "openapi-fetch": "^0.17.0",
"react": "^19.2.4", "react": "^19.2.4",

View File

@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { adminFetch } from './admin-api'; import { adminFetch } from './admin-api';
import { useRefreshInterval } from '../use-refresh-interval';
// ── Types ────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────
@@ -38,34 +39,38 @@ export interface ActiveQuery {
// ── Query Hooks ──────────────────────────────────────────────────────── // ── Query Hooks ────────────────────────────────────────────────────────
export function useDatabaseStatus() { export function useDatabaseStatus() {
const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['admin', 'database', 'status'], queryKey: ['admin', 'database', 'status'],
queryFn: () => adminFetch<DatabaseStatus>('/database/status'), queryFn: () => adminFetch<DatabaseStatus>('/database/status'),
refetchInterval: 30_000, refetchInterval,
}); });
} }
export function useConnectionPool() { export function useConnectionPool() {
const refetchInterval = useRefreshInterval(10_000);
return useQuery({ return useQuery({
queryKey: ['admin', 'database', 'pool'], queryKey: ['admin', 'database', 'pool'],
queryFn: () => adminFetch<PoolStats>('/database/pool'), queryFn: () => adminFetch<PoolStats>('/database/pool'),
refetchInterval: 10_000, refetchInterval,
}); });
} }
export function useDatabaseTables() { export function useDatabaseTables() {
const refetchInterval = useRefreshInterval(60_000);
return useQuery({ return useQuery({
queryKey: ['admin', 'database', 'tables'], queryKey: ['admin', 'database', 'tables'],
queryFn: () => adminFetch<TableInfo[]>('/database/tables'), queryFn: () => adminFetch<TableInfo[]>('/database/tables'),
refetchInterval: 60_000, refetchInterval,
}); });
} }
export function useActiveQueries() { export function useActiveQueries() {
const refetchInterval = useRefreshInterval(5_000);
return useQuery({ return useQuery({
queryKey: ['admin', 'database', 'queries'], queryKey: ['admin', 'database', 'queries'],
queryFn: () => adminFetch<ActiveQuery[]>('/database/queries'), queryFn: () => adminFetch<ActiveQuery[]>('/database/queries'),
refetchInterval: 5_000, refetchInterval,
}); });
} }

View File

@@ -1,5 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { adminFetch } from './admin-api'; import { adminFetch } from './admin-api';
import { useRefreshInterval } from '../use-refresh-interval';
// ── Types ────────────────────────────────────────────────────────────── // ── Types ──────────────────────────────────────────────────────────────
@@ -53,18 +54,20 @@ export interface PerformanceStats {
// ── Query Hooks ──────────────────────────────────────────────────────── // ── Query Hooks ────────────────────────────────────────────────────────
export function useOpenSearchStatus() { export function useOpenSearchStatus() {
const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['admin', 'opensearch', 'status'], queryKey: ['admin', 'opensearch', 'status'],
queryFn: () => adminFetch<OpenSearchStatus>('/opensearch/status'), queryFn: () => adminFetch<OpenSearchStatus>('/opensearch/status'),
refetchInterval: 30_000, refetchInterval,
}); });
} }
export function usePipelineStats() { export function usePipelineStats() {
const refetchInterval = useRefreshInterval(10_000);
return useQuery({ return useQuery({
queryKey: ['admin', 'opensearch', 'pipeline'], queryKey: ['admin', 'opensearch', 'pipeline'],
queryFn: () => adminFetch<PipelineStats>('/opensearch/pipeline'), queryFn: () => adminFetch<PipelineStats>('/opensearch/pipeline'),
refetchInterval: 10_000, refetchInterval,
}); });
} }
@@ -83,10 +86,11 @@ export function useOpenSearchIndices(page = 0, size = 20, search = '') {
} }
export function useOpenSearchPerformance() { export function useOpenSearchPerformance() {
const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['admin', 'opensearch', 'performance'], queryKey: ['admin', 'opensearch', 'performance'],
queryFn: () => adminFetch<PerformanceStats>('/opensearch/performance'), queryFn: () => adminFetch<PerformanceStats>('/opensearch/performance'),
refetchInterval: 30_000, refetchInterval,
}); });
} }

View File

@@ -1,8 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { config } from '../../config'; import { config } from '../../config';
import { useAuthStore } from '../../auth/auth-store'; import { useAuthStore } from '../../auth/auth-store';
import { useRefreshInterval } from './use-refresh-interval';
export function useAgentMetrics(agentId: string | null, names: string[], buckets = 60) { export function useAgentMetrics(agentId: string | null, names: string[], buckets = 60) {
const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['agent-metrics', agentId, names.join(','), buckets], queryKey: ['agent-metrics', agentId, names.join(','), buckets],
queryFn: async () => { queryFn: async () => {
@@ -21,6 +23,6 @@ export function useAgentMetrics(agentId: string | null, names: string[], buckets
return res.json() as Promise<{ metrics: Record<string, Array<{ time: string; value: number }>> }>; return res.json() as Promise<{ metrics: Record<string, Array<{ time: string; value: number }>> }>;
}, },
enabled: !!agentId && names.length > 0, enabled: !!agentId && names.length > 0,
refetchInterval: 30_000, refetchInterval,
}); });
} }

View File

@@ -2,8 +2,10 @@ import { useQuery } from '@tanstack/react-query';
import { api } from '../client'; import { api } from '../client';
import { config } from '../../config'; import { config } from '../../config';
import { useAuthStore } from '../../auth/auth-store'; import { useAuthStore } from '../../auth/auth-store';
import { useRefreshInterval } from './use-refresh-interval';
export function useAgents(status?: string, application?: string) { export function useAgents(status?: string, application?: string) {
const refetchInterval = useRefreshInterval(10_000);
return useQuery({ return useQuery({
queryKey: ['agents', status, application], queryKey: ['agents', status, application],
queryFn: async () => { queryFn: async () => {
@@ -13,11 +15,12 @@ export function useAgents(status?: string, application?: string) {
if (error) throw new Error('Failed to load agents'); if (error) throw new Error('Failed to load agents');
return data!; return data!;
}, },
refetchInterval: 10_000, refetchInterval,
}); });
} }
export function useAgentEvents(appId?: string, agentId?: string, limit = 50) { export function useAgentEvents(appId?: string, agentId?: string, limit = 50) {
const refetchInterval = useRefreshInterval(15_000);
return useQuery({ return useQuery({
queryKey: ['agents', 'events', appId, agentId, limit], queryKey: ['agents', 'events', appId, agentId, limit],
queryFn: async () => { 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'); if (!res.ok) throw new Error('Failed to load agent events');
return res.json(); return res.json();
}, },
refetchInterval: 15_000, refetchInterval,
}); });
} }

View File

@@ -1,8 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { config } from '../../config'; import { config } from '../../config';
import { useAuthStore } from '../../auth/auth-store'; import { useAuthStore } from '../../auth/auth-store';
import { useRefreshInterval } from './use-refresh-interval';
export function useRouteCatalog() { export function useRouteCatalog() {
const refetchInterval = useRefreshInterval(15_000);
return useQuery({ return useQuery({
queryKey: ['routes', 'catalog'], queryKey: ['routes', 'catalog'],
queryFn: async () => { queryFn: async () => {
@@ -16,11 +18,12 @@ export function useRouteCatalog() {
if (!res.ok) throw new Error('Failed to load route catalog'); if (!res.ok) throw new Error('Failed to load route catalog');
return res.json(); return res.json();
}, },
refetchInterval: 15_000, refetchInterval,
}); });
} }
export function useRouteMetrics(from?: string, to?: string, appId?: string) { export function useRouteMetrics(from?: string, to?: string, appId?: string) {
const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['routes', 'metrics', from, to, appId], queryKey: ['routes', 'metrics', from, to, appId],
queryFn: async () => { 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'); if (!res.ok) throw new Error('Failed to load route metrics');
return res.json(); return res.json();
}, },
refetchInterval: 30_000, refetchInterval,
}); });
} }

View File

@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '../client'; import { api } from '../client';
import type { SearchRequest } from '../types'; import type { SearchRequest } from '../types';
import { useRefreshInterval } from './use-refresh-interval';
export function useExecutionStats( export function useExecutionStats(
timeFrom: string | undefined, timeFrom: string | undefined,
@@ -8,6 +9,7 @@ export function useExecutionStats(
routeId?: string, routeId?: string,
application?: string, application?: string,
) { ) {
const refetchInterval = useRefreshInterval(10_000);
return useQuery({ return useQuery({
queryKey: ['executions', 'stats', timeFrom, timeTo, routeId, application], queryKey: ['executions', 'stats', timeFrom, timeTo, routeId, application],
queryFn: async () => { queryFn: async () => {
@@ -26,11 +28,12 @@ export function useExecutionStats(
}, },
enabled: !!timeFrom, enabled: !!timeFrom,
placeholderData: (prev) => prev, placeholderData: (prev) => prev,
refetchInterval: 10_000, refetchInterval,
}); });
} }
export function useSearchExecutions(filters: SearchRequest, live = false) { export function useSearchExecutions(filters: SearchRequest, live = false) {
const refetchInterval = useRefreshInterval(5_000);
return useQuery({ return useQuery({
queryKey: ['executions', 'search', filters], queryKey: ['executions', 'search', filters],
queryFn: async () => { queryFn: async () => {
@@ -41,7 +44,7 @@ export function useSearchExecutions(filters: SearchRequest, live = false) {
return data!; return data!;
}, },
placeholderData: (prev) => prev, placeholderData: (prev) => prev,
refetchInterval: live ? 5_000 : false, refetchInterval: live ? refetchInterval : false,
}); });
} }
@@ -51,6 +54,7 @@ export function useStatsTimeseries(
routeId?: string, routeId?: string,
application?: string, application?: string,
) { ) {
const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['executions', 'timeseries', timeFrom, timeTo, routeId, application], queryKey: ['executions', 'timeseries', timeFrom, timeTo, routeId, application],
queryFn: async () => { queryFn: async () => {
@@ -70,7 +74,7 @@ export function useStatsTimeseries(
}, },
enabled: !!timeFrom, enabled: !!timeFrom,
placeholderData: (prev) => prev, placeholderData: (prev) => prev,
refetchInterval: 30_000, refetchInterval,
}); });
} }

View File

@@ -1,8 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { config } from '../../config'; import { config } from '../../config';
import { useAuthStore } from '../../auth/auth-store'; import { useAuthStore } from '../../auth/auth-store';
import { useRefreshInterval } from './use-refresh-interval';
export function useProcessorMetrics(routeId: string | null, appId?: string) { export function useProcessorMetrics(routeId: string | null, appId?: string) {
const refetchInterval = useRefreshInterval(30_000);
return useQuery({ return useQuery({
queryKey: ['processor-metrics', routeId, appId], queryKey: ['processor-metrics', routeId, appId],
queryFn: async () => { queryFn: async () => {
@@ -20,6 +22,6 @@ export function useProcessorMetrics(routeId: string | null, appId?: string) {
return res.json(); return res.json();
}, },
enabled: !!routeId, enabled: !!routeId,
refetchInterval: 30_000, refetchInterval,
}); });
} }

View File

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