feat(ui): add useInfiniteApplicationLogs hook

Server-side filters on source/level/time-range, client-side text
search on top of flattened items. Leaves useApplicationLogs and
useStartupLogs untouched for bounded consumers (LogTab, StartupLogPanel).
This commit is contained in:
hsiegeln
2026-04-17 12:42:35 +02:00
parent c2ce508565
commit 43f145157d

View File

@@ -4,6 +4,7 @@ import { useAuthStore } from '../../auth/auth-store';
import { useRefreshInterval } from './use-refresh-interval'; import { useRefreshInterval } from './use-refresh-interval';
import { useGlobalFilters } from '@cameleer/design-system'; import { useGlobalFilters } from '@cameleer/design-system';
import { useEnvironmentStore } from '../environment-store'; import { useEnvironmentStore } from '../environment-store';
import { useInfiniteStream, type UseInfiniteStreamResult } from '../../hooks/useInfiniteStream';
export interface LogEntryResponse { export interface LogEntryResponse {
timestamp: string; timestamp: string;
@@ -157,3 +158,79 @@ export function useStartupLogs(
refetchInterval: isStarting ? 3_000 : false, 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<LogEntryResponse> {
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<LogEntryResponse>({
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 };
},
});
}