@mastra/playground-ui 51.3.2-alpha.0 → 51.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/dist/Badge-DcllBqFg.cjs +125 -0
- package/dist/Badge-DcllBqFg.cjs.map +1 -0
- package/dist/Badge-uGehUtus.js +123 -0
- package/dist/Badge-uGehUtus.js.map +1 -0
- package/dist/components/Badge.cjs.js +1 -1
- package/dist/components/Badge.es.js +1 -1
- package/dist/components/DataList.cjs.js +4 -4
- package/dist/components/DataList.es.js +4 -4
- package/dist/components/EnvironmentVariablesEditor.cjs.js +1 -1
- package/dist/components/EnvironmentVariablesEditor.es.js +1 -1
- package/dist/components/ItemList.cjs.js +59 -40
- package/dist/components/ItemList.cjs.js.map +1 -1
- package/dist/components/ItemList.es.js +58 -41
- package/dist/components/ItemList.es.js.map +1 -1
- package/dist/components/LogsDataList.cjs.js +2 -2
- package/dist/components/LogsDataList.es.js +2 -2
- package/dist/components/ai/ask-user.cjs.js +3 -3
- package/dist/components/ai/ask-user.cjs.js.map +1 -1
- package/dist/components/ai/ask-user.es.js +3 -3
- package/dist/components/ai/ask-user.es.js.map +1 -1
- package/dist/components/ai/plan.cjs.js +3 -3
- package/dist/components/ai/plan.cjs.js.map +1 -1
- package/dist/components/ai/plan.es.js +3 -3
- package/dist/components/ai/plan.es.js.map +1 -1
- package/dist/{data-list-BOI8fRuk.js → data-list-C3tjx5Vb.js} +2 -2
- package/dist/{data-list-BOI8fRuk.js.map → data-list-C3tjx5Vb.js.map} +1 -1
- package/dist/{data-list-CCacNODd.cjs → data-list-CRDewYj9.cjs} +2 -2
- package/dist/{data-list-CCacNODd.cjs.map → data-list-CRDewYj9.cjs.map} +1 -1
- package/dist/{data-list-skeleton-DDO_OE14.js → data-list-skeleton-BCrkk6wG.js} +2 -2
- package/dist/{data-list-skeleton-DDO_OE14.js.map → data-list-skeleton-BCrkk6wG.js.map} +1 -1
- package/dist/{data-list-skeleton-BHBSIQHN.cjs → data-list-skeleton-CONfqzH2.cjs} +2 -2
- package/dist/{data-list-skeleton-BHBSIQHN.cjs.map → data-list-skeleton-CONfqzH2.cjs.map} +1 -1
- package/dist/{data-list-top-cell-BOXO5s7I.cjs → data-list-top-cell-DAztx3m8.cjs} +6 -1
- package/dist/data-list-top-cell-DAztx3m8.cjs.map +1 -0
- package/dist/{data-list-top-cell-CC5jjv5s.js → data-list-top-cell-hNkk6S-Q.js} +6 -2
- package/dist/data-list-top-cell-hNkk6S-Q.js.map +1 -0
- package/dist/domains/logs/components/logs-list-view.cjs.js +2 -2
- package/dist/domains/logs/components/logs-list-view.es.js +2 -2
- package/dist/domains/metrics/components/memory-card-view.cjs.js +1 -1
- package/dist/domains/metrics/components/memory-card-view.es.js +1 -1
- package/dist/domains/metrics/components/model-usage-cost-card-view.cjs.js +1 -1
- package/dist/domains/metrics/components/model-usage-cost-card-view.es.js +1 -1
- package/dist/domains/metrics/components/scores-card-view.cjs.js +1 -1
- package/dist/domains/metrics/components/scores-card-view.es.js +1 -1
- package/dist/domains/traces/components/format-hierarchical-spans.cjs.js +2 -1
- package/dist/domains/traces/components/format-hierarchical-spans.cjs.js.map +1 -1
- package/dist/domains/traces/components/format-hierarchical-spans.es.js +2 -1
- package/dist/domains/traces/components/format-hierarchical-spans.es.js.map +1 -1
- package/dist/domains/traces/components/timeline-name-col.cjs.js +10 -1
- package/dist/domains/traces/components/timeline-name-col.cjs.js.map +1 -1
- package/dist/domains/traces/components/timeline-name-col.es.js +10 -1
- package/dist/domains/traces/components/timeline-name-col.es.js.map +1 -1
- package/dist/domains/traces/components/trace-data-panel-view.cjs.js +17 -5
- package/dist/domains/traces/components/trace-data-panel-view.cjs.js.map +1 -1
- package/dist/domains/traces/components/trace-data-panel-view.es.js +17 -5
- package/dist/domains/traces/components/trace-data-panel-view.es.js.map +1 -1
- package/dist/domains/traces/components/traces-list-view.cjs.js +7 -9
- package/dist/domains/traces/components/traces-list-view.cjs.js.map +1 -1
- package/dist/domains/traces/components/traces-list-view.es.js +7 -9
- package/dist/domains/traces/components/traces-list-view.es.js.map +1 -1
- package/dist/domains/traces/hooks/use-trace-search.cjs.js +10 -4
- package/dist/domains/traces/hooks/use-trace-search.cjs.js.map +1 -1
- package/dist/domains/traces/hooks/use-trace-search.es.js +10 -4
- package/dist/domains/traces/hooks/use-trace-search.es.js.map +1 -1
- package/dist/domains/traces/trace-list-columns.cjs.js +1 -1
- package/dist/domains/traces/trace-list-columns.cjs.js.map +1 -1
- package/dist/domains/traces/trace-list-columns.es.js +1 -1
- package/dist/domains/traces/trace-list-columns.es.js.map +1 -1
- package/dist/domains/traces/types.cjs.js.map +1 -1
- package/dist/domains/traces/types.es.js.map +1 -1
- package/dist/ee/topics/components/topic-trace-summary-list.cjs.js +1 -1
- package/dist/ee/topics/components/topic-trace-summary-list.es.js +1 -1
- package/dist/hooks/use-scroll-to-first-highlight.cjs.js +44 -0
- package/dist/hooks/use-scroll-to-first-highlight.cjs.js.map +1 -0
- package/dist/hooks/use-scroll-to-first-highlight.d.ts +2 -0
- package/dist/hooks/use-scroll-to-first-highlight.es.js +40 -0
- package/dist/hooks/use-scroll-to-first-highlight.es.js.map +1 -0
- package/dist/hooks/use-text-highlight.cjs.js +49 -2
- package/dist/hooks/use-text-highlight.cjs.js.map +1 -1
- package/dist/hooks/use-text-highlight.es.js +48 -3
- package/dist/hooks/use-text-highlight.es.js.map +1 -1
- package/dist/{logs-data-list-Xayqbjw4.cjs → logs-data-list-D88JF2lo.cjs} +2 -2
- package/dist/{logs-data-list-Xayqbjw4.cjs.map → logs-data-list-D88JF2lo.cjs.map} +1 -1
- package/dist/{logs-data-list-DRXJ89IZ.js → logs-data-list-DnAXTwr7.js} +2 -2
- package/dist/{logs-data-list-DRXJ89IZ.js.map → logs-data-list-DnAXTwr7.js.map} +1 -1
- package/dist/src/domains/traces/components/format-hierarchical-spans.d.ts +1 -0
- package/dist/src/domains/traces/components/trace-data-panel-view.d.ts +1 -1
- package/dist/src/domains/traces/hooks/use-trace-search.d.ts +7 -0
- package/dist/src/domains/traces/types.d.ts +5 -0
- package/dist/src/ds/components/Badge/Badge.d.ts +86 -12
- package/dist/src/ds/components/Badge/badge.stories.d.ts +3 -0
- package/dist/src/ds/components/DataList/TracesDataList/traces-data-list.d.ts +2 -3
- package/dist/src/ds/components/DataList/data-list-cells.d.ts +5 -0
- package/dist/src/ds/components/ItemList/helpers.d.ts +8 -0
- package/dist/src/ds/components/ItemList/item-list-link-cell.d.ts +2 -1
- package/dist/src/ds/components/ItemList/item-list-row-button.d.ts +2 -1
- package/dist/src/ds/components/ai/plan/plan.d.ts +1 -1
- package/dist/src/hooks/use-scroll-to-first-highlight.d.ts +20 -0
- package/dist/src/hooks/use-text-highlight.d.ts +18 -0
- package/dist/src/test/highlight-api.d.ts +6 -4
- package/dist/style.css +364 -219
- package/dist/{traces-data-list-BLZgf-Es.cjs → traces-data-list-COksp2d4.cjs} +12 -13
- package/dist/traces-data-list-COksp2d4.cjs.map +1 -0
- package/dist/{traces-data-list-DmXGwe4Q.js → traces-data-list-c5I-8hwt.js} +12 -13
- package/dist/traces-data-list-c5I-8hwt.js.map +1 -0
- package/dist/use-text-highlight.css +8 -0
- package/package.json +7 -7
- package/theme.css +53 -0
- package/dist/Badge-BlJpI3fY.cjs +0 -57
- package/dist/Badge-BlJpI3fY.cjs.map +0 -1
- package/dist/Badge-woV6OD8k.js +0 -55
- package/dist/Badge-woV6OD8k.js.map +0 -1
- package/dist/chip-9P9t0dvN.cjs +0 -68
- package/dist/chip-9P9t0dvN.cjs.map +0 -1
- package/dist/chip-Bg0-95sX.js +0 -66
- package/dist/chip-Bg0-95sX.js.map +0 -1
- package/dist/components/Chip.cjs.js +0 -27
- package/dist/components/Chip.cjs.js.map +0 -1
- package/dist/components/Chip.d.ts +0 -2
- package/dist/components/Chip.es.js +0 -22
- package/dist/components/Chip.es.js.map +0 -1
- package/dist/components/StatusBadge.cjs.js +0 -76
- package/dist/components/StatusBadge.cjs.js.map +0 -1
- package/dist/components/StatusBadge.d.ts +0 -2
- package/dist/components/StatusBadge.es.js +0 -72
- package/dist/components/StatusBadge.es.js.map +0 -1
- package/dist/data-list-top-cell-BOXO5s7I.cjs.map +0 -1
- package/dist/data-list-top-cell-CC5jjv5s.js.map +0 -1
- package/dist/src/ds/components/Chip/chip.d.ts +0 -8
- package/dist/src/ds/components/Chip/chip.stories.d.ts +0 -11
- package/dist/src/ds/components/Chip/chips-group.d.ts +0 -2
- package/dist/src/ds/components/Chip/index.d.ts +0 -2
- package/dist/src/ds/components/StatusBadge/StatusBadge.d.ts +0 -13
- package/dist/src/ds/components/StatusBadge/index.d.ts +0 -2
- package/dist/src/ds/components/StatusBadge/status-badge.stories.d.ts +0 -8
- package/dist/traces-data-list-BLZgf-Es.cjs.map +0 -1
- package/dist/traces-data-list-DmXGwe4Q.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"traces-list-view.es.js","sources":["../../../../src/domains/traces/components/traces-list-view.tsx"],"sourcesContent":["import { useVirtualizer } from '@tanstack/react-virtual';\nimport { useEffect, useRef } from 'react';\nimport {\n DEFAULT_TRACE_COLUMN_PREFERENCES,\n buildTraceListColumns,\n formatTraceMetadataValue,\n hasTraceColumn,\n} from '../trace-list-columns';\nimport type { TraceColumnPreferences, TraceUsageSummary } from '../trace-list-columns';\nimport { formatSpanDuration, getInputPreview } from '../utils/span-utils';\nimport { formatCompact, formatCost } from '@/domains/metrics/components/metrics-utils';\nimport { DataList, DataListSkeleton, TracesDataList, useDataListKeyboard } from '@/ds/components/DataList';\nimport { cn } from '@/lib/utils';\n\nexport type TracesListViewTrace = {\n traceId: string;\n /** Required for branch rows; absent on plain trace rows (which are root-rooted). */\n spanId?: string | null;\n /** `null`/missing → root span. Drives the Kind column's icon (top-level trace vs nested branch). */\n parentSpanId?: string | null;\n name: string;\n entityType?: string | null;\n entityId?: string | null;\n entityName?: string | null;\n status?: string | null;\n /** Server-rendered preview of `input`. Present on lightweight rows, which omit `input` itself. */\n inputPreview?: string | null;\n input?: unknown;\n metadata?: Record<string, unknown> | null;\n startedAt?: Date | string | null;\n endedAt?: Date | string | null;\n createdAt: Date | string;\n};\n\nconst ROW_HEIGHT = 36;\nconst OVERSCAN = 8;\n\nexport type TracesListViewProps = {\n traces: TracesListViewTrace[];\n isLoading?: boolean;\n isFetchingNextPage?: boolean;\n hasNextPage?: boolean;\n setEndOfListElement?: (element: HTMLDivElement | null) => void;\n filtersApplied?: boolean;\n /** Currently featured/selected trace — its row gets the highlighted background. */\n featuredTraceId?: string | null;\n /**\n * Required in branches mode to disambiguate rows sharing a `traceId`. When set,\n * a row is featured only when both `traceId` and `spanId` match.\n */\n featuredSpanId?: string | null;\n /** Branches mode mixes root traces with subtraces — enables the Trace/Subtrace tooltip on the\n * level icon in the Name column, which is meaningless when every row is a root. */\n isBranchesMode?: boolean;\n /** Keys (`traceId:spanId`) of rows that just arrived via delta polling. Rows whose key is in\n * this set get a temporary tint to distinguish them from rows present since the last page-mode\n * fetch. Auto-expires upstream (in useTraces) after a short window. */\n recentlyAddedKeys?: Set<string>;\n /** Optional and metadata columns selected for this project. */\n columnPreferences?: TraceColumnPreferences;\n /** Token and estimated-cost totals, aggregated across model spans by trace ID. */\n usageByTraceId?: ReadonlyMap<string, TraceUsageSummary>;\n /** Called when a row is clicked. The current selection logic (toggle on same id) is the consumer's call. */\n onTraceClick: (trace: TracesListViewTrace) => void;\n};\n\n/**\n * Virtualized presentational list. Renders only the visible window via TanStack Virtual, and\n * uses DataList primitives for layout (CSS Grid with subgrid rows).\n */\nexport function TracesListView({\n traces,\n isLoading,\n isFetchingNextPage,\n hasNextPage,\n setEndOfListElement,\n filtersApplied,\n featuredTraceId,\n featuredSpanId,\n isBranchesMode,\n recentlyAddedKeys,\n columnPreferences = DEFAULT_TRACE_COLUMN_PREFERENCES,\n usageByTraceId,\n onTraceClick,\n}: TracesListViewProps) {\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const columns = buildTraceListColumns(columnPreferences);\n\n const virtualizer = useVirtualizer({\n count: traces.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => ROW_HEIGHT,\n overscan: OVERSCAN,\n });\n\n const { getRowProps } = useDataListKeyboard({\n count: traces.length,\n containerRef: scrollRef,\n onNavigate: index => virtualizer.scrollToIndex(index),\n });\n\n // Reset scroll to top whenever a fresh query resolves (filter / date range change).\n // `isLoading` only flips on initial fetches — `fetchNextPage` keeps it `false`, so this\n // effect doesn't fire during pagination.\n //\n // Why the manual scroll event: when the skeleton-vs-list branch swaps in the new scroll\n // container, it mounts at `scrollTop = 0`. The virtualizer rebinds its listener but\n // doesn't re-read `scrollTop`, so it keeps the stale `scrollOffset` from the previous\n // element. `scrollToOffset(0)` no-ops because the new element is already at 0 (no scroll\n // event fires). Dispatching a synthetic `scroll` forces the virtualizer's handler to\n // read the fresh `scrollTop` and recompute `virtualItems` with `paddingTop = 0`.\n const wasLoadingRef = useRef(isLoading);\n useEffect(() => {\n if (wasLoadingRef.current && !isLoading) {\n scrollRef.current?.dispatchEvent(new Event('scroll'));\n }\n wasLoadingRef.current = isLoading;\n }, [isLoading]);\n\n if (isLoading) {\n return <DataListSkeleton columns={columns} />;\n }\n\n const virtualItems = virtualizer.getVirtualItems();\n const totalSize = virtualizer.getTotalSize();\n const paddingTop = virtualItems[0]?.start ?? 0;\n const paddingBottom =\n virtualItems.length > 0 ? Math.max(0, totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0)) : 0;\n\n return (\n <TracesDataList columns={columns} variant=\"striped\" scrollRef={scrollRef} className=\"min-w-0\">\n <TracesDataList.Top>\n <TracesDataList.TopCell>Date</TracesDataList.TopCell>\n <TracesDataList.TopCell>Time</TracesDataList.TopCell>\n <TracesDataList.TopCell>Name</TracesDataList.TopCell>\n {hasTraceColumn(columnPreferences, 'input') && <TracesDataList.TopCell>Input</TracesDataList.TopCell>}\n {hasTraceColumn(columnPreferences, 'entity') && <TracesDataList.TopCell>Entity</TracesDataList.TopCell>}\n <TracesDataList.TopCell>Status</TracesDataList.TopCell>\n {hasTraceColumn(columnPreferences, 'duration') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Duration</TracesDataList.TopCell>\n )}\n {hasTraceColumn(columnPreferences, 'inputTokens') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Input tokens</TracesDataList.TopCell>\n )}\n {hasTraceColumn(columnPreferences, 'outputTokens') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Output tokens</TracesDataList.TopCell>\n )}\n {hasTraceColumn(columnPreferences, 'estimatedCost') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Est. cost</TracesDataList.TopCell>\n )}\n {columnPreferences.metadataKeys.map(key => (\n <TracesDataList.TopCellWithTooltip key={key} tooltip={key}>\n {key}\n </TracesDataList.TopCellWithTooltip>\n ))}\n </TracesDataList.Top>\n\n {traces.length === 0 ? (\n <TracesDataList.NoMatch\n message={filtersApplied ? 'No traces found for applied filters' : 'No traces found yet'}\n />\n ) : (\n <>\n <TracesDataList.Spacer height={paddingTop} />\n {virtualItems.map(vi => {\n const trace = traces[vi.index];\n if (!trace) return null;\n\n const isFeatured =\n trace.traceId === featuredTraceId && (featuredSpanId == null || trace.spanId === featuredSpanId);\n const rowKey = `${trace.traceId}:${trace.spanId ?? ''}`;\n const isRecentlyAdded = recentlyAddedKeys?.has(rowKey) ?? false;\n const displayDate = trace.startedAt ?? trace.createdAt;\n const entityName = trace.entityName || trace.entityId;\n const usage = usageByTraceId?.get(trace.traceId);\n\n return (\n <TracesDataList.RowButton\n key={rowKey}\n ref={virtualizer.measureElement}\n data-index={vi.index}\n {...getRowProps(vi.index)}\n onClick={() => onTraceClick(trace)}\n featured={isFeatured}\n className={cn(isRecentlyAdded && 'animate-row-highlight')}\n >\n <TracesDataList.DateCell timestamp={displayDate} />\n <TracesDataList.TimeCell timestamp={displayDate} />\n <TracesDataList.NameCell\n name={trace.name}\n parentSpanId={trace.parentSpanId}\n showLevelTooltip={isBranchesMode}\n />\n {hasTraceColumn(columnPreferences, 'input') && (\n <TracesDataList.InputCell input={trace.inputPreview ?? getInputPreview(trace.input)} />\n )}\n {hasTraceColumn(columnPreferences, 'entity') && (\n <TracesDataList.EntityCell entityType={trace.entityType} entityName={entityName} />\n )}\n <TracesDataList.StatusCell status={trace.status} />\n {hasTraceColumn(columnPreferences, 'duration') && (\n <DataList.NumberCell>{formatSpanDuration(trace.startedAt, trace.endedAt)}</DataList.NumberCell>\n )}\n {hasTraceColumn(columnPreferences, 'inputTokens') && (\n <DataList.NumberCell>\n {usage?.inputTokens === undefined ? undefined : formatCompact(usage.inputTokens)}\n </DataList.NumberCell>\n )}\n {hasTraceColumn(columnPreferences, 'outputTokens') && (\n <DataList.NumberCell>\n {usage?.outputTokens === undefined ? undefined : formatCompact(usage.outputTokens)}\n </DataList.NumberCell>\n )}\n {hasTraceColumn(columnPreferences, 'estimatedCost') && (\n <DataList.NumberCell>\n {usage?.estimatedCost === undefined ? undefined : formatCost(usage.estimatedCost, usage.costUnit)}\n </DataList.NumberCell>\n )}\n {columnPreferences.metadataKeys.map(key => {\n const value = formatTraceMetadataValue(trace.metadata, key);\n return (\n <DataList.TextCell font=\"mono\" key={key}>\n {value}\n </DataList.TextCell>\n );\n })}\n </TracesDataList.RowButton>\n );\n })}\n <TracesDataList.Spacer height={paddingBottom} />\n <TracesDataList.NextPageLoading\n isLoading={isFetchingNextPage}\n hasMore={hasNextPage}\n setEndOfListElement={setEndOfListElement}\n />\n </>\n )}\n </TracesDataList>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"traces-list-view.es.js","sources":["../../../../src/domains/traces/components/traces-list-view.tsx"],"sourcesContent":["import { useVirtualizer } from '@tanstack/react-virtual';\nimport { useEffect, useRef } from 'react';\nimport {\n DEFAULT_TRACE_COLUMN_PREFERENCES,\n buildTraceListColumns,\n formatTraceMetadataValue,\n hasTraceColumn,\n} from '../trace-list-columns';\nimport type { TraceColumnPreferences, TraceUsageSummary } from '../trace-list-columns';\nimport { formatSpanDuration, getInputPreview } from '../utils/span-utils';\nimport { formatCompact, formatCost } from '@/domains/metrics/components/metrics-utils';\nimport { DataList, DataListSkeleton, TracesDataList, useDataListKeyboard } from '@/ds/components/DataList';\nimport { cn } from '@/lib/utils';\n\nexport type TracesListViewTrace = {\n traceId: string;\n /** Required for branch rows; absent on plain trace rows (which are root-rooted). */\n spanId?: string | null;\n /** `null`/missing → root span. Drives the Kind column's icon (top-level trace vs nested branch). */\n parentSpanId?: string | null;\n name: string;\n entityType?: string | null;\n entityId?: string | null;\n entityName?: string | null;\n status?: string | null;\n /** Server-rendered preview of `input`. Present on lightweight rows, which omit `input` itself. */\n inputPreview?: string | null;\n input?: unknown;\n metadata?: Record<string, unknown> | null;\n startedAt?: Date | string | null;\n endedAt?: Date | string | null;\n createdAt: Date | string;\n};\n\nconst ROW_HEIGHT = 36;\nconst OVERSCAN = 8;\n\nexport type TracesListViewProps = {\n traces: TracesListViewTrace[];\n isLoading?: boolean;\n isFetchingNextPage?: boolean;\n hasNextPage?: boolean;\n setEndOfListElement?: (element: HTMLDivElement | null) => void;\n filtersApplied?: boolean;\n /** Currently featured/selected trace — its row gets the highlighted background. */\n featuredTraceId?: string | null;\n /**\n * Required in branches mode to disambiguate rows sharing a `traceId`. When set,\n * a row is featured only when both `traceId` and `spanId` match.\n */\n featuredSpanId?: string | null;\n /** Branches mode mixes root traces with subtraces — enables the Trace/Subtrace tooltip on the\n * level icon in the Name column, which is meaningless when every row is a root. */\n isBranchesMode?: boolean;\n /** Keys (`traceId:spanId`) of rows that just arrived via delta polling. Rows whose key is in\n * this set get a temporary tint to distinguish them from rows present since the last page-mode\n * fetch. Auto-expires upstream (in useTraces) after a short window. */\n recentlyAddedKeys?: Set<string>;\n /** Optional and metadata columns selected for this project. */\n columnPreferences?: TraceColumnPreferences;\n /** Token and estimated-cost totals, aggregated across model spans by trace ID. */\n usageByTraceId?: ReadonlyMap<string, TraceUsageSummary>;\n /** Called when a row is clicked. The current selection logic (toggle on same id) is the consumer's call. */\n onTraceClick: (trace: TracesListViewTrace) => void;\n};\n\n/**\n * Virtualized presentational list. Renders only the visible window via TanStack Virtual, and\n * uses DataList primitives for layout (CSS Grid with subgrid rows).\n */\nexport function TracesListView({\n traces,\n isLoading,\n isFetchingNextPage,\n hasNextPage,\n setEndOfListElement,\n filtersApplied,\n featuredTraceId,\n featuredSpanId,\n isBranchesMode,\n recentlyAddedKeys,\n columnPreferences = DEFAULT_TRACE_COLUMN_PREFERENCES,\n usageByTraceId,\n onTraceClick,\n}: TracesListViewProps) {\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const columns = buildTraceListColumns(columnPreferences);\n\n const virtualizer = useVirtualizer({\n count: traces.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => ROW_HEIGHT,\n overscan: OVERSCAN,\n });\n\n const { getRowProps } = useDataListKeyboard({\n count: traces.length,\n containerRef: scrollRef,\n onNavigate: index => virtualizer.scrollToIndex(index),\n });\n\n // Reset scroll to top whenever a fresh query resolves (filter / date range change).\n // `isLoading` only flips on initial fetches — `fetchNextPage` keeps it `false`, so this\n // effect doesn't fire during pagination.\n //\n // Why the manual scroll event: when the skeleton-vs-list branch swaps in the new scroll\n // container, it mounts at `scrollTop = 0`. The virtualizer rebinds its listener but\n // doesn't re-read `scrollTop`, so it keeps the stale `scrollOffset` from the previous\n // element. `scrollToOffset(0)` no-ops because the new element is already at 0 (no scroll\n // event fires). Dispatching a synthetic `scroll` forces the virtualizer's handler to\n // read the fresh `scrollTop` and recompute `virtualItems` with `paddingTop = 0`.\n const wasLoadingRef = useRef(isLoading);\n useEffect(() => {\n if (wasLoadingRef.current && !isLoading) {\n scrollRef.current?.dispatchEvent(new Event('scroll'));\n }\n wasLoadingRef.current = isLoading;\n }, [isLoading]);\n\n if (isLoading) {\n return <DataListSkeleton columns={columns} fit=\"container\" />;\n }\n\n const virtualItems = virtualizer.getVirtualItems();\n const totalSize = virtualizer.getTotalSize();\n const paddingTop = virtualItems[0]?.start ?? 0;\n const paddingBottom =\n virtualItems.length > 0 ? Math.max(0, totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0)) : 0;\n\n return (\n <TracesDataList columns={columns} variant=\"striped\" fit=\"container\" scrollRef={scrollRef} className=\"min-w-0\">\n <TracesDataList.Top>\n <TracesDataList.TopCell>Created</TracesDataList.TopCell>\n <TracesDataList.TopCell>Name</TracesDataList.TopCell>\n {hasTraceColumn(columnPreferences, 'input') && <TracesDataList.TopCell>Input</TracesDataList.TopCell>}\n {hasTraceColumn(columnPreferences, 'entity') && <TracesDataList.TopCell>Entity</TracesDataList.TopCell>}\n <TracesDataList.TopCell>Status</TracesDataList.TopCell>\n {hasTraceColumn(columnPreferences, 'duration') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Duration</TracesDataList.TopCell>\n )}\n {hasTraceColumn(columnPreferences, 'inputTokens') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Input tokens</TracesDataList.TopCell>\n )}\n {hasTraceColumn(columnPreferences, 'outputTokens') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Output tokens</TracesDataList.TopCell>\n )}\n {hasTraceColumn(columnPreferences, 'estimatedCost') && (\n <TracesDataList.TopCell className=\"justify-end text-right\">Est. cost</TracesDataList.TopCell>\n )}\n {columnPreferences.metadataKeys.map(key => (\n <TracesDataList.TopCellWithTooltip key={key} tooltip={key}>\n {key}\n </TracesDataList.TopCellWithTooltip>\n ))}\n </TracesDataList.Top>\n\n {traces.length === 0 ? (\n <TracesDataList.NoMatch\n message={filtersApplied ? 'No traces found for applied filters' : 'No traces found yet'}\n />\n ) : (\n <>\n <TracesDataList.Spacer height={paddingTop} />\n {virtualItems.map(vi => {\n const trace = traces[vi.index];\n if (!trace) return null;\n\n const isFeatured =\n trace.traceId === featuredTraceId && (featuredSpanId == null || trace.spanId === featuredSpanId);\n const rowKey = `${trace.traceId}:${trace.spanId ?? ''}`;\n const isRecentlyAdded = recentlyAddedKeys?.has(rowKey) ?? false;\n const displayDate = trace.startedAt ?? trace.createdAt;\n const entityName = trace.entityName || trace.entityId;\n const usage = usageByTraceId?.get(trace.traceId);\n\n return (\n <TracesDataList.RowButton\n key={rowKey}\n ref={virtualizer.measureElement}\n data-index={vi.index}\n {...getRowProps(vi.index)}\n onClick={() => onTraceClick(trace)}\n featured={isFeatured}\n className={cn(isRecentlyAdded && 'animate-row-highlight')}\n >\n <TracesDataList.CreatedCell timestamp={displayDate} />\n <TracesDataList.NameCell\n name={trace.name}\n parentSpanId={trace.parentSpanId}\n showLevelTooltip={isBranchesMode}\n />\n {hasTraceColumn(columnPreferences, 'input') && (\n <TracesDataList.InputCell input={trace.inputPreview ?? getInputPreview(trace.input)} />\n )}\n {hasTraceColumn(columnPreferences, 'entity') && (\n <TracesDataList.EntityCell entityType={trace.entityType} entityName={entityName} />\n )}\n <TracesDataList.StatusCell status={trace.status} />\n {hasTraceColumn(columnPreferences, 'duration') && (\n <DataList.NumberCell>{formatSpanDuration(trace.startedAt, trace.endedAt)}</DataList.NumberCell>\n )}\n {hasTraceColumn(columnPreferences, 'inputTokens') && (\n <DataList.NumberCell>\n {usage?.inputTokens === undefined ? undefined : formatCompact(usage.inputTokens)}\n </DataList.NumberCell>\n )}\n {hasTraceColumn(columnPreferences, 'outputTokens') && (\n <DataList.NumberCell>\n {usage?.outputTokens === undefined ? undefined : formatCompact(usage.outputTokens)}\n </DataList.NumberCell>\n )}\n {hasTraceColumn(columnPreferences, 'estimatedCost') && (\n <DataList.NumberCell>\n {usage?.estimatedCost === undefined ? undefined : formatCost(usage.estimatedCost, usage.costUnit)}\n </DataList.NumberCell>\n )}\n {columnPreferences.metadataKeys.map(key => {\n const value = formatTraceMetadataValue(trace.metadata, key);\n return (\n <DataList.TextCell font=\"mono\" key={key}>\n {value}\n </DataList.TextCell>\n );\n })}\n </TracesDataList.RowButton>\n );\n })}\n <TracesDataList.Spacer height={paddingBottom} />\n <TracesDataList.NextPageLoading\n isLoading={isFetchingNextPage}\n hasMore={hasNextPage}\n setEndOfListElement={setEndOfListElement}\n />\n </>\n )}\n </TracesDataList>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -9,12 +9,18 @@ require('date-fns');
|
|
|
9
9
|
function useTraceSearch(spans) {
|
|
10
10
|
const [query, setQuery] = React.useState("");
|
|
11
11
|
const deferredQuery = React.useDeferredValue(query);
|
|
12
|
-
const results = React.useMemo(() => {
|
|
12
|
+
const { results, payloadOnlyMatchIds } = React.useMemo(() => {
|
|
13
13
|
const term = deferredQuery.trim().toLowerCase();
|
|
14
|
-
if (!term) return spans;
|
|
15
|
-
|
|
14
|
+
if (!term) return { results: spans, payloadOnlyMatchIds: /* @__PURE__ */ new Set() };
|
|
15
|
+
const payloadOnly = /* @__PURE__ */ new Set();
|
|
16
|
+
const filtered = domains_traces_utils_filterSpansKeepingAncestors.filterSpansKeepingAncestors(spans, (span) => {
|
|
17
|
+
if (!span.searchText.includes(term)) return false;
|
|
18
|
+
if (!span.name.toLowerCase().includes(term)) payloadOnly.add(span.spanId);
|
|
19
|
+
return true;
|
|
20
|
+
});
|
|
21
|
+
return { results: filtered, payloadOnlyMatchIds: payloadOnly };
|
|
16
22
|
}, [spans, deferredQuery]);
|
|
17
|
-
return { query, setQuery, results, isPending: query !== deferredQuery };
|
|
23
|
+
return { query, setQuery, results, payloadOnlyMatchIds, isPending: query !== deferredQuery };
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
exports.useTraceSearch = useTraceSearch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-trace-search.cjs.js","sources":["../../../../src/domains/traces/hooks/use-trace-search.ts"],"sourcesContent":["import { useDeferredValue, useMemo, useState } from 'react';\nimport type { SearchableSpan } from '../types';\nimport { filterSpansKeepingAncestors } from '../utils';\n\nexport interface UseTraceSearchResult {\n /** The immediate, user-facing input value. Bind this to the search field. */\n query: string;\n setQuery: (query: string) => void;\n /** Rows matching the deferred query. An empty query returns the input array reference. */\n results: SearchableSpan[];\n /** True while the deferred value is behind `query` (the list is still catching up). */\n isPending: boolean;\n}\n\n/**\n * Client-side search over a flat span list.\n *\n * Matching is a substring test against `searchText`, the haystack built once per span when\n * the query resolved (see `toSearchableSpans`). That is what makes the open-ended `metadata`\n * and `error` payloads searchable: their shapes are unknown, so no fixed list of fields can\n * read them. Both sides are already lowercased, so the test is a bare `includes`.\n *\n * Filtering is keyed on a deferred copy of the query, so typing stays responsive while a\n * large list re-filters at lower priority — bind the input to `query`, not to the deferred\n * value. A matching span keeps its ancestors and its whole subtree, so the hierarchy stays\n * intact.\n *\n * `spans` is required. Resolving the loading/empty state is the caller's job — a component\n * holding a query result passes `data?.spans ?? []`.\n */\nexport function useTraceSearch(spans: SearchableSpan[]): UseTraceSearchResult {\n const [query, setQuery] = useState('');\n const deferredQuery = useDeferredValue(query);\n\n const results = useMemo(() => {\n const term = deferredQuery.trim().toLowerCase();\n if (!term) return spans;\n
|
|
1
|
+
{"version":3,"file":"use-trace-search.cjs.js","sources":["../../../../src/domains/traces/hooks/use-trace-search.ts"],"sourcesContent":["import { useDeferredValue, useMemo, useState } from 'react';\nimport type { SearchableSpan } from '../types';\nimport { filterSpansKeepingAncestors } from '../utils';\n\nexport interface UseTraceSearchResult {\n /** The immediate, user-facing input value. Bind this to the search field. */\n query: string;\n setQuery: (query: string) => void;\n /** Rows matching the deferred query. An empty query returns the input array reference. */\n results: SearchableSpan[];\n /**\n * Spans that matched somewhere other than their name — in `metadata`, `attributes` or\n * `error`. Their row shows no visible occurrence of the term, so the surface needs to say\n * why it is there. Ancestors kept only to preserve the hierarchy are not in here: they did\n * not match at all. Empty while the query is empty.\n */\n payloadOnlyMatchIds: Set<string>;\n /** True while the deferred value is behind `query` (the list is still catching up). */\n isPending: boolean;\n}\n\n/**\n * Client-side search over a flat span list.\n *\n * Matching is a substring test against `searchText`, the haystack built once per span when\n * the query resolved (see `toSearchableSpans`). That is what makes the open-ended `metadata`\n * and `error` payloads searchable: their shapes are unknown, so no fixed list of fields can\n * read them. Both sides are already lowercased, so the test is a bare `includes`.\n *\n * Filtering is keyed on a deferred copy of the query, so typing stays responsive while a\n * large list re-filters at lower priority — bind the input to `query`, not to the deferred\n * value. A matching span keeps its ancestors and its whole subtree, so the hierarchy stays\n * intact.\n *\n * `spans` is required. Resolving the loading/empty state is the caller's job — a component\n * holding a query result passes `data?.spans ?? []`.\n */\nexport function useTraceSearch(spans: SearchableSpan[]): UseTraceSearchResult {\n const [query, setQuery] = useState('');\n const deferredQuery = useDeferredValue(query);\n\n const { results, payloadOnlyMatchIds } = useMemo(() => {\n const term = deferredQuery.trim().toLowerCase();\n if (!term) return { results: spans, payloadOnlyMatchIds: new Set<string>() };\n\n const payloadOnly = new Set<string>();\n const filtered = filterSpansKeepingAncestors(spans, span => {\n if (!span.searchText.includes(term)) return false;\n // The name is the only part of a span the timeline paints, so a match it doesn't\n // contain came from the payload.\n if (!span.name.toLowerCase().includes(term)) payloadOnly.add(span.spanId);\n return true;\n });\n\n return { results: filtered, payloadOnlyMatchIds: payloadOnly };\n }, [spans, deferredQuery]);\n\n return { query, setQuery, results, payloadOnlyMatchIds, isPending: query !== deferredQuery };\n}\n"],"names":["useState","useDeferredValue","useMemo","filterSpansKeepingAncestors"],"mappings":";;;;;;;;AAqCO,SAAS,eAAe,KAAA,EAA+C;AAC5E,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,eAAS,EAAE,CAAA;AACrC,EAAA,MAAM,aAAA,GAAgBC,uBAAiB,KAAK,CAAA;AAE5C,EAAA,MAAM,EAAE,OAAA,EAAS,mBAAA,EAAoB,GAAIC,cAAQ,MAAM;AACrD,IAAA,MAAM,IAAA,GAAO,aAAA,CAAc,IAAA,EAAK,CAAE,WAAA,EAAY;AAC9C,IAAA,IAAI,CAAC,MAAM,OAAO,EAAE,SAAS,KAAA,EAAO,mBAAA,kBAAqB,IAAI,GAAA,EAAY,EAAE;AAE3E,IAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,IAAA,MAAM,QAAA,GAAWC,4EAAA,CAA4B,KAAA,EAAO,CAAA,IAAA,KAAQ;AAC1D,MAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,QAAA,CAAS,IAAI,GAAG,OAAO,KAAA;AAG5C,MAAA,IAAI,CAAC,IAAA,CAAK,IAAA,CAAK,WAAA,EAAY,CAAE,QAAA,CAAS,IAAI,CAAA,EAAG,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AACxE,MAAA,OAAO,IAAA;AAAA,IACT,CAAC,CAAA;AAED,IAAA,OAAO,EAAE,OAAA,EAAS,QAAA,EAAU,mBAAA,EAAqB,WAAA,EAAY;AAAA,EAC/D,CAAA,EAAG,CAAC,KAAA,EAAO,aAAa,CAAC,CAAA;AAEzB,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,SAAS,mBAAA,EAAqB,SAAA,EAAW,UAAU,aAAA,EAAc;AAC7F;;;;"}
|
|
@@ -5,12 +5,18 @@ import 'date-fns';
|
|
|
5
5
|
function useTraceSearch(spans) {
|
|
6
6
|
const [query, setQuery] = useState("");
|
|
7
7
|
const deferredQuery = useDeferredValue(query);
|
|
8
|
-
const results = useMemo(() => {
|
|
8
|
+
const { results, payloadOnlyMatchIds } = useMemo(() => {
|
|
9
9
|
const term = deferredQuery.trim().toLowerCase();
|
|
10
|
-
if (!term) return spans;
|
|
11
|
-
|
|
10
|
+
if (!term) return { results: spans, payloadOnlyMatchIds: /* @__PURE__ */ new Set() };
|
|
11
|
+
const payloadOnly = /* @__PURE__ */ new Set();
|
|
12
|
+
const filtered = filterSpansKeepingAncestors(spans, (span) => {
|
|
13
|
+
if (!span.searchText.includes(term)) return false;
|
|
14
|
+
if (!span.name.toLowerCase().includes(term)) payloadOnly.add(span.spanId);
|
|
15
|
+
return true;
|
|
16
|
+
});
|
|
17
|
+
return { results: filtered, payloadOnlyMatchIds: payloadOnly };
|
|
12
18
|
}, [spans, deferredQuery]);
|
|
13
|
-
return { query, setQuery, results, isPending: query !== deferredQuery };
|
|
19
|
+
return { query, setQuery, results, payloadOnlyMatchIds, isPending: query !== deferredQuery };
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export { useTraceSearch };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-trace-search.es.js","sources":["../../../../src/domains/traces/hooks/use-trace-search.ts"],"sourcesContent":["import { useDeferredValue, useMemo, useState } from 'react';\nimport type { SearchableSpan } from '../types';\nimport { filterSpansKeepingAncestors } from '../utils';\n\nexport interface UseTraceSearchResult {\n /** The immediate, user-facing input value. Bind this to the search field. */\n query: string;\n setQuery: (query: string) => void;\n /** Rows matching the deferred query. An empty query returns the input array reference. */\n results: SearchableSpan[];\n /** True while the deferred value is behind `query` (the list is still catching up). */\n isPending: boolean;\n}\n\n/**\n * Client-side search over a flat span list.\n *\n * Matching is a substring test against `searchText`, the haystack built once per span when\n * the query resolved (see `toSearchableSpans`). That is what makes the open-ended `metadata`\n * and `error` payloads searchable: their shapes are unknown, so no fixed list of fields can\n * read them. Both sides are already lowercased, so the test is a bare `includes`.\n *\n * Filtering is keyed on a deferred copy of the query, so typing stays responsive while a\n * large list re-filters at lower priority — bind the input to `query`, not to the deferred\n * value. A matching span keeps its ancestors and its whole subtree, so the hierarchy stays\n * intact.\n *\n * `spans` is required. Resolving the loading/empty state is the caller's job — a component\n * holding a query result passes `data?.spans ?? []`.\n */\nexport function useTraceSearch(spans: SearchableSpan[]): UseTraceSearchResult {\n const [query, setQuery] = useState('');\n const deferredQuery = useDeferredValue(query);\n\n const results = useMemo(() => {\n const term = deferredQuery.trim().toLowerCase();\n if (!term) return spans;\n
|
|
1
|
+
{"version":3,"file":"use-trace-search.es.js","sources":["../../../../src/domains/traces/hooks/use-trace-search.ts"],"sourcesContent":["import { useDeferredValue, useMemo, useState } from 'react';\nimport type { SearchableSpan } from '../types';\nimport { filterSpansKeepingAncestors } from '../utils';\n\nexport interface UseTraceSearchResult {\n /** The immediate, user-facing input value. Bind this to the search field. */\n query: string;\n setQuery: (query: string) => void;\n /** Rows matching the deferred query. An empty query returns the input array reference. */\n results: SearchableSpan[];\n /**\n * Spans that matched somewhere other than their name — in `metadata`, `attributes` or\n * `error`. Their row shows no visible occurrence of the term, so the surface needs to say\n * why it is there. Ancestors kept only to preserve the hierarchy are not in here: they did\n * not match at all. Empty while the query is empty.\n */\n payloadOnlyMatchIds: Set<string>;\n /** True while the deferred value is behind `query` (the list is still catching up). */\n isPending: boolean;\n}\n\n/**\n * Client-side search over a flat span list.\n *\n * Matching is a substring test against `searchText`, the haystack built once per span when\n * the query resolved (see `toSearchableSpans`). That is what makes the open-ended `metadata`\n * and `error` payloads searchable: their shapes are unknown, so no fixed list of fields can\n * read them. Both sides are already lowercased, so the test is a bare `includes`.\n *\n * Filtering is keyed on a deferred copy of the query, so typing stays responsive while a\n * large list re-filters at lower priority — bind the input to `query`, not to the deferred\n * value. A matching span keeps its ancestors and its whole subtree, so the hierarchy stays\n * intact.\n *\n * `spans` is required. Resolving the loading/empty state is the caller's job — a component\n * holding a query result passes `data?.spans ?? []`.\n */\nexport function useTraceSearch(spans: SearchableSpan[]): UseTraceSearchResult {\n const [query, setQuery] = useState('');\n const deferredQuery = useDeferredValue(query);\n\n const { results, payloadOnlyMatchIds } = useMemo(() => {\n const term = deferredQuery.trim().toLowerCase();\n if (!term) return { results: spans, payloadOnlyMatchIds: new Set<string>() };\n\n const payloadOnly = new Set<string>();\n const filtered = filterSpansKeepingAncestors(spans, span => {\n if (!span.searchText.includes(term)) return false;\n // The name is the only part of a span the timeline paints, so a match it doesn't\n // contain came from the payload.\n if (!span.name.toLowerCase().includes(term)) payloadOnly.add(span.spanId);\n return true;\n });\n\n return { results: filtered, payloadOnlyMatchIds: payloadOnly };\n }, [spans, deferredQuery]);\n\n return { query, setQuery, results, payloadOnlyMatchIds, isPending: query !== deferredQuery };\n}\n"],"names":[],"mappings":";;;;AAqCO,SAAS,eAAe,KAAA,EAA+C;AAC5E,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAS,EAAE,CAAA;AACrC,EAAA,MAAM,aAAA,GAAgB,iBAAiB,KAAK,CAAA;AAE5C,EAAA,MAAM,EAAE,OAAA,EAAS,mBAAA,EAAoB,GAAI,QAAQ,MAAM;AACrD,IAAA,MAAM,IAAA,GAAO,aAAA,CAAc,IAAA,EAAK,CAAE,WAAA,EAAY;AAC9C,IAAA,IAAI,CAAC,MAAM,OAAO,EAAE,SAAS,KAAA,EAAO,mBAAA,kBAAqB,IAAI,GAAA,EAAY,EAAE;AAE3E,IAAA,MAAM,WAAA,uBAAkB,GAAA,EAAY;AACpC,IAAA,MAAM,QAAA,GAAW,2BAAA,CAA4B,KAAA,EAAO,CAAA,IAAA,KAAQ;AAC1D,MAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,QAAA,CAAS,IAAI,GAAG,OAAO,KAAA;AAG5C,MAAA,IAAI,CAAC,IAAA,CAAK,IAAA,CAAK,WAAA,EAAY,CAAE,QAAA,CAAS,IAAI,CAAA,EAAG,WAAA,CAAY,GAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AACxE,MAAA,OAAO,IAAA;AAAA,IACT,CAAC,CAAA;AAED,IAAA,OAAO,EAAE,OAAA,EAAS,QAAA,EAAU,mBAAA,EAAqB,WAAA,EAAY;AAAA,EAC/D,CAAA,EAAG,CAAC,KAAA,EAAO,aAAa,CAAC,CAAA;AAEzB,EAAA,OAAO,EAAE,KAAA,EAAO,QAAA,EAAU,SAAS,mBAAA,EAAqB,SAAA,EAAW,UAAU,aAAA,EAAc;AAC7F;;;;"}
|
|
@@ -58,7 +58,7 @@ function serializeTraceColumnPreferences(preferences) {
|
|
|
58
58
|
}
|
|
59
59
|
function buildTraceListColumns(preferences) {
|
|
60
60
|
const visible = new Set(preferences.visibleColumns);
|
|
61
|
-
const columns = ["
|
|
61
|
+
const columns = ["11rem", visible.has("input") ? "14rem" : "minmax(8rem,1fr)"];
|
|
62
62
|
if (visible.has("input")) columns.push("minmax(8rem,1fr)");
|
|
63
63
|
if (visible.has("entity")) columns.push("14rem");
|
|
64
64
|
columns.push("6rem");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-list-columns.cjs.js","sources":["../../../src/domains/traces/trace-list-columns.ts"],"sourcesContent":["export const TRACE_OPTIONAL_COLUMNS = [\n 'input',\n 'entity',\n 'duration',\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const;\n\nexport type TraceOptionalColumn = (typeof TRACE_OPTIONAL_COLUMNS)[number];\n\nexport const TRACE_USAGE_COLUMNS = [\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const satisfies readonly TraceOptionalColumn[];\n\nexport type TraceColumnPreferences = {\n readonly visibleColumns: readonly TraceOptionalColumn[];\n readonly metadataKeys: readonly string[];\n};\n\nexport type TraceUsageSummary = {\n inputTokens?: number;\n outputTokens?: number;\n estimatedCost?: number;\n costUnit?: string;\n};\n\nexport const DEFAULT_TRACE_COLUMN_PREFERENCES: TraceColumnPreferences = {\n visibleColumns: ['input', 'entity'],\n metadataKeys: [],\n};\n\nconst TRACE_COLUMN_PREFERENCES_VERSION = 1;\nconst TRACE_COLUMN_SET = new Set<string>(TRACE_OPTIONAL_COLUMNS);\nconst TRACE_USAGE_COLUMN_SET = new Set<TraceOptionalColumn>(TRACE_USAGE_COLUMNS);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isTraceOptionalColumn(value: unknown): value is TraceOptionalColumn {\n return typeof value === 'string' && TRACE_COLUMN_SET.has(value);\n}\n\nfunction uniqueMetadataKeys(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n\n const keys = value\n .filter(key => typeof key === 'string')\n .map(key => key.trim())\n .filter(Boolean);\n\n return [...new Set(keys)];\n}\n\nexport function parseTraceColumnPreferences(serialized: string | undefined): TraceColumnPreferences {\n if (!serialized) return DEFAULT_TRACE_COLUMN_PREFERENCES;\n\n try {\n const parsed: unknown = JSON.parse(serialized);\n if (!isRecord(parsed) || parsed.version !== TRACE_COLUMN_PREFERENCES_VERSION) {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n\n const visibleColumns = Array.isArray(parsed.visibleColumns)\n ? [...new Set(parsed.visibleColumns.filter(isTraceOptionalColumn))]\n : [...DEFAULT_TRACE_COLUMN_PREFERENCES.visibleColumns];\n\n return {\n visibleColumns,\n metadataKeys: uniqueMetadataKeys(parsed.metadataKeys),\n };\n } catch {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n}\n\nexport function serializeTraceColumnPreferences(preferences: TraceColumnPreferences): string {\n return JSON.stringify({\n version: TRACE_COLUMN_PREFERENCES_VERSION,\n visibleColumns: preferences.visibleColumns,\n metadataKeys: preferences.metadataKeys,\n });\n}\n\nexport function buildTraceListColumns(preferences: TraceColumnPreferences): string {\n const visible = new Set(preferences.visibleColumns);\n const columns = ['
|
|
1
|
+
{"version":3,"file":"trace-list-columns.cjs.js","sources":["../../../src/domains/traces/trace-list-columns.ts"],"sourcesContent":["export const TRACE_OPTIONAL_COLUMNS = [\n 'input',\n 'entity',\n 'duration',\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const;\n\nexport type TraceOptionalColumn = (typeof TRACE_OPTIONAL_COLUMNS)[number];\n\nexport const TRACE_USAGE_COLUMNS = [\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const satisfies readonly TraceOptionalColumn[];\n\nexport type TraceColumnPreferences = {\n readonly visibleColumns: readonly TraceOptionalColumn[];\n readonly metadataKeys: readonly string[];\n};\n\nexport type TraceUsageSummary = {\n inputTokens?: number;\n outputTokens?: number;\n estimatedCost?: number;\n costUnit?: string;\n};\n\nexport const DEFAULT_TRACE_COLUMN_PREFERENCES: TraceColumnPreferences = {\n visibleColumns: ['input', 'entity'],\n metadataKeys: [],\n};\n\nconst TRACE_COLUMN_PREFERENCES_VERSION = 1;\nconst TRACE_COLUMN_SET = new Set<string>(TRACE_OPTIONAL_COLUMNS);\nconst TRACE_USAGE_COLUMN_SET = new Set<TraceOptionalColumn>(TRACE_USAGE_COLUMNS);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isTraceOptionalColumn(value: unknown): value is TraceOptionalColumn {\n return typeof value === 'string' && TRACE_COLUMN_SET.has(value);\n}\n\nfunction uniqueMetadataKeys(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n\n const keys = value\n .filter(key => typeof key === 'string')\n .map(key => key.trim())\n .filter(Boolean);\n\n return [...new Set(keys)];\n}\n\nexport function parseTraceColumnPreferences(serialized: string | undefined): TraceColumnPreferences {\n if (!serialized) return DEFAULT_TRACE_COLUMN_PREFERENCES;\n\n try {\n const parsed: unknown = JSON.parse(serialized);\n if (!isRecord(parsed) || parsed.version !== TRACE_COLUMN_PREFERENCES_VERSION) {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n\n const visibleColumns = Array.isArray(parsed.visibleColumns)\n ? [...new Set(parsed.visibleColumns.filter(isTraceOptionalColumn))]\n : [...DEFAULT_TRACE_COLUMN_PREFERENCES.visibleColumns];\n\n return {\n visibleColumns,\n metadataKeys: uniqueMetadataKeys(parsed.metadataKeys),\n };\n } catch {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n}\n\nexport function serializeTraceColumnPreferences(preferences: TraceColumnPreferences): string {\n return JSON.stringify({\n version: TRACE_COLUMN_PREFERENCES_VERSION,\n visibleColumns: preferences.visibleColumns,\n metadataKeys: preferences.metadataKeys,\n });\n}\n\nexport function buildTraceListColumns(preferences: TraceColumnPreferences): string {\n const visible = new Set(preferences.visibleColumns);\n // Name is bounded when Input is visible so Input (1fr) absorbs the free space;\n // without Input, Name is the flexible track that fills the grid.\n const columns = ['11rem', visible.has('input') ? '14rem' : 'minmax(8rem,1fr)'];\n\n if (visible.has('input')) columns.push('minmax(8rem,1fr)');\n if (visible.has('entity')) columns.push('14rem');\n\n columns.push('6rem');\n\n if (visible.has('duration')) columns.push('7rem');\n if (visible.has('inputTokens')) columns.push('8rem');\n if (visible.has('outputTokens')) columns.push('8rem');\n if (visible.has('estimatedCost')) columns.push('8rem');\n\n for (const _key of preferences.metadataKeys) {\n columns.push('minmax(8rem,14rem)');\n }\n\n return columns.join(' ');\n}\n\nexport function formatTraceMetadataValue(\n metadata: Record<string, unknown> | null | undefined,\n key: string,\n): string | undefined {\n if (!metadata || !Object.prototype.hasOwnProperty.call(metadata, key)) return undefined;\n const value = metadata[key];\n if (value == null) return undefined;\n\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);\n\n try {\n return JSON.stringify(value) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function hasTraceColumn(preferences: TraceColumnPreferences, column: TraceOptionalColumn): boolean {\n return preferences.visibleColumns.includes(column);\n}\n\nexport function hasTraceUsageColumn(preferences: TraceColumnPreferences): boolean {\n return preferences.visibleColumns.some(isTraceUsageColumn);\n}\n\nexport function isTraceUsageColumn(column: TraceOptionalColumn): boolean {\n return TRACE_USAGE_COLUMN_SET.has(column);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -54,7 +54,7 @@ function serializeTraceColumnPreferences(preferences) {
|
|
|
54
54
|
}
|
|
55
55
|
function buildTraceListColumns(preferences) {
|
|
56
56
|
const visible = new Set(preferences.visibleColumns);
|
|
57
|
-
const columns = ["
|
|
57
|
+
const columns = ["11rem", visible.has("input") ? "14rem" : "minmax(8rem,1fr)"];
|
|
58
58
|
if (visible.has("input")) columns.push("minmax(8rem,1fr)");
|
|
59
59
|
if (visible.has("entity")) columns.push("14rem");
|
|
60
60
|
columns.push("6rem");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-list-columns.es.js","sources":["../../../src/domains/traces/trace-list-columns.ts"],"sourcesContent":["export const TRACE_OPTIONAL_COLUMNS = [\n 'input',\n 'entity',\n 'duration',\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const;\n\nexport type TraceOptionalColumn = (typeof TRACE_OPTIONAL_COLUMNS)[number];\n\nexport const TRACE_USAGE_COLUMNS = [\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const satisfies readonly TraceOptionalColumn[];\n\nexport type TraceColumnPreferences = {\n readonly visibleColumns: readonly TraceOptionalColumn[];\n readonly metadataKeys: readonly string[];\n};\n\nexport type TraceUsageSummary = {\n inputTokens?: number;\n outputTokens?: number;\n estimatedCost?: number;\n costUnit?: string;\n};\n\nexport const DEFAULT_TRACE_COLUMN_PREFERENCES: TraceColumnPreferences = {\n visibleColumns: ['input', 'entity'],\n metadataKeys: [],\n};\n\nconst TRACE_COLUMN_PREFERENCES_VERSION = 1;\nconst TRACE_COLUMN_SET = new Set<string>(TRACE_OPTIONAL_COLUMNS);\nconst TRACE_USAGE_COLUMN_SET = new Set<TraceOptionalColumn>(TRACE_USAGE_COLUMNS);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isTraceOptionalColumn(value: unknown): value is TraceOptionalColumn {\n return typeof value === 'string' && TRACE_COLUMN_SET.has(value);\n}\n\nfunction uniqueMetadataKeys(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n\n const keys = value\n .filter(key => typeof key === 'string')\n .map(key => key.trim())\n .filter(Boolean);\n\n return [...new Set(keys)];\n}\n\nexport function parseTraceColumnPreferences(serialized: string | undefined): TraceColumnPreferences {\n if (!serialized) return DEFAULT_TRACE_COLUMN_PREFERENCES;\n\n try {\n const parsed: unknown = JSON.parse(serialized);\n if (!isRecord(parsed) || parsed.version !== TRACE_COLUMN_PREFERENCES_VERSION) {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n\n const visibleColumns = Array.isArray(parsed.visibleColumns)\n ? [...new Set(parsed.visibleColumns.filter(isTraceOptionalColumn))]\n : [...DEFAULT_TRACE_COLUMN_PREFERENCES.visibleColumns];\n\n return {\n visibleColumns,\n metadataKeys: uniqueMetadataKeys(parsed.metadataKeys),\n };\n } catch {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n}\n\nexport function serializeTraceColumnPreferences(preferences: TraceColumnPreferences): string {\n return JSON.stringify({\n version: TRACE_COLUMN_PREFERENCES_VERSION,\n visibleColumns: preferences.visibleColumns,\n metadataKeys: preferences.metadataKeys,\n });\n}\n\nexport function buildTraceListColumns(preferences: TraceColumnPreferences): string {\n const visible = new Set(preferences.visibleColumns);\n const columns = ['
|
|
1
|
+
{"version":3,"file":"trace-list-columns.es.js","sources":["../../../src/domains/traces/trace-list-columns.ts"],"sourcesContent":["export const TRACE_OPTIONAL_COLUMNS = [\n 'input',\n 'entity',\n 'duration',\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const;\n\nexport type TraceOptionalColumn = (typeof TRACE_OPTIONAL_COLUMNS)[number];\n\nexport const TRACE_USAGE_COLUMNS = [\n 'inputTokens',\n 'outputTokens',\n 'estimatedCost',\n] as const satisfies readonly TraceOptionalColumn[];\n\nexport type TraceColumnPreferences = {\n readonly visibleColumns: readonly TraceOptionalColumn[];\n readonly metadataKeys: readonly string[];\n};\n\nexport type TraceUsageSummary = {\n inputTokens?: number;\n outputTokens?: number;\n estimatedCost?: number;\n costUnit?: string;\n};\n\nexport const DEFAULT_TRACE_COLUMN_PREFERENCES: TraceColumnPreferences = {\n visibleColumns: ['input', 'entity'],\n metadataKeys: [],\n};\n\nconst TRACE_COLUMN_PREFERENCES_VERSION = 1;\nconst TRACE_COLUMN_SET = new Set<string>(TRACE_OPTIONAL_COLUMNS);\nconst TRACE_USAGE_COLUMN_SET = new Set<TraceOptionalColumn>(TRACE_USAGE_COLUMNS);\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isTraceOptionalColumn(value: unknown): value is TraceOptionalColumn {\n return typeof value === 'string' && TRACE_COLUMN_SET.has(value);\n}\n\nfunction uniqueMetadataKeys(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n\n const keys = value\n .filter(key => typeof key === 'string')\n .map(key => key.trim())\n .filter(Boolean);\n\n return [...new Set(keys)];\n}\n\nexport function parseTraceColumnPreferences(serialized: string | undefined): TraceColumnPreferences {\n if (!serialized) return DEFAULT_TRACE_COLUMN_PREFERENCES;\n\n try {\n const parsed: unknown = JSON.parse(serialized);\n if (!isRecord(parsed) || parsed.version !== TRACE_COLUMN_PREFERENCES_VERSION) {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n\n const visibleColumns = Array.isArray(parsed.visibleColumns)\n ? [...new Set(parsed.visibleColumns.filter(isTraceOptionalColumn))]\n : [...DEFAULT_TRACE_COLUMN_PREFERENCES.visibleColumns];\n\n return {\n visibleColumns,\n metadataKeys: uniqueMetadataKeys(parsed.metadataKeys),\n };\n } catch {\n return DEFAULT_TRACE_COLUMN_PREFERENCES;\n }\n}\n\nexport function serializeTraceColumnPreferences(preferences: TraceColumnPreferences): string {\n return JSON.stringify({\n version: TRACE_COLUMN_PREFERENCES_VERSION,\n visibleColumns: preferences.visibleColumns,\n metadataKeys: preferences.metadataKeys,\n });\n}\n\nexport function buildTraceListColumns(preferences: TraceColumnPreferences): string {\n const visible = new Set(preferences.visibleColumns);\n // Name is bounded when Input is visible so Input (1fr) absorbs the free space;\n // without Input, Name is the flexible track that fills the grid.\n const columns = ['11rem', visible.has('input') ? '14rem' : 'minmax(8rem,1fr)'];\n\n if (visible.has('input')) columns.push('minmax(8rem,1fr)');\n if (visible.has('entity')) columns.push('14rem');\n\n columns.push('6rem');\n\n if (visible.has('duration')) columns.push('7rem');\n if (visible.has('inputTokens')) columns.push('8rem');\n if (visible.has('outputTokens')) columns.push('8rem');\n if (visible.has('estimatedCost')) columns.push('8rem');\n\n for (const _key of preferences.metadataKeys) {\n columns.push('minmax(8rem,14rem)');\n }\n\n return columns.join(' ');\n}\n\nexport function formatTraceMetadataValue(\n metadata: Record<string, unknown> | null | undefined,\n key: string,\n): string | undefined {\n if (!metadata || !Object.prototype.hasOwnProperty.call(metadata, key)) return undefined;\n const value = metadata[key];\n if (value == null) return undefined;\n\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);\n\n try {\n return JSON.stringify(value) ?? undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function hasTraceColumn(preferences: TraceColumnPreferences, column: TraceOptionalColumn): boolean {\n return preferences.visibleColumns.includes(column);\n}\n\nexport function hasTraceUsageColumn(preferences: TraceColumnPreferences): boolean {\n return preferences.visibleColumns.some(isTraceUsageColumn);\n}\n\nexport function isTraceUsageColumn(column: TraceOptionalColumn): boolean {\n return TRACE_USAGE_COLUMN_SET.has(column);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.cjs.js","sources":["../../../src/domains/traces/types.ts"],"sourcesContent":["import type { EntityType } from '@mastra/core/observability';\nimport type { LightSpanRecord } from '@mastra/core/storage';\nimport type { ReactNode } from 'react';\n\n/**\n * A light span carrying a precomputed haystack of everything it holds.\n *\n * `LightSpanRecord` exposes searchable content across a fixed set of columns\n * plus open-ended payloads whose shape is unknown, so a search cannot read it\n * field by field. `searchText` is that content flattened once — see\n * `flattenToSearchText` — so filtering is a substring test per span instead of\n * a re-walk of every payload on every keystroke.\n *\n * It holds field names as well as values, so a span is findable by the shape of\n * its payload and not only by its content.\n */\nexport type SearchableSpan = LightSpanRecord & {\n searchText: string;\n};\n\nexport type UISpan = {\n id: string;\n name: string;\n type: string;\n latency: number;\n startTime: string;\n endTime?: string;\n spans?: UISpan[];\n parentSpanId?: string | null;\n};\n\nexport type UISpanStyle = {\n icon?: ReactNode;\n color?: string;\n label?: string;\n typePrefix: string;\n};\n\n// -- Trace filtering types ----------------------------------------------------\n\nexport type EntityOptions =\n | { value: string; label: string; type: EntityType.AGENT }\n | { value: string; label: string; type: EntityType.WORKFLOW_RUN }\n | { value: string; label: string; type: 'all' };\n\nexport type TraceDatePreset = 'all' | 'last-24h' | 'last-3d' | 'last-7d' | 'last-14d' | 'last-30d' | 'custom';\n\n/** Tab identifier for SpanDataPanelView. */\nexport type SpanTab = 'details' | 'feedback';\n\n/** Canonical list of context field IDs used for trace filtering and value extraction */\nexport const CONTEXT_FIELD_IDS = [\n 'environment',\n 'serviceName',\n 'source',\n 'scope',\n 'userId',\n 'organizationId',\n 'resourceId',\n 'runId',\n 'sessionId',\n 'threadId',\n 'requestId',\n 'experimentId',\n 'spanType',\n 'entityName',\n 'parentEntityType',\n 'parentEntityId',\n 'parentEntityName',\n 'rootEntityType',\n 'rootEntityId',\n 'rootEntityName',\n] as const;\n"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"types.cjs.js","sources":["../../../src/domains/traces/types.ts"],"sourcesContent":["import type { EntityType } from '@mastra/core/observability';\nimport type { LightSpanRecord } from '@mastra/core/storage';\nimport type { ReactNode } from 'react';\n\n/**\n * A light span carrying a precomputed haystack of everything it holds.\n *\n * `LightSpanRecord` exposes searchable content across a fixed set of columns\n * plus open-ended payloads whose shape is unknown, so a search cannot read it\n * field by field. `searchText` is that content flattened once — see\n * `flattenToSearchText` — so filtering is a substring test per span instead of\n * a re-walk of every payload on every keystroke.\n *\n * It holds field names as well as values, so a span is findable by the shape of\n * its payload and not only by its content.\n */\nexport type SearchableSpan = LightSpanRecord & {\n searchText: string;\n};\n\nexport type UISpan = {\n id: string;\n name: string;\n type: string;\n latency: number;\n startTime: string;\n endTime?: string;\n spans?: UISpan[];\n parentSpanId?: string | null;\n /**\n * Set when the span survived the active search because of its payload, not its name — the\n * timeline paints such a row differently since the term is nowhere on it.\n */\n matchedInPayloadOnly?: boolean;\n};\n\nexport type UISpanStyle = {\n icon?: ReactNode;\n color?: string;\n label?: string;\n typePrefix: string;\n};\n\n// -- Trace filtering types ----------------------------------------------------\n\nexport type EntityOptions =\n | { value: string; label: string; type: EntityType.AGENT }\n | { value: string; label: string; type: EntityType.WORKFLOW_RUN }\n | { value: string; label: string; type: 'all' };\n\nexport type TraceDatePreset = 'all' | 'last-24h' | 'last-3d' | 'last-7d' | 'last-14d' | 'last-30d' | 'custom';\n\n/** Tab identifier for SpanDataPanelView. */\nexport type SpanTab = 'details' | 'feedback';\n\n/** Canonical list of context field IDs used for trace filtering and value extraction */\nexport const CONTEXT_FIELD_IDS = [\n 'environment',\n 'serviceName',\n 'source',\n 'scope',\n 'userId',\n 'organizationId',\n 'resourceId',\n 'runId',\n 'sessionId',\n 'threadId',\n 'requestId',\n 'experimentId',\n 'spanType',\n 'entityName',\n 'parentEntityType',\n 'parentEntityId',\n 'parentEntityName',\n 'rootEntityType',\n 'rootEntityId',\n 'rootEntityName',\n] as const;\n"],"names":[],"mappings":";;;;AAwDO,MAAM,iBAAA,GAAoB;AAAA,EAC/B,aAAA;AAAA,EACA,aAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,gBAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EACA,cAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,kBAAA;AAAA,EACA,gBAAA;AAAA,EACA,kBAAA;AAAA,EACA,gBAAA;AAAA,EACA,cAAA;AAAA,EACA;AACF;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.es.js","sources":["../../../src/domains/traces/types.ts"],"sourcesContent":["import type { EntityType } from '@mastra/core/observability';\nimport type { LightSpanRecord } from '@mastra/core/storage';\nimport type { ReactNode } from 'react';\n\n/**\n * A light span carrying a precomputed haystack of everything it holds.\n *\n * `LightSpanRecord` exposes searchable content across a fixed set of columns\n * plus open-ended payloads whose shape is unknown, so a search cannot read it\n * field by field. `searchText` is that content flattened once — see\n * `flattenToSearchText` — so filtering is a substring test per span instead of\n * a re-walk of every payload on every keystroke.\n *\n * It holds field names as well as values, so a span is findable by the shape of\n * its payload and not only by its content.\n */\nexport type SearchableSpan = LightSpanRecord & {\n searchText: string;\n};\n\nexport type UISpan = {\n id: string;\n name: string;\n type: string;\n latency: number;\n startTime: string;\n endTime?: string;\n spans?: UISpan[];\n parentSpanId?: string | null;\n};\n\nexport type UISpanStyle = {\n icon?: ReactNode;\n color?: string;\n label?: string;\n typePrefix: string;\n};\n\n// -- Trace filtering types ----------------------------------------------------\n\nexport type EntityOptions =\n | { value: string; label: string; type: EntityType.AGENT }\n | { value: string; label: string; type: EntityType.WORKFLOW_RUN }\n | { value: string; label: string; type: 'all' };\n\nexport type TraceDatePreset = 'all' | 'last-24h' | 'last-3d' | 'last-7d' | 'last-14d' | 'last-30d' | 'custom';\n\n/** Tab identifier for SpanDataPanelView. */\nexport type SpanTab = 'details' | 'feedback';\n\n/** Canonical list of context field IDs used for trace filtering and value extraction */\nexport const CONTEXT_FIELD_IDS = [\n 'environment',\n 'serviceName',\n 'source',\n 'scope',\n 'userId',\n 'organizationId',\n 'resourceId',\n 'runId',\n 'sessionId',\n 'threadId',\n 'requestId',\n 'experimentId',\n 'spanType',\n 'entityName',\n 'parentEntityType',\n 'parentEntityId',\n 'parentEntityName',\n 'rootEntityType',\n 'rootEntityId',\n 'rootEntityName',\n] as const;\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.es.js","sources":["../../../src/domains/traces/types.ts"],"sourcesContent":["import type { EntityType } from '@mastra/core/observability';\nimport type { LightSpanRecord } from '@mastra/core/storage';\nimport type { ReactNode } from 'react';\n\n/**\n * A light span carrying a precomputed haystack of everything it holds.\n *\n * `LightSpanRecord` exposes searchable content across a fixed set of columns\n * plus open-ended payloads whose shape is unknown, so a search cannot read it\n * field by field. `searchText` is that content flattened once — see\n * `flattenToSearchText` — so filtering is a substring test per span instead of\n * a re-walk of every payload on every keystroke.\n *\n * It holds field names as well as values, so a span is findable by the shape of\n * its payload and not only by its content.\n */\nexport type SearchableSpan = LightSpanRecord & {\n searchText: string;\n};\n\nexport type UISpan = {\n id: string;\n name: string;\n type: string;\n latency: number;\n startTime: string;\n endTime?: string;\n spans?: UISpan[];\n parentSpanId?: string | null;\n /**\n * Set when the span survived the active search because of its payload, not its name — the\n * timeline paints such a row differently since the term is nowhere on it.\n */\n matchedInPayloadOnly?: boolean;\n};\n\nexport type UISpanStyle = {\n icon?: ReactNode;\n color?: string;\n label?: string;\n typePrefix: string;\n};\n\n// -- Trace filtering types ----------------------------------------------------\n\nexport type EntityOptions =\n | { value: string; label: string; type: EntityType.AGENT }\n | { value: string; label: string; type: EntityType.WORKFLOW_RUN }\n | { value: string; label: string; type: 'all' };\n\nexport type TraceDatePreset = 'all' | 'last-24h' | 'last-3d' | 'last-7d' | 'last-14d' | 'last-30d' | 'custom';\n\n/** Tab identifier for SpanDataPanelView. */\nexport type SpanTab = 'details' | 'feedback';\n\n/** Canonical list of context field IDs used for trace filtering and value extraction */\nexport const CONTEXT_FIELD_IDS = [\n 'environment',\n 'serviceName',\n 'source',\n 'scope',\n 'userId',\n 'organizationId',\n 'resourceId',\n 'runId',\n 'sessionId',\n 'threadId',\n 'requestId',\n 'experimentId',\n 'spanType',\n 'entityName',\n 'parentEntityType',\n 'parentEntityId',\n 'parentEntityName',\n 'rootEntityType',\n 'rootEntityId',\n 'rootEntityName',\n] as const;\n"],"names":[],"mappings":"AAwDO,MAAM,iBAAA,GAAoB;AAAA,EAC/B,aAAA;AAAA,EACA,aAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,gBAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EACA,cAAA;AAAA,EACA,UAAA;AAAA,EACA,YAAA;AAAA,EACA,kBAAA;AAAA,EACA,gBAAA;AAAA,EACA,kBAAA;AAAA,EACA,gBAAA;AAAA,EACA,cAAA;AAAA,EACA;AACF;;;;"}
|
|
@@ -7,7 +7,7 @@ const lucideReact = require('lucide-react');
|
|
|
7
7
|
const React = require('react');
|
|
8
8
|
const ee_topics_utils = require('../utils.cjs.js');
|
|
9
9
|
const Button = require('../../../Button-DA5XLUon.cjs');
|
|
10
|
-
const dataList = require('../../../data-list-
|
|
10
|
+
const dataList = require('../../../data-list-CRDewYj9.cjs');
|
|
11
11
|
const useDataListKeyboard = require('../../../use-data-list-keyboard-k5D-WlNX.cjs');
|
|
12
12
|
const inputGroup = require('../../../input-group-4EQi2l7H.cjs');
|
|
13
13
|
|
|
@@ -3,7 +3,7 @@ import { SearchIcon } from 'lucide-react';
|
|
|
3
3
|
import { useState, useMemo } from 'react';
|
|
4
4
|
import { getVisibleTraceSummaries } from '../utils.es.js';
|
|
5
5
|
import { B as Button } from '../../../Button-vr_I4iME.js';
|
|
6
|
-
import { D as DataList } from '../../../data-list-
|
|
6
|
+
import { D as DataList } from '../../../data-list-C3tjx5Vb.js';
|
|
7
7
|
import { u as useDataListKeyboard } from '../../../use-data-list-keyboard-BzZmvqE_.js';
|
|
8
8
|
import { I as InputGroup, a as InputGroupAddon, c as InputGroupInput } from '../../../input-group-U2f3A4Ya.js';
|
|
9
9
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
4
|
+
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const hooks_useTextHighlight = require('./use-text-highlight.cjs.js');
|
|
7
|
+
|
|
8
|
+
function useScrollToFirstHighlight(search, resetKey) {
|
|
9
|
+
const [root, setRoot] = React.useState(null);
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
if (!root || search.trim().length < hooks_useTextHighlight.MIN_SEARCH_LENGTH) return;
|
|
12
|
+
let animationFrame = null;
|
|
13
|
+
let scrolledTo = null;
|
|
14
|
+
let scrolledToData = "";
|
|
15
|
+
const scan = () => {
|
|
16
|
+
const match = hooks_useTextHighlight.findFirstMatchTextNode(root, search);
|
|
17
|
+
if (!match) return;
|
|
18
|
+
scrolledTo = match;
|
|
19
|
+
scrolledToData = match.data;
|
|
20
|
+
const target = match.parentElement;
|
|
21
|
+
if (target && typeof target.scrollIntoView === "function") {
|
|
22
|
+
target.scrollIntoView({ block: "center" });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const scrolledContentIntact = () => scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;
|
|
26
|
+
scan();
|
|
27
|
+
const observer = new MutationObserver(() => {
|
|
28
|
+
if (animationFrame !== null) return;
|
|
29
|
+
animationFrame = requestAnimationFrame(() => {
|
|
30
|
+
animationFrame = null;
|
|
31
|
+
if (!scrolledContentIntact()) scan();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
observer.observe(root, { subtree: true, childList: true, characterData: true });
|
|
35
|
+
return () => {
|
|
36
|
+
observer.disconnect();
|
|
37
|
+
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
|
|
38
|
+
};
|
|
39
|
+
}, [root, search, resetKey]);
|
|
40
|
+
return { ref: setRoot };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
exports.useScrollToFirstHighlight = useScrollToFirstHighlight;
|
|
44
|
+
//# sourceMappingURL=use-scroll-to-first-highlight.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-scroll-to-first-highlight.cjs.js","sources":["../../src/hooks/use-scroll-to-first-highlight.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { MIN_SEARCH_LENGTH, findFirstMatchTextNode } from './use-text-highlight';\n\nexport interface UseScrollToFirstHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Scrolls the first text-highlight match inside the referenced subtree into view. The\n * CSS Custom Highlight API paints ranges without wrapping DOM nodes, so there is no\n * `<mark>` to scroll to — instead the first text node `useTextHighlight` would paint is\n * located (same matching rules) and its parent element is brought into view.\n *\n * Content often arrives after the effect runs: the span payload loads async, and when a\n * different span is selected the panel briefly still shows the previous span's DOM. So a\n * MutationObserver stays attached for the effect's lifetime (rescans coalesced to one per\n * frame) and re-scrolls whenever the previously scrolled-to node is gone or no longer\n * contains the term — i.e. the content it scrolled to was replaced. While the matched\n * node stays put, mutations never re-scroll, so the user can scroll freely afterwards.\n * Pass `resetKey` to re-trigger the scroll for the same query, e.g. when a different\n * span is selected.\n */\nexport function useScrollToFirstHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n resetKey?: unknown,\n): UseScrollToFirstHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useEffect(() => {\n if (!root || search.trim().length < MIN_SEARCH_LENGTH) return;\n\n let animationFrame: number | null = null;\n let scrolledTo: Text | null = null;\n let scrolledToData = '';\n\n const scan = () => {\n const match = findFirstMatchTextNode(root, search);\n if (!match) return;\n\n scrolledTo = match;\n scrolledToData = match.data;\n const target = match.parentElement;\n // jsdom guard: scrollIntoView is not implemented there.\n if (target && typeof target.scrollIntoView === 'function') {\n target.scrollIntoView({ block: 'center' });\n }\n };\n\n // The scrolled-to content survived the mutation iff the node is still attached with\n // its text unchanged. React reuses text nodes and edits characterData in place, so\n // swapped-in content (e.g. another span's payload matching the same term) shows up\n // as a data change on the same node, not as a removal.\n const scrolledContentIntact = () =>\n scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;\n\n scan();\n\n const observer = new MutationObserver(() => {\n if (animationFrame !== null) return;\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n if (!scrolledContentIntact()) scan();\n });\n });\n observer.observe(root, { subtree: true, childList: true, characterData: true });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n };\n }, [root, search, resetKey]);\n\n return { ref: setRoot };\n}\n"],"names":["useState","useEffect","MIN_SEARCH_LENGTH","findFirstMatchTextNode"],"mappings":";;;;;;;AAwBO,SAAS,yBAAA,CACd,QACA,QAAA,EAC2C;AAC3C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIA,eAA0B,IAAI,CAAA;AAEtD,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,IAAQ,MAAA,CAAO,IAAA,EAAK,CAAE,SAASC,wCAAA,EAAmB;AAEvD,IAAA,IAAI,cAAA,GAAgC,IAAA;AACpC,IAAA,IAAI,UAAA,GAA0B,IAAA;AAC9B,IAAA,IAAI,cAAA,GAAiB,EAAA;AAErB,IAAA,MAAM,OAAO,MAAM;AACjB,MAAA,MAAM,KAAA,GAAQC,6CAAA,CAAuB,IAAA,EAAM,MAAM,CAAA;AACjD,MAAA,IAAI,CAAC,KAAA,EAAO;AAEZ,MAAA,UAAA,GAAa,KAAA;AACb,MAAA,cAAA,GAAiB,KAAA,CAAM,IAAA;AACvB,MAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AAErB,MAAA,IAAI,MAAA,IAAU,OAAO,MAAA,CAAO,cAAA,KAAmB,UAAA,EAAY;AACzD,QAAA,MAAA,CAAO,cAAA,CAAe,EAAE,KAAA,EAAO,QAAA,EAAU,CAAA;AAAA,MAC3C;AAAA,IACF,CAAA;AAMA,IAAA,MAAM,wBAAwB,MAC5B,UAAA,KAAe,QAAQ,UAAA,CAAW,WAAA,IAAe,WAAW,IAAA,KAAS,cAAA;AAEvE,IAAA,IAAA,EAAK;AAEL,IAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,MAAA,IAAI,mBAAmB,IAAA,EAAM;AAE7B,MAAA,cAAA,GAAiB,sBAAsB,MAAM;AAC3C,QAAA,cAAA,GAAiB,IAAA;AACjB,QAAA,IAAI,CAAC,qBAAA,EAAsB,EAAG,IAAA,EAAK;AAAA,MACrC,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,QAAA,CAAS,OAAA,CAAQ,MAAM,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,CAAA;AAE9E,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,UAAA,EAAW;AACpB,MAAA,IAAI,cAAA,KAAmB,IAAA,EAAM,oBAAA,CAAqB,cAAc,CAAA;AAAA,IAClE,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAA,EAAM,MAAA,EAAQ,QAAQ,CAAC,CAAA;AAE3B,EAAA,OAAO,EAAE,KAAK,OAAA,EAAQ;AACxB;;;;"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { MIN_SEARCH_LENGTH, findFirstMatchTextNode } from './use-text-highlight.es.js';
|
|
3
|
+
|
|
4
|
+
function useScrollToFirstHighlight(search, resetKey) {
|
|
5
|
+
const [root, setRoot] = useState(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!root || search.trim().length < MIN_SEARCH_LENGTH) return;
|
|
8
|
+
let animationFrame = null;
|
|
9
|
+
let scrolledTo = null;
|
|
10
|
+
let scrolledToData = "";
|
|
11
|
+
const scan = () => {
|
|
12
|
+
const match = findFirstMatchTextNode(root, search);
|
|
13
|
+
if (!match) return;
|
|
14
|
+
scrolledTo = match;
|
|
15
|
+
scrolledToData = match.data;
|
|
16
|
+
const target = match.parentElement;
|
|
17
|
+
if (target && typeof target.scrollIntoView === "function") {
|
|
18
|
+
target.scrollIntoView({ block: "center" });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const scrolledContentIntact = () => scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;
|
|
22
|
+
scan();
|
|
23
|
+
const observer = new MutationObserver(() => {
|
|
24
|
+
if (animationFrame !== null) return;
|
|
25
|
+
animationFrame = requestAnimationFrame(() => {
|
|
26
|
+
animationFrame = null;
|
|
27
|
+
if (!scrolledContentIntact()) scan();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
observer.observe(root, { subtree: true, childList: true, characterData: true });
|
|
31
|
+
return () => {
|
|
32
|
+
observer.disconnect();
|
|
33
|
+
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
|
|
34
|
+
};
|
|
35
|
+
}, [root, search, resetKey]);
|
|
36
|
+
return { ref: setRoot };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { useScrollToFirstHighlight };
|
|
40
|
+
//# sourceMappingURL=use-scroll-to-first-highlight.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-scroll-to-first-highlight.es.js","sources":["../../src/hooks/use-scroll-to-first-highlight.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { MIN_SEARCH_LENGTH, findFirstMatchTextNode } from './use-text-highlight';\n\nexport interface UseScrollToFirstHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Scrolls the first text-highlight match inside the referenced subtree into view. The\n * CSS Custom Highlight API paints ranges without wrapping DOM nodes, so there is no\n * `<mark>` to scroll to — instead the first text node `useTextHighlight` would paint is\n * located (same matching rules) and its parent element is brought into view.\n *\n * Content often arrives after the effect runs: the span payload loads async, and when a\n * different span is selected the panel briefly still shows the previous span's DOM. So a\n * MutationObserver stays attached for the effect's lifetime (rescans coalesced to one per\n * frame) and re-scrolls whenever the previously scrolled-to node is gone or no longer\n * contains the term — i.e. the content it scrolled to was replaced. While the matched\n * node stays put, mutations never re-scroll, so the user can scroll freely afterwards.\n * Pass `resetKey` to re-trigger the scroll for the same query, e.g. when a different\n * span is selected.\n */\nexport function useScrollToFirstHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n resetKey?: unknown,\n): UseScrollToFirstHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useEffect(() => {\n if (!root || search.trim().length < MIN_SEARCH_LENGTH) return;\n\n let animationFrame: number | null = null;\n let scrolledTo: Text | null = null;\n let scrolledToData = '';\n\n const scan = () => {\n const match = findFirstMatchTextNode(root, search);\n if (!match) return;\n\n scrolledTo = match;\n scrolledToData = match.data;\n const target = match.parentElement;\n // jsdom guard: scrollIntoView is not implemented there.\n if (target && typeof target.scrollIntoView === 'function') {\n target.scrollIntoView({ block: 'center' });\n }\n };\n\n // The scrolled-to content survived the mutation iff the node is still attached with\n // its text unchanged. React reuses text nodes and edits characterData in place, so\n // swapped-in content (e.g. another span's payload matching the same term) shows up\n // as a data change on the same node, not as a removal.\n const scrolledContentIntact = () =>\n scrolledTo !== null && scrolledTo.isConnected && scrolledTo.data === scrolledToData;\n\n scan();\n\n const observer = new MutationObserver(() => {\n if (animationFrame !== null) return;\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n if (!scrolledContentIntact()) scan();\n });\n });\n observer.observe(root, { subtree: true, childList: true, characterData: true });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n };\n }, [root, search, resetKey]);\n\n return { ref: setRoot };\n}\n"],"names":[],"mappings":";;;AAwBO,SAAS,yBAAA,CACd,QACA,QAAA,EAC2C;AAC3C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAA0B,IAAI,CAAA;AAEtD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,IAAQ,MAAA,CAAO,IAAA,EAAK,CAAE,SAAS,iBAAA,EAAmB;AAEvD,IAAA,IAAI,cAAA,GAAgC,IAAA;AACpC,IAAA,IAAI,UAAA,GAA0B,IAAA;AAC9B,IAAA,IAAI,cAAA,GAAiB,EAAA;AAErB,IAAA,MAAM,OAAO,MAAM;AACjB,MAAA,MAAM,KAAA,GAAQ,sBAAA,CAAuB,IAAA,EAAM,MAAM,CAAA;AACjD,MAAA,IAAI,CAAC,KAAA,EAAO;AAEZ,MAAA,UAAA,GAAa,KAAA;AACb,MAAA,cAAA,GAAiB,KAAA,CAAM,IAAA;AACvB,MAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AAErB,MAAA,IAAI,MAAA,IAAU,OAAO,MAAA,CAAO,cAAA,KAAmB,UAAA,EAAY;AACzD,QAAA,MAAA,CAAO,cAAA,CAAe,EAAE,KAAA,EAAO,QAAA,EAAU,CAAA;AAAA,MAC3C;AAAA,IACF,CAAA;AAMA,IAAA,MAAM,wBAAwB,MAC5B,UAAA,KAAe,QAAQ,UAAA,CAAW,WAAA,IAAe,WAAW,IAAA,KAAS,cAAA;AAEvE,IAAA,IAAA,EAAK;AAEL,IAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,MAAM;AAC1C,MAAA,IAAI,mBAAmB,IAAA,EAAM;AAE7B,MAAA,cAAA,GAAiB,sBAAsB,MAAM;AAC3C,QAAA,cAAA,GAAiB,IAAA;AACjB,QAAA,IAAI,CAAC,qBAAA,EAAsB,EAAG,IAAA,EAAK;AAAA,MACrC,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,QAAA,CAAS,OAAA,CAAQ,MAAM,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,IAAA,EAAM,aAAA,EAAe,IAAA,EAAM,CAAA;AAE9E,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,UAAA,EAAW;AACpB,MAAA,IAAI,cAAA,KAAmB,IAAA,EAAM,oBAAA,CAAqB,cAAc,CAAA;AAAA,IAClE,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAA,EAAM,MAAA,EAAQ,QAAQ,CAAC,CAAA;AAE3B,EAAA,OAAO,EAAE,KAAK,OAAA,EAAQ;AACxB;;;;"}
|
|
@@ -6,30 +6,67 @@ require('../use-text-highlight.css');const React = require('react');
|
|
|
6
6
|
const hooks_useIsomorphicLayoutEffect = require('./use-isomorphic-layout-effect.cjs.js');
|
|
7
7
|
|
|
8
8
|
const HIGHLIGHT_NAME = "search-result";
|
|
9
|
+
const INDIRECT_HIGHLIGHT_NAME = "search-result-indirect";
|
|
9
10
|
const MIN_SEARCH_LENGTH = 2;
|
|
10
11
|
const INCLUDED_SELECTOR = "[data-highlight]";
|
|
12
|
+
const INDIRECT_SELECTOR = "[data-highlight-indirect]";
|
|
11
13
|
function escapeRegExp(value) {
|
|
12
14
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
15
|
}
|
|
16
|
+
function findFirstMatchTextNode(root, search) {
|
|
17
|
+
const regex = new RegExp(escapeRegExp(search), "iu");
|
|
18
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
19
|
+
acceptNode(node) {
|
|
20
|
+
const parent = node.parentElement;
|
|
21
|
+
return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
while (walker.nextNode()) {
|
|
25
|
+
const textNode = walker.currentNode;
|
|
26
|
+
const parent = textNode.parentElement;
|
|
27
|
+
if (parent.closest(INDIRECT_SELECTOR)) {
|
|
28
|
+
if (textNode.data.length > 0) return textNode;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (regex.test(textNode.data)) return textNode;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
14
35
|
function useTextHighlight(search) {
|
|
15
36
|
const [root, setRoot] = React.useState(null);
|
|
16
37
|
hooks_useIsomorphicLayoutEffect.useIsomorphicLayoutEffect(() => {
|
|
17
38
|
if (!root || search.trim().length < MIN_SEARCH_LENGTH || typeof CSS === "undefined" || !("highlights" in CSS) || typeof StaticRange === "undefined") {
|
|
18
39
|
CSS?.highlights?.delete(HIGHLIGHT_NAME);
|
|
40
|
+
CSS?.highlights?.delete(INDIRECT_HIGHLIGHT_NAME);
|
|
19
41
|
return;
|
|
20
42
|
}
|
|
21
43
|
let animationFrame = null;
|
|
22
44
|
const updateHighlight = () => {
|
|
23
45
|
const ranges = [];
|
|
46
|
+
const indirectRanges = [];
|
|
24
47
|
const regex = new RegExp(escapeRegExp(search), "giu");
|
|
25
48
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
|
26
49
|
acceptNode(node) {
|
|
27
50
|
const parent = node.parentElement;
|
|
28
|
-
return parent.closest(INCLUDED_SELECTOR) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
51
|
+
return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
|
29
52
|
}
|
|
30
53
|
});
|
|
31
54
|
while (walker.nextNode()) {
|
|
32
55
|
const textNode = walker.currentNode;
|
|
56
|
+
const parent = textNode.parentElement;
|
|
57
|
+
if (parent.closest(INDIRECT_SELECTOR)) {
|
|
58
|
+
if (textNode.data.length > 0) {
|
|
59
|
+
indirectRanges.push(
|
|
60
|
+
new StaticRange({
|
|
61
|
+
startContainer: textNode,
|
|
62
|
+
startOffset: 0,
|
|
63
|
+
endContainer: textNode,
|
|
64
|
+
endOffset: textNode.data.length
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
33
70
|
regex.lastIndex = 0;
|
|
34
71
|
let match;
|
|
35
72
|
while ((match = regex.exec(textNode.data)) !== null) {
|
|
@@ -44,6 +81,7 @@ function useTextHighlight(search) {
|
|
|
44
81
|
}
|
|
45
82
|
}
|
|
46
83
|
CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges));
|
|
84
|
+
CSS.highlights.set(INDIRECT_HIGHLIGHT_NAME, new Highlight(...indirectRanges));
|
|
47
85
|
};
|
|
48
86
|
const scheduleUpdate = () => {
|
|
49
87
|
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
|
|
@@ -54,15 +92,24 @@ function useTextHighlight(search) {
|
|
|
54
92
|
};
|
|
55
93
|
updateHighlight();
|
|
56
94
|
const observer = new MutationObserver(scheduleUpdate);
|
|
57
|
-
observer.observe(root, {
|
|
95
|
+
observer.observe(root, {
|
|
96
|
+
subtree: true,
|
|
97
|
+
childList: true,
|
|
98
|
+
characterData: true,
|
|
99
|
+
attributes: true,
|
|
100
|
+
attributeFilter: ["data-highlight", "data-highlight-indirect"]
|
|
101
|
+
});
|
|
58
102
|
return () => {
|
|
59
103
|
observer.disconnect();
|
|
60
104
|
if (animationFrame !== null) cancelAnimationFrame(animationFrame);
|
|
61
105
|
CSS.highlights.delete(HIGHLIGHT_NAME);
|
|
106
|
+
CSS.highlights.delete(INDIRECT_HIGHLIGHT_NAME);
|
|
62
107
|
};
|
|
63
108
|
}, [root, search]);
|
|
64
109
|
return { ref: setRoot };
|
|
65
110
|
}
|
|
66
111
|
|
|
112
|
+
exports.MIN_SEARCH_LENGTH = MIN_SEARCH_LENGTH;
|
|
113
|
+
exports.findFirstMatchTextNode = findFirstMatchTextNode;
|
|
67
114
|
exports.useTextHighlight = useTextHighlight;
|
|
68
115
|
//# sourceMappingURL=use-text-highlight.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-text-highlight.cjs.js","sources":["../../src/hooks/use-text-highlight.ts"],"sourcesContent":["import { useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\nimport './use-text-highlight.css';\n\n/** Single registry entry, so only one search surface can be highlighted at a time. */\nconst HIGHLIGHT_NAME = 'search-result';\n\n/**\n * A single character matches almost everywhere, which paints noise instead of results.\n * Highlighting only starts once the term is discriminating enough.\n */\nconst MIN_SEARCH_LENGTH = 2;\n\n/**\n * Opt-in marker: only text inside a `data-highlight` subtree can be painted. Highlighting\n * is a claim about which text is searchable, so surfaces state it explicitly rather than\n * having every piece of surrounding chrome remember to opt out.\n */\nconst INCLUDED_SELECTOR = '[data-highlight]';\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport interface UseTextHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Paints every occurrence of `search` inside the referenced subtree using the CSS Custom\n * Highlight API. Only text under an element carrying `data-highlight` is painted, so a\n * surface names its searchable regions and everything else — headers, labels, metadata —\n * is left alone by default. The DOM is never mutated — no wrapper elements are injected — so text\n * selection, copy/paste and virtualised renderers are unaffected. Styling lives in\n * `use-text-highlight.css` (`::highlight(search-result)`).\n *\n * Terms shorter than two characters are ignored — they match too much to be useful.\n * Matching is case-insensitive and literal (the term is escaped, not a pattern). The\n * subtree is re-scanned on content changes, coalesced to one scan per frame. Browsers\n * without the API simply render nothing highlighted.\n */\nexport function useTextHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n): UseTextHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useIsomorphicLayoutEffect(() => {\n if (\n !root ||\n search.trim().length < MIN_SEARCH_LENGTH ||\n typeof CSS === 'undefined' ||\n !('highlights' in CSS) ||\n typeof StaticRange === 'undefined'\n ) {\n CSS?.highlights?.delete(HIGHLIGHT_NAME);\n return;\n }\n\n let animationFrame: number | null = null;\n\n const updateHighlight = () => {\n const ranges: StaticRange[] = [];\n const regex = new RegExp(escapeRegExp(search), 'giu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n // Inside a rooted walker every text node has a parent element.\n const parent = node.parentElement as HTMLElement;\n return parent.closest(INCLUDED_SELECTOR) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n regex.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(textNode.data)) !== null) {\n ranges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: match.index,\n endContainer: textNode,\n endOffset: match.index + match[0].length,\n }),\n );\n }\n }\n\n CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges));\n };\n\n const scheduleUpdate = () => {\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n updateHighlight();\n });\n };\n\n updateHighlight();\n\n const observer = new MutationObserver(scheduleUpdate);\n observer.observe(root, { subtree: true, childList: true, characterData: true });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n CSS.highlights.delete(HIGHLIGHT_NAME);\n };\n }, [root, search]);\n\n return { ref: setRoot };\n}\n"],"names":["useState","useIsomorphicLayoutEffect"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-text-highlight.cjs.js","sources":["../../src/hooks/use-text-highlight.ts"],"sourcesContent":["import { useState } from 'react';\nimport type { RefCallback } from 'react';\n\nimport { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';\nimport './use-text-highlight.css';\n\n/** Single registry entry, so only one search surface can be highlighted at a time. */\nconst HIGHLIGHT_NAME = 'search-result';\n\n/**\n * Second registry entry for text that carries no occurrence of the term itself, but stands\n * for something elsewhere that does — a span name whose match hides in its metadata. Painted\n * in its own color so the two claims stay distinguishable.\n */\nconst INDIRECT_HIGHLIGHT_NAME = 'search-result-indirect';\n\n/**\n * A single character matches almost everywhere, which paints noise instead of results.\n * Highlighting only starts once the term is discriminating enough.\n */\nexport const MIN_SEARCH_LENGTH = 2;\n\n/**\n * Opt-in marker: only text inside a `data-highlight` subtree can be painted. Highlighting\n * is a claim about which text is searchable, so surfaces state it explicitly rather than\n * having every piece of surrounding chrome remember to opt out.\n */\nconst INCLUDED_SELECTOR = '[data-highlight]';\n\n/**\n * Opt-in marker for the indirect highlight: the whole text of such a subtree is painted\n * while a query is active, regardless of what the term is. Marking is the caller's decision\n * — it knows why the row survived the filter, this hook only knows the term.\n */\nconst INDIRECT_SELECTOR = '[data-highlight-indirect]';\n\nfunction escapeRegExp(value: string) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/**\n * The first text node the highlighter would paint under `root` for `search`, or `null`.\n * Mirrors the walker below exactly: only `data-highlight` / `data-highlight-indirect`\n * subtrees count; indirect text is painted whole, so any non-empty text node in one is a\n * hit; direct text needs a literal, case-insensitive occurrence of the term. Colocated\n * here so the scroll target (`useScrollToFirstHighlight`) can't drift from the paint.\n */\nexport function findFirstMatchTextNode(root: HTMLElement, search: string): Text | null {\n const regex = new RegExp(escapeRegExp(search), 'iu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) return textNode;\n continue;\n }\n\n if (regex.test(textNode.data)) return textNode;\n }\n\n return null;\n}\n\nexport interface UseTextHighlightResult<TElement extends HTMLElement> {\n ref: RefCallback<TElement>;\n}\n\n/**\n * Paints every occurrence of `search` inside the referenced subtree using the CSS Custom\n * Highlight API. Only text under an element carrying `data-highlight` is painted, so a\n * surface names its searchable regions and everything else — headers, labels, metadata —\n * is left alone by default. The DOM is never mutated — no wrapper elements are injected — so text\n * selection, copy/paste and virtualised renderers are unaffected. Styling lives in\n * `use-text-highlight.css` (`::highlight(search-result)`).\n *\n * Text under `data-highlight-indirect` is painted in full, in a second color\n * (`::highlight(search-result-indirect)`), whatever the term is. That is for labels standing\n * in for a match that lives somewhere not on screen — a span name whose hit is in its\n * metadata. It wins over `data-highlight` when both apply, so a text is never painted twice.\n *\n * Terms shorter than two characters are ignored — they match too much to be useful.\n * Matching is case-insensitive and literal (the term is escaped, not a pattern). The\n * subtree is re-scanned on content changes, coalesced to one scan per frame. Browsers\n * without the API simply render nothing highlighted.\n */\nexport function useTextHighlight<TElement extends HTMLElement = HTMLElement>(\n search: string,\n): UseTextHighlightResult<TElement> {\n const [root, setRoot] = useState<TElement | null>(null);\n\n useIsomorphicLayoutEffect(() => {\n if (\n !root ||\n search.trim().length < MIN_SEARCH_LENGTH ||\n typeof CSS === 'undefined' ||\n !('highlights' in CSS) ||\n typeof StaticRange === 'undefined'\n ) {\n CSS?.highlights?.delete(HIGHLIGHT_NAME);\n CSS?.highlights?.delete(INDIRECT_HIGHLIGHT_NAME);\n return;\n }\n\n let animationFrame: number | null = null;\n\n const updateHighlight = () => {\n const ranges: StaticRange[] = [];\n const indirectRanges: StaticRange[] = [];\n const regex = new RegExp(escapeRegExp(search), 'giu');\n\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {\n acceptNode(node) {\n // Inside a rooted walker every text node has a parent element.\n const parent = node.parentElement as HTMLElement;\n return parent.closest(`${INCLUDED_SELECTOR}, ${INDIRECT_SELECTOR}`)\n ? NodeFilter.FILTER_ACCEPT\n : NodeFilter.FILTER_REJECT;\n },\n });\n\n while (walker.nextNode()) {\n const textNode = walker.currentNode as Text;\n const parent = textNode.parentElement as HTMLElement;\n\n // The indirect claim is about the whole text, so there is nothing to search for\n // inside it: the term lives in the payload this text stands for, not in the text.\n if (parent.closest(INDIRECT_SELECTOR)) {\n if (textNode.data.length > 0) {\n indirectRanges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: 0,\n endContainer: textNode,\n endOffset: textNode.data.length,\n }),\n );\n }\n continue;\n }\n\n regex.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(textNode.data)) !== null) {\n ranges.push(\n new StaticRange({\n startContainer: textNode,\n startOffset: match.index,\n endContainer: textNode,\n endOffset: match.index + match[0].length,\n }),\n );\n }\n }\n\n CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges));\n CSS.highlights.set(INDIRECT_HIGHLIGHT_NAME, new Highlight(...indirectRanges));\n };\n\n const scheduleUpdate = () => {\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n\n animationFrame = requestAnimationFrame(() => {\n animationFrame = null;\n updateHighlight();\n });\n };\n\n updateHighlight();\n\n // The opt-in attributes are watched too: a region can change which highlight it claims\n // while its text stays put, and that alone must repaint it.\n const observer = new MutationObserver(scheduleUpdate);\n observer.observe(root, {\n subtree: true,\n childList: true,\n characterData: true,\n attributes: true,\n attributeFilter: ['data-highlight', 'data-highlight-indirect'],\n });\n\n return () => {\n observer.disconnect();\n if (animationFrame !== null) cancelAnimationFrame(animationFrame);\n CSS.highlights.delete(HIGHLIGHT_NAME);\n CSS.highlights.delete(INDIRECT_HIGHLIGHT_NAME);\n };\n }, [root, search]);\n\n return { ref: setRoot };\n}\n"],"names":["useState","useIsomorphicLayoutEffect"],"mappings":"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"}
|