From 626501cb043fda5eaad1b6bca76ffe3c0ef0b024 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:32:55 +0100 Subject: [PATCH] feat(ui): add Log tab to diagram detail panel with exchange/processor filtering --- .../ExecutionDiagram/DetailPanel.tsx | 9 ++ .../ExecutionDiagram/tabs/LogTab.tsx | 117 ++++++++++++++++++ ui/src/components/ExecutionDiagram/types.ts | 2 +- 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/ExecutionDiagram/tabs/LogTab.tsx diff --git a/ui/src/components/ExecutionDiagram/DetailPanel.tsx b/ui/src/components/ExecutionDiagram/DetailPanel.tsx index 74c90411..d2b28c7e 100644 --- a/ui/src/components/ExecutionDiagram/DetailPanel.tsx +++ b/ui/src/components/ExecutionDiagram/DetailPanel.tsx @@ -7,6 +7,7 @@ import { BodyTab } from './tabs/BodyTab'; import { ErrorTab } from './tabs/ErrorTab'; import { ConfigTab } from './tabs/ConfigTab'; import { TimelineTab } from './tabs/TimelineTab'; +import { LogTab } from './tabs/LogTab'; import styles from './ExecutionDiagram.module.css'; interface DetailPanelProps { @@ -24,6 +25,7 @@ const TABS: { key: DetailTab; label: string }[] = [ { key: 'error', label: 'Error' }, { key: 'config', label: 'Config' }, { key: 'timeline', label: 'Timeline' }, + { key: 'log', label: 'Log' }, ]; function formatDuration(ms: number | undefined): string { @@ -158,6 +160,13 @@ export function DetailPanel({ onSelectProcessor={onSelectProcessor} /> )} + {activeTab === 'log' && ( + + )} ); diff --git a/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx b/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx new file mode 100644 index 00000000..6495c540 --- /dev/null +++ b/ui/src/components/ExecutionDiagram/tabs/LogTab.tsx @@ -0,0 +1,117 @@ +import { useState, useMemo } from 'react'; +import { useApplicationLogs } from '../../../api/queries/logs'; +import type { LogEntryResponse } from '../../../api/queries/logs'; +import styles from '../ExecutionDiagram.module.css'; + +interface LogTabProps { + applicationName: string; + exchangeId?: string; + processorId: string | null; +} + +function levelColor(level: string): string { + switch (level?.toUpperCase()) { + case 'ERROR': return 'var(--error)'; + case 'WARN': return 'var(--warning)'; + case 'DEBUG': return 'var(--text-muted)'; + case 'TRACE': return 'var(--text-faint, var(--text-muted))'; + default: return 'var(--text-secondary)'; + } +} + +function formatTime(iso: string): string { + const d = new Date(iso); + const h = String(d.getHours()).padStart(2, '0'); + const m = String(d.getMinutes()).padStart(2, '0'); + const s = String(d.getSeconds()).padStart(2, '0'); + const ms = String(d.getMilliseconds()).padStart(3, '0'); + return `${h}:${m}:${s}.${ms}`; +} + +export function LogTab({ applicationName, exchangeId, processorId }: LogTabProps) { + const [filter, setFilter] = useState(''); + + const { data: logs, isLoading } = useApplicationLogs( + applicationName, + undefined, + { exchangeId, limit: 500 }, + ); + + const entries: LogEntryResponse[] = useMemo(() => { + if (!logs) return []; + let items = logs as LogEntryResponse[]; + + // If a processor is selected, filter logs by logger name containing the processor ID + if (processorId) { + items = items.filter((e) => + e.message?.includes(processorId) || + e.loggerName?.includes(processorId) + ); + } + + // Text filter + if (filter) { + const q = filter.toLowerCase(); + items = items.filter((e) => + e.message?.toLowerCase().includes(q) || + e.level?.toLowerCase().includes(q) || + e.loggerName?.toLowerCase().includes(q) + ); + } + + return items; + }, [logs, processorId, filter]); + + if (isLoading) { + return
Loading logs...
; + } + + return ( +
+
+ setFilter(e.target.value)} + style={{ + width: '100%', + padding: '4px 8px', + fontSize: '11px', + border: '1px solid var(--border-subtle)', + borderRadius: 'var(--radius-sm)', + background: 'var(--bg-surface)', + color: 'var(--text-primary)', + outline: 'none', + fontFamily: 'var(--font-body)', + }} + /> +
+
+ {entries.length === 0 ? ( +
+ {processorId ? 'No logs for this processor' : 'No logs available'} +
+ ) : ( + + + {entries.map((entry, i) => ( + + + + + + ))} + +
+ {formatTime(entry.timestamp)} + + {entry.level} + + {entry.message} +
+ )} +
+
+ ); +} diff --git a/ui/src/components/ExecutionDiagram/types.ts b/ui/src/components/ExecutionDiagram/types.ts index 5837fbd3..7cb52f74 100644 --- a/ui/src/components/ExecutionDiagram/types.ts +++ b/ui/src/components/ExecutionDiagram/types.ts @@ -21,4 +21,4 @@ export interface IterationInfo { type: 'loop' | 'split' | 'multicast'; } -export type DetailTab = 'info' | 'headers' | 'input' | 'output' | 'error' | 'config' | 'timeline'; +export type DetailTab = 'info' | 'headers' | 'input' | 'output' | 'error' | 'config' | 'timeline' | 'log';