/* Copyright 2026 Marimo. All rights reserved. */ "use no memo"; import { type ColumnDef, type ColumnSort, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, type SortingState, useReactTable, } from "@tanstack/react-table"; import { SquareEqualIcon, WorkflowIcon } from "lucide-react"; import React, { memo, useMemo } from "react"; import { useLocale } from "react-aria"; import { CellLink } from "@/components/editor/links/cell-link"; import { getCellEditorView, useCellNames } from "@/core/cells/cells"; import type { CellId } from "@/core/cells/ids"; import { isInternalCellName } from "@/core/cells/names"; import { goToVariableDefinition } from "@/core/codemirror/go-to-definition/commands"; import type { Variable, Variables } from "@/core/variables/types"; import { sortBy } from "@/utils/arrays"; import { cn } from "@/utils/cn"; import { DataTableColumnHeader } from "../data-table/column-header"; import { CellLinkList } from "../editor/links/cell-link-list"; import { SearchInput } from "../ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../ui/table"; import { VariableName } from "./common"; interface Props { className?: string; /** * Used to sort the variables. */ cellIds: CellId[]; variables: Variables; } interface ResolvedVariable extends Variable { declaredByNames: string[]; usedByNames: string[]; } /* Column Definitions */ function columnDefOf(columnDef: ColumnDef) { return columnDef; } const ColumnIds = { name: "name", type: "type-value", defs: "defs-refs", }; const COLUMNS = [ columnDefOf({ id: ColumnIds.name, accessorFn: (v) => [v.name, v.declaredBy] as const, enableSorting: true, sortingFn: "alphanumeric", header: ({ column }) => ( ), cell: ({ getValue }) => { const [name, declaredBy] = getValue(); return ; }, }), columnDefOf({ id: ColumnIds.type, accessorFn: (v) => [v.dataType, v.value] as const, enableSorting: true, sortingFn: "alphanumeric", header: ({ column }) => ( Type Value } column={column} /> ), cell: ({ getValue }) => { const [dataType, value] = getValue(); return (
{dataType}
{value}
); }, }), columnDefOf({ id: ColumnIds.defs, // Include declaredByNames and usedByNames for filtering accessorFn: (v) => [ v.declaredBy, v.usedBy, v.name, v.declaredByNames, v.usedByNames, ] as const, enableSorting: true, sortingFn: "basic", header: ({ column }) => ( Declared By Used By } column={column} /> ), cell: ({ getValue }) => { const [declaredBy, usedBy, name] = getValue(); // Highlight the variable in the cell editor const highlightInCell = (cellId: CellId) => { const editorView = getCellEditorView(cellId); if (editorView) { goToVariableDefinition(editorView, name); } }; return (
{declaredBy.length === 1 ? ( highlightInCell(declaredBy[0])} /> ) : (
{declaredBy.slice(0, 3).map((cellId, idx) => ( highlightInCell(cellId)} /> {idx < declaredBy.length - 1 && ", "} ))}
)}
); }, }), ]; /** * Sort the variables by the specified column sort * Defaults to the order they are defined in the notebook */ function sortData({ variables, sort, cellIdToIndex, }: { variables: ResolvedVariable[]; sort: ColumnSort | undefined; cellIdToIndex: Map; }) { // Default to sort by the cell that defined it if (!sort) { sort = { id: ColumnIds.defs, desc: false }; } let sortedVariables: ResolvedVariable[] = []; switch (sort.id) { case ColumnIds.name: sortedVariables = sortBy(variables, (v) => v.name); break; case ColumnIds.type: sortedVariables = sortBy(variables, (v) => v.dataType); break; case ColumnIds.defs: sortedVariables = sortBy(variables, (v) => cellIdToIndex.get(v.declaredBy[0]), ); break; } return sort.desc ? sortedVariables.toReversed() : sortedVariables; } export const VariableTable: React.FC = memo( ({ className, cellIds, variables }) => { const [sorting, setSorting] = React.useState([]); const [globalFilter, setGlobalFilter] = React.useState(""); const cellNames = useCellNames(); const { locale } = useLocale(); const resolvedVariables: ResolvedVariable[] = useMemo(() => { const getName = (id: CellId) => { const name = cellNames[id]; if (isInternalCellName(name)) { return `cell-${cellIds.indexOf(id)}`; } return name ?? `cell-${cellIds.indexOf(id)}`; }; return Object.values(variables).map((variable) => { return { ...variable, declaredByNames: variable.declaredBy.map(getName), usedByNames: variable.usedBy.map(getName), }; }); }, [variables, cellNames, cellIds]); const sortedVariables = useMemo(() => { const cellIdToIndex = new Map(); cellIds.forEach((id, index) => cellIdToIndex.set(id, index)); return sortData({ variables: resolvedVariables, sort: sorting[0], cellIdToIndex, }); }, [resolvedVariables, sorting, cellIds]); const table = useReactTable({ data: sortedVariables, columns: COLUMNS, getCoreRowModel: getCoreRowModel(), // filtering onGlobalFilterChange: setGlobalFilter, getFilteredRowModel: getFilteredRowModel(), enableFilters: true, enableGlobalFilter: true, enableColumnPinning: false, getColumnCanGlobalFilter(column) { // Opt-out only return column.columnDef.enableGlobalFilter ?? true; }, globalFilterFn: "auto", // sorting manualSorting: true, locale: locale, onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), state: { sorting, globalFilter, }, }); return ( <> setGlobalFilter(e.target.value)} /> {table.getFlatHeaders().map((header) => ( {flexRender( header.column.columnDef.header, header.getContext(), )} ))} {table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ))}
); }, ); VariableTable.displayName = "VariableTable";