import { memo, useState, type KeyboardEvent, type MouseEvent } from 'react'; import { Copy } from 'lucide-react'; import type { LogsEntry } from '@/lib/api-client'; import { cn } from '@/lib/utils'; import { LogLevelBadge } from './log-level-badge'; import { FOCUS_RING, MONO_NUMERIC, ROW_DENSITY, ROW_INTERACTIVE, type RowDensity } from './tokens'; import { getDisplayLatency, getDisplayModule, getDisplayRequestId } from './utils'; export interface LogsRowProps { entry: LogsEntry; isSelected: boolean; density: RowDensity; sourceLabel: string; onSelect: (entryId: string) => void; /** Optional indent (px) when row sits inside an expanded trace. */ indent?: number; /** Optional stage chip text (e.g. for child rows of a trace). */ stageHint?: string; /** Optional repeat counter — when the row coalesces N identical consecutive entries. */ repeatCount?: number; } function formatHms(timestamp: string): string { const d = new Date(timestamp); if (Number.isNaN(d.getTime())) return timestamp; return d.toLocaleTimeString(undefined, { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', }); } async function copyText(text: string): Promise { try { await navigator.clipboard.writeText(text); return true; } catch { // Insecure context or clipboard permission denied. Caller should NOT // claim success in the UI when this returns false. return false; } } function LogsRowImpl({ entry, isSelected, density, sourceLabel, onSelect, indent = 0, stageHint, repeatCount, }: LogsRowProps) { const [justCopied, setJustCopied] = useState(false); const moduleLabel = getDisplayModule(entry, sourceLabel); const latencyLabel = getDisplayLatency(entry); const shortRequestId = getDisplayRequestId(entry, { short: true }); const handleSelect = () => onSelect(entry.id); const handleKey = (e: KeyboardEvent) => { // Ignore keys that bubbled from a nested interactive element (e.g. the // copy-requestId button). Without this guard, pressing Enter on the // copy button would also select the row and shift the detail panel. if (e.target !== e.currentTarget) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleSelect(); } }; const handleCopyRequestId = async (e: MouseEvent) => { e.stopPropagation(); if (!entry.requestId) return; const ok = await copyText(entry.requestId); if (!ok) return; setJustCopied(true); window.setTimeout(() => setJustCopied(false), 1200); }; // Row is a focusable div (not a button) so it can host a real ) : null} ); } export const LogsRow = memo(LogsRowImpl, (prev, next) => { return ( prev.entry.id === next.entry.id && prev.isSelected === next.isSelected && prev.density === next.density && prev.indent === next.indent && prev.stageHint === next.stageHint && prev.sourceLabel === next.sourceLabel && prev.repeatCount === next.repeatCount ); });