diff --git a/ui/src/api/queries/logs.ts b/ui/src/api/queries/logs.ts index 959dff8e..8ad1e391 100644 --- a/ui/src/api/queries/logs.ts +++ b/ui/src/api/queries/logs.ts @@ -4,6 +4,7 @@ import { useAuthStore } from '../../auth/auth-store'; import { useRefreshInterval } from './use-refresh-interval'; import { useGlobalFilters } from '@cameleer/design-system'; import { useEnvironmentStore } from '../environment-store'; +import { useInfiniteStream, type UseInfiniteStreamResult } from '../../hooks/useInfiniteStream'; export interface LogEntryResponse { timestamp: string; @@ -157,3 +158,79 @@ export function useStartupLogs( refetchInterval: isStarting ? 3_000 : false, }); } + +export interface UseInfiniteApplicationLogsArgs { + application?: string; + agentId?: string; + sources?: string[]; // multi-select, server-side OR + levels?: string[]; // multi-select, server-side OR + exchangeId?: string; + isAtTop: boolean; + pageSize?: number; +} + +/** + * Cursor-paginated log stream. Filters `sources`, `levels`, and the global + * time range are applied server-side. Free-text search is applied by the + * caller on top of the flattened items. + */ +export function useInfiniteApplicationLogs( + args: UseInfiniteApplicationLogsArgs, +): UseInfiniteStreamResult { + const { timeRange } = useGlobalFilters(); + const selectedEnv = useEnvironmentStore((s) => s.environment); + + const useTimeRange = !args.exchangeId; + const fromIso = useTimeRange ? timeRange.start.toISOString() : undefined; + const toIso = useTimeRange ? timeRange.end.toISOString() : undefined; + + const sortedSources = (args.sources ?? []).slice().sort(); + const sortedLevels = (args.levels ?? []).slice().sort(); + const sourcesParam = sortedSources.join(','); + const levelsParam = sortedLevels.join(','); + const pageSize = args.pageSize ?? 100; + + return useInfiniteStream({ + queryKey: [ + 'logs', 'infinite', + selectedEnv ?? '', + args.application ?? '', + args.agentId ?? '', + args.exchangeId ?? '', + sourcesParam, + levelsParam, + fromIso ?? '', + toIso ?? '', + pageSize, + ], + enabled: !!args.application && !!selectedEnv, + isAtTop: args.isAtTop, + fetchPage: async (cursor) => { + const token = useAuthStore.getState().accessToken; + const qp = new URLSearchParams(); + if (args.application) qp.set('application', args.application); + if (args.agentId) qp.set('agentId', args.agentId); + if (args.exchangeId) qp.set('exchangeId', args.exchangeId); + if (sourcesParam) qp.set('source', sourcesParam); + if (levelsParam) qp.set('level', levelsParam); + if (fromIso) qp.set('from', fromIso); + if (toIso) qp.set('to', toIso); + if (cursor) qp.set('cursor', cursor); + qp.set('limit', String(pageSize)); + qp.set('sort', 'desc'); + + const res = await fetch( + `${config.apiBaseUrl}/environments/${encodeURIComponent(selectedEnv ?? '')}/logs?${qp}`, + { + headers: { + Authorization: `Bearer ${token}`, + 'X-Cameleer-Protocol-Version': '1', + }, + }, + ); + if (!res.ok) throw new Error('Failed to load logs'); + const page: LogSearchPageResponse = await res.json(); + return { data: page.data, nextCursor: page.nextCursor, hasMore: page.hasMore }; + }, + }); +}