diff --git a/ui/src/pages/Admin/AppConfigPage.module.css b/ui/src/pages/Admin/AppConfigPage.module.css deleted file mode 100644 index 339aa5e8..00000000 --- a/ui/src/pages/Admin/AppConfigPage.module.css +++ /dev/null @@ -1,143 +0,0 @@ -.widePanel { - width: 520px !important; -} - -.panelSection { - margin-bottom: 16px; -} - -.panelSectionHeader { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); - margin-bottom: 8px; -} - -.settingsGrid { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 12px; -} - -.field { - display: flex; - flex-direction: column; - gap: 3px; -} - -.fieldLabel { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); -} - -.select { - padding: 4px 8px; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - background: var(--bg-body); - color: var(--text-primary); - font-size: 12px; - font-family: var(--font-mono); - outline: none; -} - -.select:focus { - border-color: var(--amber); -} - -.toggleRow { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text-secondary); -} - -.sectionSummary { - font-size: 12px; - color: var(--text-muted); - margin-bottom: 8px; - display: block; -} - -.tapBadges { - display: flex; - gap: 4px; - flex-wrap: wrap; -} - -.hint { - color: var(--text-faint); - font-size: 12px; -} - -.routeLabel { - font-size: 12px; - color: var(--text-secondary); -} - -.tapBadgeLink { - background: none; - border: none; - padding: 0; - cursor: pointer; - border-radius: var(--radius-sm); - transition: opacity 0.15s; -} - -.tapBadgeLink:hover { - opacity: 0.75; -} - -.removeBtn { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - font-size: 16px; - padding: 0 4px; - line-height: 1; -} - -.removeBtn:hover { - color: var(--error); -} - -.panelToolbar { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; -} - -.panelMeta { - font-size: 12px; - color: var(--text-muted); -} - -.panelActions { - display: flex; - gap: 8px; - align-items: center; -} - -.editBtn { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - font-size: 14px; - padding: 4px 6px; - border-radius: var(--radius-sm); - transition: color 0.15s; -} - -.editBtn:hover { - color: var(--text-primary); -} - diff --git a/ui/src/pages/Admin/AppConfigPage.tsx b/ui/src/pages/Admin/AppConfigPage.tsx deleted file mode 100644 index 1bfe2a01..00000000 --- a/ui/src/pages/Admin/AppConfigPage.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { useState, useMemo, useEffect } from 'react'; -import { useNavigate, useSearchParams, useParams } from 'react-router'; -import { Pencil, X } from 'lucide-react'; -import { - DataTable, Badge, MonoText, DetailPanel, SectionHeader, Button, Toggle, Spinner, useToast, -} from '@cameleer/design-system'; -import type { Column } from '@cameleer/design-system'; -import { useAllApplicationConfigs, useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands'; -import type { ApplicationConfig, TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands'; -import { useRouteCatalog } from '../../api/queries/catalog'; -import { useCanControl } from '../../auth/auth-store'; -import type { AppCatalogEntry, RouteSummary } from '../../api/types'; -import styles from './AppConfigPage.module.css'; - -type ConfigRow = ApplicationConfig & { id: string }; -type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'; - -interface TracedTapRow { id: string; processorId: string; captureMode: string | null; taps: TapDefinition[]; } -interface RouteRecordingRow { id: string; routeId: string; recording: boolean; } - -function timeAgo(iso?: string): string { - if (!iso) return '\u2014'; - const diff = Date.now() - new Date(iso).getTime(); - const secs = Math.floor(diff / 1000); - if (secs < 60) return `${secs}s ago`; - const mins = Math.floor(secs / 60); - if (mins < 60) return `${mins}m ago`; - const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; -} - -function logLevelColor(level?: string): BadgeColor { - switch (level?.toUpperCase()) { - case 'ERROR': return 'error'; case 'WARN': return 'warning'; case 'DEBUG': return 'running'; case 'TRACE': return 'auto'; default: return 'success'; - } -} -function engineLevelColor(level?: string): BadgeColor { - switch (level?.toUpperCase()) { - case 'NONE': return 'error'; case 'MINIMAL': return 'warning'; case 'COMPLETE': return 'running'; default: return 'success'; - } -} -function payloadColor(mode?: string): BadgeColor { - switch (mode?.toUpperCase()) { - case 'INPUT': case 'OUTPUT': return 'warning'; case 'BOTH': return 'running'; default: return 'auto'; - } -} -function captureColor(mode: string): BadgeColor { - switch (mode?.toUpperCase()) { - case 'INPUT': case 'OUTPUT': return 'warning'; case 'BOTH': return 'running'; default: return 'auto'; - } -} - -// ── Table columns (overview) ───────────────────────────────────────────────── - -function buildColumns(): Column[] { - return [ - { key: 'application', header: 'Application', sortable: true, render: (_v, row) => {row.application} }, - { key: 'applicationLogLevel', header: 'App Log', render: (_v, row) => { const val = row.applicationLogLevel ?? 'INFO'; return ; } }, - { key: 'agentLogLevel', header: 'Agent Log', render: (_v, row) => { const val = row.agentLogLevel ?? 'INFO'; return ; } }, - { key: 'engineLevel', header: 'Engine Level', render: (_v, row) => { const val = row.engineLevel ?? 'REGULAR'; return ; } }, - { key: 'payloadCaptureMode', header: 'Payload Capture', render: (_v, row) => { const val = row.payloadCaptureMode ?? 'NONE'; return ; } }, - { key: 'metricsEnabled', header: 'Metrics', width: '80px', render: (_v, row) => }, - { key: 'tracedProcessors', header: 'Traced', width: '70px', render: (_v, row) => { const c = row.tracedProcessors ? Object.keys(row.tracedProcessors).length : 0; return c > 0 ? : 0; } }, - { key: 'taps', header: 'Taps', width: '70px', render: (_v, row) => { const t = row.taps?.length ?? 0; const e = row.taps?.filter(x => x.enabled).length ?? 0; return t === 0 ? 0 : ; } }, - { key: 'version', header: 'v', width: '40px', render: (_v, row) => {row.version} }, - { key: 'updatedAt', header: 'Updated', render: (_v, row) => {timeAgo(row.updatedAt)} }, - ]; -} - -// ── Detail Panel Content ───────────────────────────────────────────────────── - -function AppConfigDetail({ appId, onClose }: { appId: string; onClose: () => void }) { - const { toast } = useToast(); - const navigate = useNavigate(); - const canEdit = useCanControl(); - const { data: config, isLoading } = useApplicationConfig(appId); - const updateConfig = useUpdateApplicationConfig(); - const { data: catalog } = useRouteCatalog(); - - const [editing, setEditing] = useState(false); - const [form, setForm] = useState | null>(null); - const [tracedDraft, setTracedDraft] = useState>({}); - const [routeRecordingDraft, setRouteRecordingDraft] = useState>({}); - - const appRoutes: RouteSummary[] = useMemo(() => { - if (!catalog || !appId) return []; - const entry = (catalog as AppCatalogEntry[]).find((e) => e.appId === appId); - return entry?.routes ?? []; - }, [catalog, appId]); - - // processorId → routeId mapping from backend - const { data: processorToRoute = {} } = useProcessorRouteMapping(appId); - - useEffect(() => { - if (config) { - setForm({ - applicationLogLevel: config.applicationLogLevel ?? 'INFO', - agentLogLevel: config.agentLogLevel ?? 'INFO', - engineLevel: config.engineLevel ?? 'REGULAR', - payloadCaptureMode: config.payloadCaptureMode ?? 'NONE', - metricsEnabled: config.metricsEnabled, - samplingRate: config.samplingRate, - compressSuccess: config.compressSuccess, - }); - setTracedDraft({ ...config.tracedProcessors }); - setRouteRecordingDraft({ ...config.routeRecording }); - } - }, [config]); - - function startEditing() { - if (!config) return; - setForm({ - applicationLogLevel: config.applicationLogLevel ?? 'INFO', - agentLogLevel: config.agentLogLevel ?? 'INFO', - engineLevel: config.engineLevel ?? 'REGULAR', - payloadCaptureMode: config.payloadCaptureMode ?? 'NONE', - metricsEnabled: config.metricsEnabled, - samplingRate: config.samplingRate, - compressSuccess: config.compressSuccess, - }); - setTracedDraft({ ...config.tracedProcessors }); - setRouteRecordingDraft({ ...config.routeRecording }); - setEditing(true); - } - - function updateField(key: K, value: ApplicationConfig[K]) { - setForm((prev) => prev ? { ...prev, [key]: value } : prev); - } - - function updateTracedProcessor(processorId: string, mode: string) { - setTracedDraft((prev) => { - if (mode === 'REMOVE') { const next = { ...prev }; delete next[processorId]; return next; } - return { ...prev, [processorId]: mode }; - }); - } - - function updateRouteRecording(routeId: string, recording: boolean) { - setRouteRecordingDraft((prev) => ({ ...prev, [routeId]: recording })); - } - - function handleSave() { - if (!config || !form) return; - const updated = { ...config, ...form, tracedProcessors: tracedDraft, routeRecording: routeRecordingDraft } as ApplicationConfig; - updateConfig.mutate(updated, { - onSuccess: (saved: ConfigUpdateResponse) => { - setEditing(false); - if (saved.pushResult.success) { - toast({ title: 'Config saved', description: `${appId} updated to v${saved.config.version} — pushed to ${saved.pushResult.total}/${saved.pushResult.total} agents`, variant: 'success' }); - } else { - const failed = [...saved.pushResult.responses.filter(r => r.status !== 'SUCCESS').map(r => r.agentId), ...saved.pushResult.timedOut]; - toast({ title: 'Config saved — partial push failure', description: `${saved.pushResult.responded}/${saved.pushResult.total} responded. Failed: ${failed.join(', ')}`, variant: 'warning', duration: 86_400_000 }); - } - }, - onError: () => { toast({ title: 'Save failed', description: 'Could not update configuration', variant: 'error', duration: 86_400_000 }); }, - }); - } - - function navigateToTaps(processorId: string) { - const routeId = processorToRoute[processorId]; - onClose(); - if (routeId) { - navigate(`/routes/${appId}/${routeId}?tab=taps`); - } else { - navigate(`/routes/${appId}`); - } - } - - // ── Traces & Taps merged rows - const tracedTapRows: TracedTapRow[] = useMemo(() => { - const traced = editing ? tracedDraft : (config?.tracedProcessors ?? {}); - const taps = config?.taps ?? []; - const pids = new Set([...Object.keys(traced), ...taps.map(t => t.processorId)]); - return Array.from(pids).sort().map(pid => ({ id: pid, processorId: pid, captureMode: traced[pid] ?? null, taps: taps.filter(t => t.processorId === pid) })); - }, [editing, tracedDraft, config?.tracedProcessors, config?.taps]); - - const tracedCount = useMemo(() => Object.keys(editing ? tracedDraft : (config?.tracedProcessors ?? {})).length, [editing, tracedDraft, config?.tracedProcessors]); - const tapCount = config?.taps?.length ?? 0; - - const tracedTapColumns: Column[] = useMemo(() => [ - { key: 'route' as any, header: 'Route', render: (_v, row) => { - const routeId = processorToRoute[row.processorId]; - return routeId ? {routeId} : ; - }}, - { key: 'processorId', header: 'Processor', render: (_v, row) => {row.processorId} }, - { - key: 'captureMode', header: 'Capture', - render: (_v, row) => { - if (row.captureMode === null) return ; - if (editing) return ( - - ); - return ; - }, - }, - { - key: 'taps', header: 'Taps', - render: (_v, row) => row.taps.length === 0 - ? - :
{row.taps.map(t => ( - - ))}
, - }, - ...(editing ? [{ - key: '_remove' as const, header: '', width: '36px', - render: (_v: unknown, row: TracedTapRow) => row.captureMode === null ? null : ( - - ), - }] : []), - ], [editing, processorToRoute]); - - // ── Route Recording rows - const routeRecordingRows: RouteRecordingRow[] = useMemo(() => { - const rec = editing ? routeRecordingDraft : (config?.routeRecording ?? {}); - return appRoutes.map(r => ({ id: r.routeId, routeId: r.routeId, recording: rec[r.routeId] !== false })); - }, [editing, routeRecordingDraft, config?.routeRecording, appRoutes]); - - const recordingCount = routeRecordingRows.filter(r => r.recording).length; - - const routeRecordingColumns: Column[] = useMemo(() => [ - { key: 'routeId', header: 'Route', render: (_v, row) => {row.routeId} }, - { key: 'recording', header: 'Recording', width: '100px', render: (_v, row) => { if (editing) updateRouteRecording(row.routeId, !row.recording); }} disabled={!editing} /> }, - ], [editing, routeRecordingDraft]); - - if (isLoading) return
; - if (!config || !form) return
No configuration found.
; - - return ( - <> -
-
- Version {config.version} - {config.updatedAt && <> · Updated {timeAgo(config.updatedAt)}} -
- {editing ? ( -
- - -
- ) : canEdit ? ( - - ) : null} -
- - {/* Settings */} -
-
Settings
-
-
- App Log Level - {editing - ? - : } -
-
- Agent Log Level - {editing - ? - : } -
-
- Engine Level - {editing - ? - : } -
-
- Payload Capture - {editing - ? - : } -
-
- Metrics - {editing - ? updateField('metricsEnabled', (e.target as HTMLInputElement).checked)} /> - : } -
-
- Compress Success - {editing - ? updateField('compressSuccess', (e.target as HTMLInputElement).checked)} /> - : } -
-
- Sampling Rate - {editing - ? updateField('samplingRate', parseFloat(e.target.value) || 0)} /> - : {form.samplingRate}} -
-
-
- - {/* Traces & Taps */} -
-
Traces & Taps
- {tracedCount} traced · {tapCount} taps · click tap to manage - {tracedTapRows.length > 0 - ? columns={tracedTapColumns} data={tracedTapRows} pageSize={20} flush /> - : No processor traces or taps configured.} -
- - {/* Route Recording */} -
-
Route Recording
- {recordingCount} of {routeRecordingRows.length} routes recording - {routeRecordingRows.length > 0 - ? columns={routeRecordingColumns} data={routeRecordingRows} pageSize={20} flush /> - : No routes found for this application.} -
- - ); -} - -// ── Main Page ──────────────────────────────────────────────────────────────── - -export default function AppConfigPage() { - const { appId: routeAppId } = useParams<{ appId?: string }>(); - const { data: configs } = useAllApplicationConfigs(); - const [searchParams, setSearchParams] = useSearchParams(); - const [selectedApp, setSelectedApp] = useState(routeAppId ?? null); - const columns = useMemo(buildColumns, []); - - // Sync from route param when it changes (sidebar navigation) - useEffect(() => { - if (routeAppId) setSelectedApp(routeAppId); - }, [routeAppId]); - - // Auto-select app from query param (e.g., ?app=caller-app) - useEffect(() => { - const appParam = searchParams.get('app'); - if (appParam && !selectedApp) { - setSelectedApp(appParam); - searchParams.delete('app'); - searchParams.delete('processor'); - setSearchParams(searchParams, { replace: true }); - } - }, [searchParams]); // eslint-disable-line react-hooks/exhaustive-deps - - return ( -
- - columns={columns} - data={(configs ?? []).map(c => ({ ...c, id: c.application }))} - onRowClick={(row) => setSelectedApp(row.application)} - selectedId={selectedApp ?? undefined} - pageSize={50} - /> - setSelectedApp(null)} - title={selectedApp ?? ''} - className={styles.widePanel} - > - {selectedApp && setSelectedApp(null)} />} - -
- ); -} diff --git a/ui/src/pages/LogsTab/LevelFilterBar.module.css b/ui/src/pages/LogsTab/LevelFilterBar.module.css deleted file mode 100644 index cca18bb3..00000000 --- a/ui/src/pages/LogsTab/LevelFilterBar.module.css +++ /dev/null @@ -1,14 +0,0 @@ -.wrapper { - display: flex; - align-items: center; - gap: 8px; -} - -.clearButton { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - font-size: 12px; - font-family: var(--font-body); -} diff --git a/ui/src/pages/LogsTab/LevelFilterBar.tsx b/ui/src/pages/LogsTab/LevelFilterBar.tsx deleted file mode 100644 index 0cacae9c..00000000 --- a/ui/src/pages/LogsTab/LevelFilterBar.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { ButtonGroup } from '@cameleer/design-system'; -import type { ButtonGroupItem } from '@cameleer/design-system'; -import styles from './LevelFilterBar.module.css'; - -function formatCount(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; - return String(n); -} - -const LEVEL_ITEMS: ButtonGroupItem[] = [ - { value: 'TRACE', label: 'Trace', color: 'var(--text-muted)' }, - { value: 'DEBUG', label: 'Debug', color: 'var(--running)' }, - { value: 'INFO', label: 'Info', color: 'var(--success)' }, - { value: 'WARN', label: 'Warn', color: 'var(--warning)' }, - { value: 'ERROR', label: 'Error', color: 'var(--error)' }, -]; - -interface LevelFilterBarProps { - activeLevels: Set; - onChange: (levels: Set) => void; - levelCounts: Record; -} - -export function LevelFilterBar({ activeLevels, onChange, levelCounts }: LevelFilterBarProps) { - const items = LEVEL_ITEMS.map((item) => ({ - ...item, - label: `${item.label} ${formatCount(levelCounts[item.value] ?? 0)}`, - })); - - return ( -
- - {activeLevels.size > 0 && ( - - )} -
- ); -} diff --git a/ui/src/pages/LogsTab/LogEntry.module.css b/ui/src/pages/LogsTab/LogEntry.module.css deleted file mode 100644 index 8dabda17..00000000 --- a/ui/src/pages/LogsTab/LogEntry.module.css +++ /dev/null @@ -1,194 +0,0 @@ -.entry { - border-bottom: 1px solid var(--border-subtle); - cursor: pointer; - transition: background 0.1s; -} - -.entry:hover { - background: var(--bg-hover); -} - -.expanded { - background: var(--bg-surface); -} - -.row { - display: flex; - align-items: baseline; - gap: 8px; - padding: 6px 12px; - font-size: 12px; - font-family: var(--font-mono); - min-height: 28px; -} - -.timestamp { - color: var(--text-muted); - white-space: nowrap; - flex-shrink: 0; -} - -.level { - font-weight: 600; - white-space: nowrap; - flex-shrink: 0; - min-width: 40px; -} - -.logger { - color: var(--text-muted); - white-space: nowrap; - flex-shrink: 0; - max-width: 180px; - overflow: hidden; - text-overflow: ellipsis; - font-size: 12px; -} - -.message { - color: var(--text-primary); - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.chips { - display: flex; - gap: 4px; - flex-shrink: 0; -} - -.chip { - font-size: 12px; - padding: 1px 6px; - border-radius: var(--radius-sm); - background: var(--bg-raised); - color: var(--text-secondary); - cursor: pointer; - font-family: var(--font-body); -} - -.chip:hover { - background: var(--border); -} - -.detail { - padding: 8px 12px 12px 60px; - font-size: 12px; -} - -.detailGrid { - display: grid; - grid-template-columns: 70px 1fr; - gap: 2px 8px; - margin-bottom: 8px; -} - -.detailLabel { - color: var(--text-muted); - font-size: 12px; -} - -.detailValue { - color: var(--text-primary); - font-family: var(--font-mono); - font-size: 12px; - word-break: break-all; -} - -.fullMessage { - color: var(--text-primary); - font-family: var(--font-mono); - font-size: 12px; - white-space: pre-wrap; - word-break: break-word; - margin-bottom: 8px; - padding: 8px; - background: var(--bg-deep); - border-radius: var(--radius-sm); -} - -.stackTrace { - font-family: var(--font-mono); - font-size: 12px; - color: var(--error); - background: var(--bg-deep); - border-radius: var(--radius-sm); - padding: 8px; - margin: 8px 0; - overflow-x: auto; - white-space: pre; - max-height: 300px; - overflow-y: auto; -} - -.mdcSection { - margin-top: 8px; -} - -.mdcGrid { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 4px; -} - -.mdcEntry { - display: flex; - gap: 2px; - font-size: 12px; - font-family: var(--font-mono); - background: var(--bg-deep); - border-radius: var(--radius-sm); - padding: 2px 6px; -} - -.mdcKey { - color: var(--text-muted); -} - -.mdcValue { - color: var(--text-primary); -} - -.actions { - display: flex; - gap: 8px; - margin-top: 8px; -} - -.actionBtn { - background: none; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: 4px 10px; - font-size: 12px; - color: var(--text-secondary); - cursor: pointer; - font-family: var(--font-body); -} - -.actionBtn:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.highlight { - background: var(--amber); - color: var(--bg-deep); - border-radius: 2px; - padding: 0 1px; -} - -.linkBtn { - background: none; - border: none; - padding: 0; - color: var(--amber); - cursor: pointer; - font-family: var(--font-mono); - font-size: 12px; - text-decoration: underline; -} diff --git a/ui/src/pages/LogsTab/LogEntry.tsx b/ui/src/pages/LogsTab/LogEntry.tsx deleted file mode 100644 index e7232066..00000000 --- a/ui/src/pages/LogsTab/LogEntry.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useState, useCallback } from 'react'; -import { useNavigate } from 'react-router'; -import { Badge } from '@cameleer/design-system'; -import type { LogEntryResponse } from '../../api/queries/logs'; -import { attributeBadgeColor } from '../../utils/attribute-color'; -import styles from './LogEntry.module.css'; - -function levelColor(level: string): string { - switch (level?.toUpperCase()) { - case 'ERROR': return 'var(--error)'; - case 'WARN': return 'var(--warning)'; - case 'INFO': return 'var(--success)'; - case 'DEBUG': return 'var(--running)'; - case 'TRACE': return '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}`; -} - -function abbreviateLogger(name: string | null): string { - if (!name) return ''; - const parts = name.split('.'); - if (parts.length <= 2) return name; - return parts.slice(0, -1).map((p) => p[0]).join('.') + '.' + parts[parts.length - 1]; -} - -function truncate(text: string, max: number): string { - return text.length > max ? text.slice(0, max) + '\u2026' : text; -} - -function highlightText(text: string, query: string | undefined): React.ReactNode { - if (!query || !text) return text; - const idx = text.toLowerCase().indexOf(query.toLowerCase()); - if (idx === -1) return text; - return ( - <> - {text.slice(0, idx)} - {text.slice(idx, idx + query.length)} - {highlightText(text.slice(idx + query.length), query)} - - ); -} - -interface LogEntryProps { - entry: LogEntryResponse; - query?: string; -} - -export function LogEntry({ entry, query }: LogEntryProps) { - const [expanded, setExpanded] = useState(false); - const navigate = useNavigate(); - - const hasStack = !!entry.stackTrace; - const hasExchange = !!entry.exchangeId; - - const handleViewExchange = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - if (!entry.exchangeId || !entry.application) return; - const routeId = entry.mdc?.['camel.routeId'] || '_'; - navigate(`/exchanges/${entry.application}/${routeId}/${entry.exchangeId}`); - }, [entry, navigate]); - - const handleCopyMessage = useCallback(async (e: React.MouseEvent) => { - e.stopPropagation(); - await navigator.clipboard.writeText(entry.message); - }, [entry.message]); - - return ( -
setExpanded(!expanded)}> -
- {formatTime(entry.timestamp)} - {entry.level} - {entry.application && } - - {abbreviateLogger(entry.loggerName)} - - {highlightText(truncate(entry.message, 200), query)} - - {hasStack && Stack} - {hasExchange && ( - Exchange - )} - -
- - {expanded && ( -
-
- Logger - {entry.loggerName} - Thread - {entry.threadName} - Instance - {entry.instanceId} - {hasExchange && ( - <> - Exchange - - - - - )} -
- -
{highlightText(entry.message, query)}
- - {hasStack && ( -
{highlightText(entry.stackTrace!, query)}
- )} - - {entry.mdc && Object.keys(entry.mdc).length > 0 && ( -
- MDC -
- {Object.entries(entry.mdc).map(([k, v]) => ( -
- {k} - {v} -
- ))} -
-
- )} - -
- {hasExchange && ( - - )} - -
-
- )} -
- ); -} diff --git a/ui/src/pages/LogsTab/LogSearch.module.css b/ui/src/pages/LogsTab/LogSearch.module.css deleted file mode 100644 index 454de827..00000000 --- a/ui/src/pages/LogsTab/LogSearch.module.css +++ /dev/null @@ -1,156 +0,0 @@ -.container { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - background: var(--bg-body); -} - -.toolbar { - padding: 12px 16px; - display: flex; - flex-direction: column; - gap: 8px; - border-bottom: 1px solid var(--border-subtle); - background: var(--bg-surface); -} - -.searchRow { - display: flex; - gap: 8px; - align-items: center; -} - -.searchInput { - flex: 1; - padding: 6px 10px; - font-size: 13px; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); - background: var(--bg-deep); - color: var(--text-primary); - outline: none; - font-family: var(--font-mono); -} - -.searchInput:focus { - border-color: var(--amber); -} - -.searchInput::placeholder { - color: var(--text-muted); -} - -.liveTailBtn { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - font-size: 12px; - font-weight: 500; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); - background: var(--bg-deep); - color: var(--text-secondary); - cursor: pointer; - font-family: var(--font-body); - white-space: nowrap; -} - -.liveTailBtn:hover { - border-color: var(--border); -} - -.liveTailActive { - border-color: var(--success); - color: var(--success); - background: var(--bg-surface); -} - -.liveDot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--success); - animation: pulse 1.5s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} - -.results { - flex: 1; - overflow-y: auto; - position: relative; -} - -.loadingWrap { - display: flex; - justify-content: center; - padding: 3rem; -} - -.loadMore { - display: flex; - justify-content: center; - padding: 12px; -} - -.loadMoreBtn { - padding: 6px 20px; - font-size: 12px; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); - background: var(--bg-surface); - color: var(--text-secondary); - cursor: pointer; - font-family: var(--font-body); -} - -.loadMoreBtn:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.loadMoreBtn:disabled { - opacity: 0.5; - cursor: default; -} - -.newEntries { - position: sticky; - bottom: 0; - text-align: center; - padding: 8px; - background: var(--amber); - color: var(--bg-deep); - font-size: 12px; - cursor: pointer; - font-weight: 500; -} - -.statusBar { - display: flex; - align-items: center; - gap: 12px; - padding: 4px 16px; - font-size: 12px; - color: var(--text-muted); - border-top: 1px solid var(--border-subtle); - background: var(--bg-surface); - font-family: var(--font-mono); -} - -.fetchDot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--amber); - animation: pulse 1s ease-in-out infinite; -} - -.scope { - margin-left: auto; -} diff --git a/ui/src/pages/LogsTab/LogSearch.tsx b/ui/src/pages/LogsTab/LogSearch.tsx deleted file mode 100644 index 50b6cc4e..00000000 --- a/ui/src/pages/LogsTab/LogSearch.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; -import { useSearchParams } from 'react-router'; -import { Spinner, EmptyState, useGlobalFilters } from '@cameleer/design-system'; -import { useLogs } from '../../api/queries/logs'; -import { useRefreshInterval } from '../../api/queries/use-refresh-interval'; -import { LevelFilterBar } from './LevelFilterBar'; -import { LogEntry } from './LogEntry'; -import styles from './LogSearch.module.css'; - -interface LogSearchProps { - defaultApplication?: string; - defaultRouteId?: string; -} - -export function LogSearch({ defaultApplication, defaultRouteId }: LogSearchProps) { - const [searchParams] = useSearchParams(); - const { timeRange } = useGlobalFilters(); - - // Initialize from URL params (for cross-navigation) - const urlExchangeId = searchParams.get('exchangeId') ?? undefined; - const urlQ = searchParams.get('q') ?? undefined; - - const [query, setQuery] = useState(urlQ ?? ''); - const [debouncedQuery, setDebouncedQuery] = useState(urlQ ?? ''); - const [activeLevels, setActiveLevels] = useState>(new Set()); - const [liveTail, setLiveTail] = useState(false); - const [cursor, setCursor] = useState(undefined); - const [allEntries, setAllEntries] = useState([]); - - const liveTailRef = useRef(liveTail); - liveTailRef.current = liveTail; - - // Debounce search query - const debounceTimer = useRef>(undefined); - const handleQueryChange = useCallback((value: string) => { - setQuery(value); - if (debounceTimer.current) clearTimeout(debounceTimer.current); - debounceTimer.current = setTimeout(() => { - setDebouncedQuery(value); - setCursor(undefined); - setAllEntries([]); - }, 300); - }, []); - - // Reset pagination when filters change - const handleLevelChange = useCallback((levels: Set) => { - setActiveLevels(levels); - setCursor(undefined); - setAllEntries([]); - }, []); - - const levelCsv = useMemo(() => - activeLevels.size > 0 ? [...activeLevels].join(',') : undefined, - [activeLevels]); - - // Build search params - const latestTsRef = useRef(undefined); - const liveRefetch = useRefreshInterval(2_000); - - const searchParamsObj = useMemo(() => ({ - q: debouncedQuery || undefined, - level: levelCsv, - application: defaultApplication, - exchangeId: urlExchangeId, - from: liveTail - ? (latestTsRef.current ?? timeRange.start.toISOString()) - : timeRange.start.toISOString(), - to: liveTail ? new Date().toISOString() : timeRange.end.toISOString(), - cursor: liveTail ? undefined : cursor, - limit: liveTail ? 200 : 100, - sort: liveTail ? 'asc' as const : 'desc' as const, - }), [debouncedQuery, levelCsv, defaultApplication, urlExchangeId, - timeRange, cursor, liveTail]); - - const { data, isLoading, isFetching } = useLogs(searchParamsObj, { - refetchInterval: liveTail ? liveRefetch : undefined, - }); - - // Live tail: append new entries - useEffect(() => { - if (!data || !liveTail) return; - if (data.data.length > 0) { - setAllEntries((prev) => { - const combined = [...prev, ...data.data]; - // Buffer limit: keep last 5000 - return combined.length > 5000 ? combined.slice(-5000) : combined; - }); - latestTsRef.current = data.data[data.data.length - 1].timestamp; - } - }, [data, liveTail]); - - // Auto-scroll for live tail - const scrollRef = useRef(null); - const [autoScroll, setAutoScroll] = useState(true); - - useEffect(() => { - if (liveTail && autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [allEntries, liveTail, autoScroll]); - - const handleScroll = useCallback(() => { - if (!scrollRef.current || !liveTail) return; - const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; - setAutoScroll(scrollHeight - scrollTop - clientHeight < 50); - }, [liveTail]); - - const handleToggleLiveTail = useCallback(() => { - setLiveTail((prev) => { - if (!prev) { - // Entering live tail - setAllEntries([]); - setCursor(undefined); - latestTsRef.current = undefined; - setAutoScroll(true); - } - return !prev; - }); - }, []); - - const handleLoadMore = useCallback(() => { - if (data?.nextCursor) { - setCursor(data.nextCursor); - } - }, [data?.nextCursor]); - - // Accumulate pages for non-live mode - useEffect(() => { - if (liveTail || !data) return; - if (cursor) { - // Appending a new page - setAllEntries((prev) => [...prev, ...data.data]); - } else { - // Fresh search - setAllEntries(data.data); - } - }, [data, cursor, liveTail]); - - const entries = liveTail ? allEntries : allEntries; - const levelCounts = data?.levelCounts ?? {}; - const hasMore = data?.hasMore ?? false; - const newEntriesCount = liveTail && !autoScroll && data?.data.length - ? data.data.length : 0; - - return ( -
-
-
- handleQueryChange(e.target.value)} - className={styles.searchInput} - /> - -
- -
- -
- {isLoading && entries.length === 0 ? ( -
- -
- ) : entries.length === 0 ? ( - 0 - ? 'Try adjusting your search or filters.' - : 'No log entries in the selected time range.'} - /> - ) : ( - <> - {entries.map((entry, i) => ( - - ))} - {!liveTail && hasMore && ( -
- -
- )} - - )} - - {liveTail && !autoScroll && newEntriesCount > 0 && ( -
setAutoScroll(true)}> - New entries arriving — click to scroll to bottom -
- )} -
- -
- {entries.length} entries{liveTail ? ' (live)' : ''} - {isFetching && } - {defaultApplication && ( - App: {defaultApplication} - )} -
-
- ); -} diff --git a/ui/src/pages/LogsTab/LogsPage.tsx b/ui/src/pages/LogsTab/LogsPage.tsx deleted file mode 100644 index c4257a01..00000000 --- a/ui/src/pages/LogsTab/LogsPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useParams } from 'react-router'; -import { LogSearch } from './LogSearch'; - -export default function LogsPage() { - const { appId, routeId } = useParams<{ appId?: string; routeId?: string }>(); - return ; -} diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo deleted file mode 100644 index 1968d6a0..00000000 --- a/ui/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./vite.config.ts","./src/config.ts","./src/main.tsx","./src/router.tsx","./src/swagger-ui-dist.d.ts","./src/vite-env.d.ts","./src/api/client.ts","./src/api/schema.d.ts","./src/api/types.ts","./src/api/queries/agents.ts","./src/api/queries/catalog.ts","./src/api/queries/diagrams.ts","./src/api/queries/executions.ts","./src/api/queries/admin/admin-api.ts","./src/api/queries/admin/audit.ts","./src/api/queries/admin/database.ts","./src/api/queries/admin/opensearch.ts","./src/api/queries/admin/rbac.ts","./src/api/queries/admin/thresholds.ts","./src/auth/loginpage.tsx","./src/auth/oidccallback.tsx","./src/auth/protectedroute.tsx","./src/auth/auth-store.ts","./src/auth/use-auth.ts","./src/components/layoutshell.tsx","./src/pages/admin/auditlogpage.tsx","./src/pages/admin/databaseadminpage.tsx","./src/pages/admin/oidcconfigpage.tsx","./src/pages/admin/opensearchadminpage.tsx","./src/pages/admin/rbacpage.tsx","./src/pages/agenthealth/agenthealth.tsx","./src/pages/agentinstance/agentinstance.tsx","./src/pages/dashboard/dashboard.tsx","./src/pages/exchangedetail/exchangedetail.tsx","./src/pages/routes/routesmetrics.tsx","./src/pages/swagger/swaggerpage.tsx"],"errors":true,"version":"5.9.3"} \ No newline at end of file