import { useMemo, useState } from "react"; import { Listbox, ListboxButton, ListboxOption, ListboxOptions, } from "@headlessui/react"; import { XMarkIcon } from "@heroicons/react/24/outline"; import { CheckIcon, ChevronUpDownIcon, FunnelIcon, MagnifyingGlassIcon, } from "@heroicons/react/16/solid"; import type { EventEnvelope } from "@qltysh/fabro-api-client"; import { Tooltip } from "./ui"; import { formatAbsoluteTs } from "../lib/format"; import { debugCategory, debugCategoryColor, debugCategoryLabel, debugCategoryTone, formatElapsed, highlightJson, type DebugCategory, } from "./event-debug-helpers"; import { FloatingTooltip } from "./floating-tooltip"; import { useWindowEvent } from "../hooks/effects"; export function DebugEventRow({ event, runStart, selected, onSelect, }: { event: EventEnvelope; runStart: string | undefined; selected: boolean; onSelect: () => void; }) { const eventName = event.event ?? ""; const category = debugCategory(eventName); return ( ); } export function DetailsPanel({ title, isOpen, onClose, children, }: { title: string; isOpen: boolean; onClose: () => void; children: React.ReactNode; }) { useWindowEvent( "keydown", (event) => { if (event.key === "Escape") onClose(); }, undefined, isOpen, ); return (

{title}

{isOpen ? children : null}
); } export type EventDisplayPayload = { event?: string | null; [key: string]: unknown; }; export function DebugEventDetailsPanel({ event, onClose, }: { event: EventDisplayPayload | null; onClose: () => void; }) { return ( {event ? : null} ); } function DebugEventDetails({ event }: { event: EventDisplayPayload }) { const text = useMemo(() => JSON.stringify(event, null, 2), [event]); const tokens = useMemo(() => highlightJson(text), [text]); return (
      {tokens}
    
); } export function MultiSelectFilter({ selected, options, labelOf, onChange, emptyMeansAll = false, }: { selected: T[]; options: readonly T[]; labelOf: (item: T) => string; onChange: (next: T[]) => void; emptyMeansAll?: boolean; }) { const allSelected = selected.length === options.length; const summary = useMemo(() => { if (allSelected || (emptyMeansAll && selected.length === 0)) return "All types"; if (selected.length === 0) return "No types"; if (selected.length <= 2) { const selectedSet = new Set(selected); const labels: string[] = []; for (const option of options) { if (selectedSet.has(option)) labels.push(labelOf(option)); } return labels.join(", "); } return `${selected.length} types`; }, [allSelected, emptyMeansAll, selected, options, labelOf]); return ( {options.map((option) => ( {labelOf(option)} ))} ); } export function EventSearchInput({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { const [focused, setFocused] = useState(false); const expanded = focused || value.length > 0; return (
); } const STRIP_HEIGHT = 32; const BAR_NORMAL_HEIGHT = 22; const BAR_HOVER_HEIGHT = 26; const BAR_SELECTED_HEIGHT = 28; const BAR_WIDTH = 4; const STRIP_MAX_MARKERS = 600; function sampleStripItems( items: T[], maxItems: number, keep: (item: T) => boolean, ): T[] { if (items.length <= maxItems) return items; const indices = new Set(); for (let i = 0; i < items.length; i += 1) { if (keep(items[i])) indices.add(i); } for (let i = 0; i < maxItems; i += 1) { indices.add(Math.round((i * (items.length - 1)) / Math.max(1, maxItems - 1))); } return Array.from(indices) .sort((a, b) => a - b) .map((index) => items[index]); } function friendlyEventName(eventName: string): string { const parts = eventName.split("."); if (parts.length <= 1) return eventName; return parts.slice(1).join("."); } export function DebugDnaStrip({ events, selectedSeq, onSelect, runStart, }: { events: EventEnvelope[]; selectedSeq: number | null; onSelect: (seq: number) => void; runStart: string | undefined; }) { const [hover, setHover] = useState<{ seq: number; rect: DOMRect; } | null>(null); const visibleEvents = useMemo( () => sampleStripItems(events, STRIP_MAX_MARKERS, (event) => event.seq === selectedSeq), [events, selectedSeq], ); const visibleEventBySeq = useMemo( () => new Map(visibleEvents.map((event) => [event.seq, event])), [visibleEvents], ); const range = useMemo(() => { if (events.length === 0) return null; let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (const event of events) { const ms = Date.parse(event.ts); if (Number.isNaN(ms)) continue; if (ms < min) min = ms; if (ms > max) max = ms; } if (!Number.isFinite(min) || !Number.isFinite(max)) return null; const startCandidate = runStart ? Date.parse(runStart) : Number.NaN; const start = Number.isFinite(startCandidate) ? Math.min(startCandidate, min) : min; const duration = Math.max(1, max - start); return { start, duration }; }, [events, runStart]); if (!range) { return (