/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ArrowLeft, FileText, Clock, Timer, Activity, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { useSchemaStore } from '@/stores/schemaStore'; import { jmapGet, getAccountId } from '@/services/jmap/client'; import { resolveObject } from '@/lib/schemaResolver'; import type { TraceEvent, TraceKeyValue, TraceValue } from '../types'; import { TraceTimeline } from './TraceTimeline'; import { jmapMapToArray } from '@/lib/jmapUtils'; function normalizeTraceEvents(raw: unknown): TraceEvent[] { const events = jmapMapToArray>(raw); return events.map((evt) => ({ event: String(evt.event ?? ''), timestamp: String(evt.timestamp ?? ''), keyValues: normalizeKeyValues(evt.keyValues), })); } function normalizeKeyValues(raw: unknown): TraceKeyValue[] { const kvs = jmapMapToArray>(raw); return kvs.map((kv) => ({ key: String(kv.key ?? ''), value: normalizeTraceValue(kv.value), })); } function normalizeTraceValue(raw: unknown): TraceValue { if (!raw || typeof raw !== 'object') return { '@type': 'Null' }; const obj = raw as Record; const type = obj['@type'] as string; if (type === 'List') { return { '@type': 'List', value: jmapMapToArray(obj.value).map(normalizeTraceValue) }; } if (type === 'Event') { return { '@type': 'Event', event: String(obj.event ?? ''), value: normalizeKeyValues(obj.value) }; } return raw as TraceValue; } interface TraceDetailViewProps { viewName: string; objectId: string; } export function TraceDetailView({ viewName, objectId }: TraceDetailViewProps) { const { t } = useTranslation(); const navigate = useNavigate(); const schema = useSchemaStore((s) => s.schema); const [events, setEvents] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!schema) return; let cancelled = false; async function load() { try { const resolved = resolveObject(schema!, viewName); if (!resolved) throw new Error(t('view.couldNotResolve', 'Could not resolve object')); const accountId = getAccountId(resolved.objectName); const responses = await jmapGet(resolved.objectName, accountId, [objectId]); if (cancelled) return; const getResult = responses.find(([name]) => name.endsWith('/get')); if (!getResult) throw new Error(t('view.noGetResponse', 'No get response')); const data = getResult[1] as { list?: Array<{ id: string; events?: unknown }> }; const trace = data.list?.[0]; if (!trace) throw new Error(t('tracing.traceNotFound', 'Trace not found')); setEvents(normalizeTraceEvents(trace.events)); } catch (e) { if (!cancelled) { setError(e instanceof Error ? e.message : t('tracing.failedToLoadTrace', 'Failed to load trace')); } } finally { if (!cancelled) setLoading(false); } } load(); return () => { cancelled = true; }; }, [schema, viewName, objectId, t]); if (loading) { return (
); } if (error) { return (
{error}
); } if (!events) return null; const eventTypeEnum = schema?.enums?.['EventType'] ?? []; const firstEvent = events[0]; const lastEvent = events[events.length - 1]; const spanLabel = firstEvent ? (eventTypeEnum.find((e) => e.name === firstEvent.event)?.label ?? firstEvent.event) : '-'; const dateStr = firstEvent ? new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'medium', }).format(new Date(firstEvent.timestamp)) : '-'; const durationMs = firstEvent && lastEvent && events.length >= 2 ? new Date(lastEvent.timestamp).getTime() - new Date(firstEvent.timestamp).getTime() : null; function formatDuration(ms: number): string { if (ms < 1000) return `${Math.round(ms)} ms`; if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`; const m = Math.floor(ms / 60_000); const s = Math.floor((ms % 60_000) / 1000); return `${m}m ${s}s`; } return (
{t('tracing.spanType', 'Span Type')}

{spanLabel}

{t('tracing.date', 'Date')}

{dateStr}

{t('tracing.duration', 'Duration')}

{durationMs !== null ? formatDuration(durationMs) : '-'}

{t('tracing.events', 'Events')}

{events.length}

); }