11import type { OutputColumnMetadata } from "@internal/clickhouse" ;
2+ import { rankItem } from "@tanstack/match-sorter-utils" ;
23import {
34 useReactTable ,
45 getCoreRowModel ,
6+ getFilteredRowModel ,
57 flexRender ,
68 type ColumnDef ,
79 type CellContext ,
810 type ColumnResizeMode ,
11+ type ColumnFiltersState ,
12+ type FilterFn ,
13+ type Column ,
914} from "@tanstack/react-table" ;
1015import { useVirtualizer } from "@tanstack/react-virtual" ;
1116import { formatDurationMilliseconds , MachinePresetName } from "@trigger.dev/core/v3" ;
12- import { ClipboardCheckIcon , ClipboardIcon } from "lucide-react" ;
13- import { memo , useMemo , useRef , useState } from "react" ;
17+ import { ClipboardCheckIcon , ClipboardIcon , FilterIcon } from "lucide-react" ;
18+ import { memo , useEffect , useMemo , useRef , useState } from "react" ;
1419import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel" ;
1520import { MachineLabelCombo } from "~/components/MachineLabelCombo" ;
1621import { DateTimeAccurate } from "~/components/primitives/DateTime" ;
@@ -45,6 +50,67 @@ const SAMPLE_SIZE = 100; // Number of rows to sample for width calculation
4550// Type for row data
4651type RowData = Record < string , unknown > ;
4752
53+ /**
54+ * Fuzzy filter function using match-sorter ranking
55+ */
56+ const fuzzyFilter : FilterFn < RowData > = ( row , columnId , value , addMeta ) => {
57+ // Get the cell value and convert to string for matching
58+ const cellValue = row . getValue ( columnId ) ;
59+ const searchValue = String ( value ) ;
60+
61+ // Handle empty search
62+ if ( ! searchValue ) return true ;
63+
64+ // Convert cell value to string for ranking
65+ const itemValue =
66+ cellValue === null
67+ ? "NULL"
68+ : cellValue === undefined
69+ ? ""
70+ : typeof cellValue === "object"
71+ ? JSON . stringify ( cellValue )
72+ : String ( cellValue ) ;
73+
74+ // Rank the item
75+ const itemRank = rankItem ( itemValue , searchValue ) ;
76+
77+ // Store the ranking info
78+ addMeta ( { itemRank } ) ;
79+
80+ // Return if the item should be filtered in/out
81+ return itemRank . passed ;
82+ } ;
83+
84+ /**
85+ * Debounced input component for filter inputs
86+ */
87+ function DebouncedInput ( {
88+ value : initialValue ,
89+ onChange,
90+ debounce = 300 ,
91+ ...props
92+ } : {
93+ value : string ;
94+ onChange : ( value : string ) => void ;
95+ debounce ?: number ;
96+ } & Omit < React . InputHTMLAttributes < HTMLInputElement > , "onChange" > ) {
97+ const [ value , setValue ] = useState ( initialValue ) ;
98+
99+ useEffect ( ( ) => {
100+ setValue ( initialValue ) ;
101+ } , [ initialValue ] ) ;
102+
103+ useEffect ( ( ) => {
104+ const timeout = setTimeout ( ( ) => {
105+ onChange ( value ) ;
106+ } , debounce ) ;
107+
108+ return ( ) => clearTimeout ( timeout ) ;
109+ } , [ value , debounce , onChange ] ) ;
110+
111+ return < input { ...props } value = { value } onChange = { ( e ) => setValue ( e . target . value ) } /> ;
112+ }
113+
48114// Extended column meta to store OutputColumnMetadata
49115interface ColumnMeta {
50116 outputColumn : OutputColumnMetadata ;
@@ -586,23 +652,29 @@ function CopyableCell({
586652}
587653
588654/**
589- * Header cell component with tooltip support
655+ * Header cell component with tooltip support and filter toggle
590656 */
591657function HeaderCellContent ( {
592658 alignment,
593659 tooltip,
594660 children,
661+ onFilterClick,
662+ showFilters,
663+ hasActiveFilter,
595664} : {
596665 alignment : "left" | "right" ;
597666 tooltip ?: React . ReactNode ;
598667 children : React . ReactNode ;
668+ onFilterClick ?: ( ) => void ;
669+ showFilters ?: boolean ;
670+ hasActiveFilter ?: boolean ;
599671} ) {
600672 const [ isHovered , setIsHovered ] = useState ( false ) ;
601673
602674 return (
603675 < div
604676 className = { cn (
605- "flex w-full items-center overflow-hidden bg-background-dimmed px-2 py-1.5" ,
677+ "flex w-full items-center gap-1 overflow-hidden bg-background-dimmed px-2 py-1.5" ,
606678 "text-sm font-medium text-text-bright" ,
607679 alignment === "right" && "justify-end"
608680 ) }
@@ -611,7 +683,7 @@ function HeaderCellContent({
611683 >
612684 { tooltip ? (
613685 < div
614- className = { cn ( "flex items-center gap-1 truncate" , {
686+ className = { cn ( "flex min-w-0 flex-1 items-center gap-1 truncate" , {
615687 "justify-end" : alignment === "right" ,
616688 } ) }
617689 >
@@ -623,12 +695,52 @@ function HeaderCellContent({
623695 />
624696 </ div >
625697 ) : (
626- < span className = "truncate" > { children } </ span >
698+ < span className = "min-w-0 flex-1 truncate" > { children } </ span >
699+ ) }
700+ { onFilterClick && (
701+ < button
702+ onClick = { ( e ) => {
703+ e . stopPropagation ( ) ;
704+ onFilterClick ( ) ;
705+ } }
706+ className = { cn (
707+ "flex-shrink-0 rounded p-0.5 transition-colors" ,
708+ "hover:bg-charcoal-700" ,
709+ showFilters || hasActiveFilter
710+ ? "text-primary"
711+ : "text-text-dimmed hover:text-text-bright"
712+ ) }
713+ title = "Toggle column filters"
714+ >
715+ < FilterIcon className = "size-3.5" />
716+ </ button >
627717 ) }
628718 </ div >
629719 ) ;
630720}
631721
722+ /**
723+ * Filter input cell for the filter row
724+ */
725+ function FilterCell ( { column, width } : { column : Column < RowData , unknown > ; width : number } ) {
726+ const columnFilterValue = column . getFilterValue ( ) as string ;
727+
728+ return (
729+ < div className = "flex items-center bg-background-dimmed px-1.5 py-1" style = { { width } } >
730+ < DebouncedInput
731+ value = { columnFilterValue ?? "" }
732+ onChange = { ( value ) => column . setFilterValue ( value ) }
733+ placeholder = "Filter..."
734+ className = { cn (
735+ "w-full rounded border border-charcoal-700 bg-charcoal-800 px-2 py-1" ,
736+ "text-xs text-text-bright placeholder:text-text-dimmed" ,
737+ "focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
738+ ) }
739+ />
740+ </ div >
741+ ) ;
742+ }
743+
632744export const TSQLResultsTable = memo ( function TSQLResultsTable ( {
633745 rows,
634746 columns,
@@ -640,6 +752,10 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
640752} ) {
641753 const tableContainerRef = useRef < HTMLDivElement > ( null ) ;
642754
755+ // State for column filters and filter row visibility
756+ const [ columnFilters , setColumnFilters ] = useState < ColumnFiltersState > ( [ ] ) ;
757+ const [ showFilters , setShowFilters ] = useState ( false ) ;
758+
643759 // Create TanStack Table column definitions from OutputColumnMetadata
644760 // Calculate column widths based on content
645761 const columnDefs = useMemo < ColumnDef < RowData , unknown > [ ] > (
@@ -660,6 +776,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
660776 alignment : isRightAlignedColumn ( col ) ? "right" : "left" ,
661777 } as ColumnMeta ,
662778 size : calculateColumnWidth ( col . name , rows , col ) ,
779+ filterFn : fuzzyFilter ,
663780 } ) ) ,
664781 [ columns , rows , prettyFormatting ]
665782 ) ;
@@ -672,7 +789,12 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
672789 data : rows ,
673790 columns : columnDefs ,
674791 columnResizeMode,
792+ state : {
793+ columnFilters,
794+ } ,
795+ onColumnFiltersChange : setColumnFilters ,
675796 getCoreRowModel : getCoreRowModel ( ) ,
797+ getFilteredRowModel : getFilteredRowModel ( ) ,
676798 } ) ;
677799
678800 const { rows : tableRows } = table . getRowModel ( ) ;
@@ -720,6 +842,9 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
720842 < HeaderCellContent
721843 alignment = { meta ?. alignment ?? "left" }
722844 tooltip = { meta ?. outputColumn . description }
845+ onFilterClick = { ( ) => setShowFilters ( ! showFilters ) }
846+ showFilters = { showFilters }
847+ hasActiveFilter = { ! ! header . column . getFilterValue ( ) }
723848 >
724849 { flexRender ( header . column . columnDef . header , header . getContext ( ) ) }
725850 </ HeaderCellContent >
@@ -740,6 +865,18 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
740865 } ) }
741866 </ tr >
742867 ) ) }
868+ { /* Filter row - shown when filters are toggled */ }
869+ { showFilters && (
870+ < tr style = { { display : "flex" , width : "100%" } } >
871+ { table . getHeaderGroups ( ) [ 0 ] ?. headers . map ( ( header ) => (
872+ < FilterCell
873+ key = { `filter-${ header . id } ` }
874+ column = { header . column }
875+ width = { header . getSize ( ) }
876+ />
877+ ) ) }
878+ </ tr >
879+ ) }
743880 </ thead >
744881 < tbody style = { { display : "grid" } } >
745882 < tr style = { { display : "flex" } } >
@@ -771,6 +908,7 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
771908 zIndex : 1 ,
772909 } }
773910 >
911+ { /* Main header row */ }
774912 { table . getHeaderGroups ( ) . map ( ( headerGroup ) => (
775913 < tr key = { headerGroup . id } style = { { display : "flex" , width : "100%" } } >
776914 { headerGroup . headers . map ( ( header ) => {
@@ -787,6 +925,9 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
787925 < HeaderCellContent
788926 alignment = { meta ?. alignment ?? "left" }
789927 tooltip = { meta ?. outputColumn . description }
928+ onFilterClick = { ( ) => setShowFilters ( ! showFilters ) }
929+ showFilters = { showFilters }
930+ hasActiveFilter = { ! ! header . column . getFilterValue ( ) }
790931 >
791932 { flexRender ( header . column . columnDef . header , header . getContext ( ) ) }
792933 </ HeaderCellContent >
@@ -807,6 +948,18 @@ export const TSQLResultsTable = memo(function TSQLResultsTable({
807948 } ) }
808949 </ tr >
809950 ) ) }
951+ { /* Filter row - shown when filters are toggled */ }
952+ { showFilters && (
953+ < tr style = { { display : "flex" , width : "100%" } } >
954+ { table . getHeaderGroups ( ) [ 0 ] ?. headers . map ( ( header ) => (
955+ < FilterCell
956+ key = { `filter-${ header . id } ` }
957+ column = { header . column }
958+ width = { header . getSize ( ) }
959+ />
960+ ) ) }
961+ </ tr >
962+ ) }
810963 </ thead >
811964 < tbody
812965 style = { {
0 commit comments