import { ChevronDownIcon, ChevronUpDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid"; import type { OutputColumnMetadata } from "@internal/clickhouse"; import { IconFilter2, IconFilter2X, IconTable } from "@tabler/icons-react"; import { rankItem } from "@tanstack/match-sorter-utils"; import { flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable, type CellContext, type Column, type ColumnDef, type ColumnFiltersState, type ColumnResizeMode, type FilterFn, type SortDirection, type SortingState, } from "@tanstack/react-table"; import { useVirtualizer } from "@tanstack/react-virtual"; import { formatDurationMilliseconds, MachinePresetName } from "@trigger.dev/core/v3"; import { AlertCircle, ClipboardCheckIcon, ClipboardIcon } from "lucide-react"; import { forwardRef, memo, useEffect, useMemo, useRef, useState } from "react"; import { EnvironmentLabel, EnvironmentSlug } from "~/components/environments/EnvironmentLabel"; import { MachineLabelCombo } from "~/components/MachineLabelCombo"; import { DateTimeAccurate } from "~/components/primitives/DateTime"; import { descriptionForTaskRunStatus, isRunFriendlyStatus, isTaskRunStatus, runStatusFromFriendlyTitle, TaskRunStatusCombo, } from "~/components/runs/v3/TaskRunStatus"; import { useCopy } from "~/hooks/useCopy"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { cn } from "~/utils/cn"; import { formatBytes, formatDecimalBytes, formatQuantity } from "~/utils/columnFormat"; import { formatCurrencyAccurate, formatNumber } from "~/utils/numberFormatter"; import { v3ProjectPath, v3RunPathFromFriendlyId } from "~/utils/pathBuilder"; import { ChartBlankState } from "../primitives/charts/ChartBlankState"; import { Paragraph } from "../primitives/Paragraph"; import { TextLink } from "../primitives/TextLink"; import { InfoIconTooltip, SimpleTooltip } from "../primitives/Tooltip"; import { QueueName } from "../runs/v3/QueueName"; const MAX_STRING_DISPLAY_LENGTH = 64; const ROW_HEIGHT = 33; // Estimated row height in pixels // Column width calculation constants const MIN_COLUMN_WIDTH = 60; const MAX_COLUMN_WIDTH = 400; const CHAR_WIDTH_PX = 7.5; // Approximate width of a monospace character at text-xs (12px) const CELL_PADDING_PX = 40; // px-2 (8px) on each side + buffer for copy button const HEADER_ICONS_WIDTH_PX = 80; // Sort icon (16px) + filter icon (12px) + info icon (16px) + gaps (12px) + header padding (24px) const SAMPLE_SIZE = 100; // Number of rows to sample for width calculation // Type for row data type RowData = Record; /** * Get the formatted display string for a value based on its column type * This mirrors the formatting logic in CellValue component */ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string { if (value === null) return "NULL"; if (value === undefined) return ""; // Handle format hints (from prettyFormat() or auto-populated from customRenderType) const formatType = column.format ?? column.customRenderType; if (formatType) { switch (formatType) { case "duration": if (typeof value === "number") { return formatDurationMilliseconds(value, { style: "short" }); } break; case "durationSeconds": if (typeof value === "number") { return formatDurationMilliseconds(value * 1000, { style: "short" }); } break; case "durationNs": if (typeof value === "number") { return formatDurationMilliseconds(value / 1_000_000, { style: "short" }); } break; case "cost": if (typeof value === "number") { return formatCurrencyAccurate(value / 100); } break; case "costInDollars": if (typeof value === "number") { return formatCurrencyAccurate(value); } break; case "runStatus": // Include friendly status names for searching if (typeof value === "string") { return value; } break; case "bytes": if (typeof value === "number") { return formatBytes(value); } break; case "decimalBytes": if (typeof value === "number") { return formatDecimalBytes(value); } break; case "percent": if (typeof value === "number") { return `${value.toFixed(2)}%`; } break; case "quantity": if (typeof value === "number") { return formatQuantity(value); } break; } } // Handle DateTime types - format for display if (isDateTimeType(column.type)) { if (typeof value === "string") { try { const date = new Date(value); // Format as a searchable string: "15 Jan 2026 12:34:56" return date.toLocaleString("en-GB", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", timeZone: "UTC", }); } catch { return String(value); } } } // Handle numeric types - format with separators if (isNumericType(column.type) && typeof value === "number") { return formatNumber(value); } // Handle booleans if (isBooleanType(column.type)) { if (typeof value === "boolean") { return value ? "true" : "false"; } if (typeof value === "number") { return value === 1 ? "true" : "false"; } } // Handle objects/arrays if (typeof value === "object") { return JSON.stringify(value); } return String(value); } const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { // Get the cell value const cellValue = row.getValue(columnId); const searchValue = String(value).toLowerCase(); // Handle empty search if (!searchValue) return true; // Get the column metadata from the cell const cell = row.getAllCells().find((c) => c.column.id === columnId); const meta = cell?.column.columnDef.meta as ColumnMeta | undefined; // Build searchable strings - raw value const rawValue = cellValue === null ? "NULL" : cellValue === undefined ? "" : typeof cellValue === "object" ? JSON.stringify(cellValue) : String(cellValue); // Build searchable strings - formatted value (if we have column metadata) const formattedValue = meta?.outputColumn ? getFormattedValue(cellValue, meta.outputColumn) : rawValue; // Combine both values for searching (separated by space to allow matching either) const combinedSearchText = `${rawValue} ${formattedValue}`.toLowerCase(); // Rank against the combined text const itemRank = rankItem(combinedSearchText, searchValue); // Store the ranking info addMeta({ itemRank }); // Return if the item should be filtered in/out return itemRank.passed; }; /** * Debounced input component for filter inputs */ const DebouncedInput = forwardRef< HTMLInputElement, { value: string; onChange: (value: string) => void; debounce?: number; } & Omit, "onChange"> >(function DebouncedInput({ value: initialValue, onChange, debounce = 300, ...props }, ref) { const [value, setValue] = useState(initialValue); useEffect(() => { setValue(initialValue); }, [initialValue]); useEffect(() => { const timeout = setTimeout(() => { onChange(value); }, debounce); return () => clearTimeout(timeout); }, [value, debounce, onChange]); return setValue(e.target.value)} />; }); // Extended column meta to store OutputColumnMetadata interface ColumnMeta { outputColumn: OutputColumnMetadata; alignment: "left" | "right"; } /** * Get the approximate display length (in characters) of a value based on its type and formatting */ function getDisplayLength(value: unknown, column: OutputColumnMetadata): number { if (value === null) return 4; // "NULL" if (value === undefined) return 9; // "UNDEFINED" // Handle format hint types - estimate their rendered width const fmt = column.format; if (fmt === "bytes" || fmt === "decimalBytes") { // e.g., "1.50 GiB" or "256.00 MB" return 12; } if (fmt === "percent") { // e.g., "45.23%" return 8; } if (fmt === "quantity") { // e.g., "1.50M" return 8; } // Handle custom render types - estimate their rendered width if (column.customRenderType) { switch (column.customRenderType) { case "runId": // Run IDs are typically like "run_abc123xyz" return typeof value === "string" ? Math.min(value.length, MAX_STRING_DISPLAY_LENGTH) : 15; case "runStatus": // Status badges have icon + text, approximate width return 12; case "duration": if (typeof value === "number") { // Format and measure: "1h 23m 45s" style const formatted = formatDurationMilliseconds(value, { style: "short" }); return formatted.length; } return 10; case "durationSeconds": if (typeof value === "number") { const formatted = formatDurationMilliseconds(value * 1000, { style: "short" }); return formatted.length; } return 10; case "durationNs": if (typeof value === "number") { const formatted = formatDurationMilliseconds(value / 1_000_000, { style: "short" }); return formatted.length; } return 10; case "cost": case "costInDollars": // Currency format: "$1,234.56" if (typeof value === "number") { const amount = column.customRenderType === "cost" ? value / 100 : value; return formatCurrencyAccurate(amount).length; } return 12; case "machine": // Machine preset names like "small-1x" return typeof value === "string" ? value.length : 10; case "environmentType": // Environment labels: "PRODUCTION", "STAGING", etc. return 12; case "project": case "environment": return typeof value === "string" ? Math.min(value.length, 20) : 12; case "queue": return typeof value === "string" ? Math.min(value.length, 25) : 15; case "deploymentId": return typeof value === "string" ? Math.min(value.length, 25) : 20; } } // Handle by ClickHouse type if (isDateTimeType(column.type)) { // DateTime format: "Jan 15, 2026, 12:34:56 PM" return 24; } if (column.type === "JSON" || column.type.startsWith("Array")) { if (typeof value === "object") { const jsonStr = JSON.stringify(value); return Math.min(jsonStr.length, MAX_STRING_DISPLAY_LENGTH); } } if (isBooleanType(column.type)) { return 5; // "true" or "false" } if (isNumericType(column.type)) { if (typeof value === "number") { return formatNumber(value).length; } } // Default: string length capped at max display length const strValue = String(value); return Math.min(strValue.length, MAX_STRING_DISPLAY_LENGTH); } /** * Calculate the optimal width for a column based on its content */ function calculateColumnWidth( columnName: string, rows: RowData[], column: OutputColumnMetadata ): number { // Calculate minimum width needed for the header (text + icons) const headerWidth = Math.ceil(columnName.length * CHAR_WIDTH_PX + HEADER_ICONS_WIDTH_PX); // Sample rows to find max content length let maxContentLength = 0; const sampleRows = rows.slice(0, SAMPLE_SIZE); for (const row of sampleRows) { const value = row[columnName]; const displayLength = getDisplayLength(value, column); if (displayLength > maxContentLength) { maxContentLength = displayLength; } } // Calculate pixel width for content: characters * char width + padding const contentWidth = Math.ceil(maxContentLength * CHAR_WIDTH_PX + CELL_PADDING_PX); // Use the larger of header width or content width const calculatedWidth = Math.max(headerWidth, contentWidth); // Apply min/max bounds return Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, calculatedWidth)); } /** * Truncate a string for display, adding ellipsis if it exceeds max length */ function truncateString(value: string, maxLength: number = MAX_STRING_DISPLAY_LENGTH): string { if (value.length <= maxLength) { return value; } return value.slice(0, maxLength) + "…"; } /** * Convert any value to a string suitable for copying * Objects and arrays are JSON stringified, primitives use String() */ function valueToString(value: unknown): string { if (value === null) return "NULL"; if (value === undefined) return "UNDEFINED"; if (typeof value === "object") return JSON.stringify(value); return String(value); } /** * Check if a ClickHouse type is a DateTime type */ function isDateTimeType(type: string): boolean { return ( type === "DateTime" || type === "DateTime64" || type === "Date" || type === "Date32" || type.startsWith("Nullable(DateTime") || type.startsWith("Nullable(Date") ); } /** * Check if a ClickHouse type is a numeric type */ function isNumericType(type: string): boolean { return ( type.startsWith("Int") || type.startsWith("UInt") || type.startsWith("Float") || type.startsWith("Nullable(Int") || type.startsWith("Nullable(UInt") || type.startsWith("Nullable(Float") ); } /** * Check if a ClickHouse type is a boolean type */ function isBooleanType(type: string): boolean { return type === "Bool" || type === "Nullable(Bool)"; } /** * Check if a column should be right-aligned (numeric columns, duration, cost) */ function isRightAlignedColumn(column: OutputColumnMetadata): boolean { if ( column.customRenderType === "duration" || column.customRenderType === "durationSeconds" || column.customRenderType === "cost" || column.customRenderType === "costInDollars" ) { return true; } const fmt = column.format; if (fmt === "bytes" || fmt === "decimalBytes" || fmt === "percent" || fmt === "quantity") { return true; } return isNumericType(column.type); } /** * Wrapper component that tracks hover state and passes it to CellValue * This optimizes rendering by only enabling tooltips when the cell is hovered */ function CellValueWrapper({ value, column, prettyFormatting, row, }: { value: unknown; column: OutputColumnMetadata; prettyFormatting: boolean; row?: Record; }) { const [hovered, setHovered] = useState(false); return ( setHovered(true)} onMouseLeave={() => setHovered(false)} > ); } /** * Render a cell value based on its type and optional customRenderType */ function CellValue({ value, column, prettyFormatting = true, hovered = false, row, }: { value: unknown; column: OutputColumnMetadata; prettyFormatting?: boolean; hovered?: boolean; row?: Record; }) { // Plain text mode - render everything as monospace text with truncation if (!prettyFormatting) { if (column.type === "JSON") { return ; } const plainValue = value === null ? "NULL" : String(value); const isTruncated = plainValue.length > MAX_STRING_DISPLAY_LENGTH; if (isTruncated) { return ( {plainValue} } button={
{truncateString(plainValue)}
} disableHoverableContent /> ); } return
{plainValue}
; } if (value === null) { return
NULL
; } if (value === undefined) { return
UNDEFINED
; } // Check format hint for new format types (from prettyFormat()) if (column.format && !column.customRenderType) { switch (column.format) { case "bytes": if (typeof value === "number") { return {formatBytes(value)}; } break; case "decimalBytes": if (typeof value === "number") { return {formatDecimalBytes(value)}; } break; case "percent": if (typeof value === "number") { return {value.toFixed(2)}%; } break; case "quantity": if (typeof value === "number") { return {formatQuantity(value)}; } break; } } // First check customRenderType for special rendering if (column.customRenderType) { switch (column.customRenderType) { case "runId": { if (typeof value === "string") { const spanId = row?.["span_id"]; const runPath = v3RunPathFromFriendlyId(value); const href = typeof spanId === "string" && spanId ? `${runPath}?span=${spanId}` : runPath; const tooltip = typeof spanId === "string" && spanId ? "Jump to span" : "Jump to run"; return (