import React, { useCallback, useMemo, type Dispatch, type SetStateAction, } from "react"; import { Button } from "@/src/components/ui/button"; import { type ColumnOrderState, type VisibilityState, } from "@tanstack/react-table"; import { ChevronDown, ChevronRight, Component, Menu, X } from "lucide-react"; import { type LangfuseColumnDef } from "@/src/components/table/types"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import DocPopup from "@/src/components/layouts/doc-popup"; import { closestCenter, DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; import { cn } from "@/src/utils/tailwind"; import { isString } from "@/src/utils/types"; import { DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, Drawer, DrawerClose, } from "@/src/components/ui/drawer"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/src/components/ui/collapsible"; import { Checkbox } from "@/src/components/ui/checkbox"; import { Separator } from "@/src/components/ui/separator"; interface DataTableColumnVisibilityFilterProps { columns: LangfuseColumnDef[]; columnVisibility: VisibilityState; setColumnVisibility: Dispatch>; columnOrder?: ColumnOrderState; setColumnOrder?: Dispatch>; } const calculateColumnCounts = ( columns: LangfuseColumnDef[], columnVisibility: VisibilityState, ) => { return columns.reduce( (acc, column) => { if (column.columns) { const groupCounts = calculateColumnCounts( column.columns, columnVisibility, ); acc.count += groupCounts.count; acc.total += groupCounts.total; } else { acc.total++; if ( (column.accessorKey in columnVisibility && columnVisibility[column.accessorKey]) || !column.enableHiding ) { acc.count++; } } return acc; }, { count: 0, total: 0 }, ); }; function ColumnVisibilityListItem({ column, toggleColumn, columnVisibility, isOrderable = false, }: { column: LangfuseColumnDef; toggleColumn: (columnId: string) => void; columnVisibility: VisibilityState; isOrderable?: boolean; }) { const isFixedPosition = column.isFixedPosition ?? false; const { attributes, isDragging, listeners, setNodeRef, transform } = useSortable({ id: column.accessorKey, disabled: !isOrderable || isFixedPosition, }); const isChecked = columnVisibility[column.accessorKey] && column.enableHiding; return (
{ if (column.enableHiding && !isFixedPosition) toggleColumn(column.accessorKey); }} disabled={!column.enableHiding || isFixedPosition} className="h-4 w-4" /> {column.header && typeof column.header === "string" ? column.header : column.accessorKey} {column.headerTooltip && ( )}
{isOrderable && !isFixedPosition && ( )}
); } function GroupVisibilityHeader({ column, groupTotalCount, groupVisibleCount, isOpen, onToggle, children, toggleAll, }: { column: LangfuseColumnDef; groupTotalCount: number; groupVisibleCount: number; isOpen: boolean; onToggle: () => void; children: React.ReactNode; toggleAll: () => void; }) { const { attributes, isDragging, listeners, setNodeRef, transform } = useSortable({ id: column.accessorKey, }); return (
{column.header && typeof column.header === "string" ? column.header : column.accessorKey} ({groupVisibleCount}/{groupTotalCount})
{attributes && listeners && ( )} {isOpen ? ( ) : ( )}
{children}
); } function setAllColumns( columns: LangfuseColumnDef[], visible: boolean, groupName?: string, ) { return (oldVisibility: VisibilityState) => { const newColumnVisibility: VisibilityState = { ...oldVisibility }; columns.forEach((col) => { if (groupName && col.header === groupName && col.columns) { col.columns.forEach((subCol) => { if (subCol.enableHiding) newColumnVisibility[subCol.accessorKey] = visible; }); } else if (!groupName && col.enableHiding) { newColumnVisibility[col.accessorKey] = visible; if (col.columns) { col.columns.forEach((subCol) => { newColumnVisibility[subCol.accessorKey] = visible; }); } } }); return newColumnVisibility; }; } export function DataTableColumnVisibilityFilter({ columns, columnVisibility, setColumnVisibility, columnOrder, setColumnOrder, }: DataTableColumnVisibilityFilterProps) { const capture = usePostHogClientCapture(); const [openGroups, setOpenGroups] = React.useState>( {}, ); const { defaultColumnOrder, defaultColumnVisibility } = useMemo(() => { return { defaultColumnOrder: columns.map((col) => col.accessorKey), defaultColumnVisibility: columns.reduce((acc, col) => { acc[col.accessorKey] = !col.defaultHidden; return acc; }, {} as VisibilityState), }; }, [columns]); const toggleColumn = useCallback( (columnId: string) => { // calculate target state outside of setState to make it idempotent const currentValue = columnVisibility[columnId]; const targetValue = !currentValue; setColumnVisibility((old: any) => { const newColumnVisibility = { ...old, [columnId]: targetValue, }; const selectedColumns = Object.keys(newColumnVisibility).filter( (key) => newColumnVisibility[key], ); capture("table:column_visibility_changed", { selectedColumns: selectedColumns, }); return newColumnVisibility; }); }, // eslint disable is because we don't want the posthog capture as deps // eslint-disable-next-line react-hooks/exhaustive-deps [setColumnVisibility, columnVisibility], ); const toggleAllColumns = useCallback( (count: number, total: number, groupName?: string) => { if (count === total) { setColumnVisibility(setAllColumns(columns, false, groupName)); } else { setColumnVisibility(setAllColumns(columns, true, groupName)); } }, [setColumnVisibility, columns], ); const toggleGroup = (columnId: string) => { setOpenGroups((prev) => ({ ...prev, [columnId]: !prev[columnId], })); }; const sensors = useSensors( useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}), ); const { count, total } = calculateColumnCounts(columns, columnVisibility); const columnIdsOrder = columnOrder ?? columns.map((col) => col.accessorKey); const isColumnOrderingEnabled = !!setColumnOrder; function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (active && over && active.id !== over.id) { const activeColumn = columns.find((col) => col.accessorKey === active.id); const overColumn = columns.find((col) => col.accessorKey === over.id); // Prevent reordering if either active or over column is fixed position if (activeColumn?.isFixedPosition || overColumn?.isFixedPosition) { return; } if (isString(active.id) && isString(over.id)) { setColumnOrder!((columnOrder) => { const oldIndex = columnOrder.indexOf(active.id as string); const newIndex = columnOrder.indexOf(over.id as string); return arrayMove(columnOrder, oldIndex, newIndex); }); } } } return (
Column Visibility
toggleAllColumns(count, total)} >
{columnIdsOrder.map((columnId) => { const column = columns.find( (col) => col.accessorKey === columnId, ); if (!column) return null; if (!!column.columns && column.columns.length > 0) { // Column groups const groupTotalCount = column.columns.length; const groupVisibleCount = column.columns.filter( (col) => columnVisibility[col.accessorKey], ).length; return ( toggleGroup(column.accessorKey)} toggleAll={() => { if ( column.header && typeof column.header === "string" ) { toggleAllColumns( groupVisibleCount, groupTotalCount, column.header, ); } }} >
{column.columns.map((col) => ( ))}
); } else { // Single columns return ( ); } })}
); }