Skip to content

Commit b4ac6f1

Browse files
committed
Added column filtering
1 parent fa3080c commit b4ac6f1

3 files changed

Lines changed: 171 additions & 6 deletions

File tree

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import type { OutputColumnMetadata } from "@internal/clickhouse";
2+
import { rankItem } from "@tanstack/match-sorter-utils";
23
import {
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";
1015
import { useVirtualizer } from "@tanstack/react-virtual";
1116
import { 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";
1419
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
1520
import { MachineLabelCombo } from "~/components/MachineLabelCombo";
1621
import { 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
4651
type 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
49115
interface 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
*/
591657
function 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+
632744
export 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={{

apps/webapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"@socket.io/redis-adapter": "^8.3.0",
114114
"@tabler/icons-react": "^3.36.1",
115115
"@tailwindcss/container-queries": "^0.1.1",
116+
"@tanstack/match-sorter-utils": "^8.19.4",
116117
"@tanstack/react-table": "^8.21.3",
117118
"@tanstack/react-virtual": "^3.0.4",
118119
"@team-plain/typescript-sdk": "^3.5.0",

pnpm-lock.yaml

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)