2026-03-28 13:51:35 +01:00
|
|
|
// ui/src/hooks/useScope.ts
|
|
|
|
|
import { useParams, useNavigate, useLocation } from 'react-router';
|
|
|
|
|
import { useCallback } from 'react';
|
|
|
|
|
|
feat: add Logs tab with cursor-paginated search, level filters, and live tail
- Extend GET /api/v1/logs with cursor pagination, multi-level filtering,
optional application scoping, and level count aggregation
- Add exchangeId, instanceId, application, mdc fields to log responses
- Refactor ClickHouseLogStore with keyset pagination (N+1 pattern)
- Add LogSearchRequest/LogSearchResponse core domain records
- Create LogSearchPageResponse wrapper DTO
- Add Logs as 4th content tab (Exchanges | Dashboard | Runtime | Logs)
- Implement LogSearch component with debounced search, level filter bar,
expandable log entries, cursor pagination, and live tail mode
- Add cross-navigation: exchange header → logs, log tab → logs tab
- Update ClickHouseLogStoreIT with cursor, multi-level, cross-app tests
Closes: #104
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 08:47:16 +02:00
|
|
|
export type TabKey = 'exchanges' | 'dashboard' | 'runtime' | 'logs';
|
2026-03-28 13:51:35 +01:00
|
|
|
|
feat: add Logs tab with cursor-paginated search, level filters, and live tail
- Extend GET /api/v1/logs with cursor pagination, multi-level filtering,
optional application scoping, and level count aggregation
- Add exchangeId, instanceId, application, mdc fields to log responses
- Refactor ClickHouseLogStore with keyset pagination (N+1 pattern)
- Add LogSearchRequest/LogSearchResponse core domain records
- Create LogSearchPageResponse wrapper DTO
- Add Logs as 4th content tab (Exchanges | Dashboard | Runtime | Logs)
- Implement LogSearch component with debounced search, level filter bar,
expandable log entries, cursor pagination, and live tail mode
- Add cross-navigation: exchange header → logs, log tab → logs tab
- Update ClickHouseLogStoreIT with cursor, multi-level, cross-app tests
Closes: #104
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 08:47:16 +02:00
|
|
|
const VALID_TABS = new Set<TabKey>(['exchanges', 'dashboard', 'runtime', 'logs']);
|
2026-03-28 13:51:35 +01:00
|
|
|
|
|
|
|
|
export interface Scope {
|
|
|
|
|
tab: TabKey;
|
|
|
|
|
appId?: string;
|
|
|
|
|
routeId?: string;
|
|
|
|
|
exchangeId?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useScope() {
|
|
|
|
|
const params = useParams<{ tab?: string; appId?: string; routeId?: string; exchangeId?: string }>();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
|
|
|
|
|
// Derive tab from first URL segment — fallback to 'exchanges'
|
|
|
|
|
const rawTab = location.pathname.split('/').filter(Boolean)[0] ?? 'exchanges';
|
|
|
|
|
const tab: TabKey = VALID_TABS.has(rawTab as TabKey) ? (rawTab as TabKey) : 'exchanges';
|
|
|
|
|
|
|
|
|
|
const scope: Scope = {
|
|
|
|
|
tab,
|
|
|
|
|
appId: params.appId,
|
|
|
|
|
routeId: params.routeId,
|
|
|
|
|
exchangeId: params.exchangeId,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const setTab = useCallback((newTab: TabKey) => {
|
|
|
|
|
const parts = ['', newTab];
|
|
|
|
|
if (scope.appId) parts.push(scope.appId);
|
|
|
|
|
if (scope.routeId) parts.push(scope.routeId);
|
|
|
|
|
navigate(parts.join('/'));
|
|
|
|
|
}, [navigate, scope.appId, scope.routeId]);
|
|
|
|
|
|
|
|
|
|
const setApp = useCallback((appId: string | undefined) => {
|
|
|
|
|
if (!appId) {
|
|
|
|
|
navigate(`/${tab}`);
|
|
|
|
|
} else {
|
|
|
|
|
navigate(`/${tab}/${appId}`);
|
|
|
|
|
}
|
|
|
|
|
}, [navigate, tab]);
|
|
|
|
|
|
|
|
|
|
const setRoute = useCallback((appId: string, routeId: string | undefined) => {
|
|
|
|
|
if (!routeId) {
|
|
|
|
|
navigate(`/${tab}/${appId}`);
|
|
|
|
|
} else {
|
|
|
|
|
navigate(`/${tab}/${appId}/${routeId}`);
|
|
|
|
|
}
|
|
|
|
|
}, [navigate, tab]);
|
|
|
|
|
|
|
|
|
|
const setExchange = useCallback((appId: string, routeId: string, exchangeId: string | undefined) => {
|
|
|
|
|
if (!exchangeId) {
|
|
|
|
|
navigate(`/${tab}/${appId}/${routeId}`);
|
|
|
|
|
} else {
|
|
|
|
|
navigate(`/${tab}/${appId}/${routeId}/${exchangeId}`);
|
|
|
|
|
}
|
|
|
|
|
}, [navigate, tab]);
|
|
|
|
|
|
|
|
|
|
const clearScope = useCallback(() => {
|
|
|
|
|
navigate(`/${tab}`);
|
|
|
|
|
}, [navigate, tab]);
|
|
|
|
|
|
|
|
|
|
return { scope, setTab, setApp, setRoute, setExchange, clearScope };
|
|
|
|
|
}
|