Skip to main content
Glama

@arizeai/phoenix-mcp

Official
by Arize-ai
Table.stories.tsx16.4 kB
import { useMemo, useState } from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { ColumnDef, ColumnSizingState, flexRender, getCoreRowModel, getSortedRowModel, RowSelectionState, SortingState, useReactTable, } from "@tanstack/react-table"; import { css } from "@emotion/react"; import { Button, Icon, IconButton, Icons, Text, Tooltip, TooltipTrigger, View, } from "@phoenix/components"; import { FloatCell, IntCell, TextCell } from "@phoenix/components/table"; import { Toolbar } from "@phoenix/components/toolbar"; import { FloatingToolbarContainer } from "@phoenix/components/toolbar/FloatingToolbarContainer"; import { IndeterminateCheckboxCell } from "../src/components/table/IndeterminateCheckboxCell"; import { selectableTableCSS } from "../src/components/table/styles"; import { TableEmpty } from "../src/components/table/TableEmpty"; // Mock data types type Person = { id: string; name: string; age: number; email: string; salary: number; status: "active" | "inactive"; department: string; }; type Product = { id: string; name: string; price: number; category: string; inStock: boolean; rating: number; }; // Mock data const mockPeople: Person[] = [ { id: "1", name: "John Doe", age: 30, email: "john.doe@example.com", salary: 75000, status: "active", department: "Engineering", }, { id: "2", name: "Jane Smith", age: 28, email: "jane.smith@example.com", salary: 82000, status: "active", department: "Design", }, { id: "3", name: "Bob Johnson", age: 35, email: "bob.johnson@example.com", salary: 68000, status: "inactive", department: "Marketing", }, { id: "4", name: "Alice Williams", age: 32, email: "alice.williams@example.com", salary: 95000, status: "active", department: "Engineering", }, { id: "5", name: "Charlie Brown", age: 45, email: "charlie.brown@example.com", salary: 120000, status: "active", department: "Management", }, { id: "6", name: "David Kim", age: 29, email: "david.kim@example.com", salary: 70000, status: "active", department: "Engineering", }, { id: "7", name: "Emily Clark", age: 26, email: "emily.clark@example.com", salary: 65000, status: "inactive", department: "Support", }, { id: "8", name: "Frank Garcia", age: 38, email: "frank.garcia@example.com", salary: 88000, status: "active", department: "Sales", }, { id: "9", name: "Grace Lee", age: 31, email: "grace.lee@example.com", salary: 93500, status: "active", department: "Marketing", }, { id: "10", name: "Hank Miller", age: 42, email: "hank.miller@example.com", salary: 112000, status: "inactive", department: "HR", }, ]; const mockProducts: Product[] = [ { id: "1", name: "Laptop Pro", price: 1299.99, category: "Electronics", inStock: true, rating: 4.5, }, { id: "2", name: "Wireless Mouse", price: 29.99, category: "Electronics", inStock: true, rating: 4.2, }, { id: "3", name: "Office Chair", price: 299.99, category: "Furniture", inStock: false, rating: 4.7, }, { id: "4", name: "Desk Lamp", price: 79.99, category: "Furniture", inStock: true, rating: 4.0, }, { id: "5", name: "Smartphone X", price: 999.99, category: "Electronics", inStock: true, rating: 4.6, }, { id: "6", name: "Noise-Cancelling Headphones", price: 199.99, category: "Electronics", inStock: false, rating: 4.4, }, { id: "7", name: "Gaming Keyboard", price: 129.99, category: "Electronics", inStock: true, rating: 4.3, }, { id: "8", name: "Standing Desk", price: 499.99, category: "Furniture", inStock: true, rating: 4.8, }, { id: "9", name: "LED Monitor", price: 249.99, category: "Electronics", inStock: true, rating: 4.5, }, ]; // Base Table Component function BaseTable<T>({ columns, data, enableResizing = true, enableSorting = true, onSelectionChange, }: { columns: ColumnDef<T>[]; data: T[]; enableResizing?: boolean; enableSorting?: boolean; onSelectionChange?: (selectedCount: number) => void; }) { const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({}); const [sorting, setSorting] = useState<SortingState>([]); const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const table = useReactTable<T>({ columns, data, state: { columnSizing, sorting, rowSelection, }, columnResizeMode: enableResizing ? "onChange" : undefined, manualSorting: false, enableRowSelection: true, onColumnSizingChange: setColumnSizing, onSortingChange: setSorting, onRowSelectionChange: (updater) => setRowSelection((prev) => { const next = typeof updater === "function" ? updater(prev) : (updater ?? {}); onSelectionChange?.(Object.keys(next).length); return next; }), getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), }); const rows = table.getRowModel().rows; const hasContent = rows.length > 0; // Performance optimization for column sizing const { columnSizingInfo, columnSizing: columnSizingState } = table.getState(); const getFlatHeaders = table.getFlatHeaders; const colLength = table.getAllColumns().length; const columnSizeVars = useMemo(() => { const headers = getFlatHeaders(); const colSizes: { [key: string]: number } = {}; for (let i = 0; i < headers.length; i++) { const header = headers[i]!; colSizes[`--header-${header.id}-size`] = header.getSize(); colSizes[`--col-${header.column.id}-size`] = header.column.getSize(); } return colSizes; // Disabled lint as per tanstack docs - dependencies are necessary for column resizing // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps }, [getFlatHeaders, columnSizingInfo, columnSizingState, colLength]); const body = hasContent ? ( <tbody> {rows.map((row) => { return ( <tr key={row.id} data-selected={row.getIsSelected()}> {row.getVisibleCells().map((cell) => { return ( <td key={cell.id} style={{ width: `calc(var(--col-${cell.column.id}-size) * 1px)`, maxWidth: `calc(var(--col-${cell.column.id}-size) * 1px)`, textWrap: "nowrap", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ); })} </tr> ); })} </tbody> ) : ( <TableEmpty /> ); return ( <div css={css` border: 1px solid var(--ac-global-border-color-default); border-radius: var(--ac-global-rounding-small); overflow: hidden; `} > <table css={selectableTableCSS} style={{ ...columnSizeVars, width: table.getTotalSize(), minWidth: "100%", }} > <thead> {table.getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => ( <th colSpan={header.colSpan} key={header.id} style={{ width: `calc(var(--header-${header.id}-size) * 1px)`, }} > {header.isPlaceholder ? null : ( <> <div {...{ className: header.column.getCanSort() && enableSorting ? "sort" : "", onClick: enableSorting ? header.column.getToggleSortingHandler() : undefined, }} > {flexRender( header.column.columnDef.header, header.getContext() )} {header.column.getIsSorted() ? ( <Icon className="sort-icon" svg={ header.column.getIsSorted() === "asc" ? ( <Icons.ArrowUpFilled /> ) : ( <Icons.ArrowDownFilled /> ) } /> ) : null} </div> {enableResizing && ( <div {...{ onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), className: `resizer ${ header.column.getIsResizing() ? "isResizing" : "" }`, }} /> )} </> )} </th> ))} </tr> ))} </thead> {body} </table> </div> ); } // Column definitions const personColumns: ColumnDef<Person>[] = [ { header: "Name", accessorKey: "name", size: 200, cell: ({ getValue }) => <strong>{getValue() as string}</strong>, }, { header: "Age", accessorKey: "age", size: 80, cell: IntCell, }, { header: "Email", accessorKey: "email", size: 250, cell: TextCell, }, { header: "Salary", accessorKey: "salary", size: 120, cell: ({ getValue }) => `$${(getValue() as number).toLocaleString()}`, }, { header: "Status", accessorKey: "status", size: 100, cell: ({ getValue }) => { const status = getValue() as string; return ( <span css={css` padding: 2px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; background-color: ${status === "active" ? "var(--ac-global-color-success-100)" : "var(--ac-global-color-grey-100)"}; color: ${status === "active" ? "var(--ac-global-color-success-900)" : "var(--ac-global-color-grey-700)"}; `} > {status} </span> ); }, }, { header: "Department", accessorKey: "department", size: 150, cell: TextCell, }, ]; const productColumns: ColumnDef<Product>[] = [ { header: "Product", accessorKey: "name", size: 200, cell: ({ getValue }) => <strong>{getValue() as string}</strong>, }, { header: "Price", accessorKey: "price", size: 100, cell: FloatCell, }, { header: "Category", accessorKey: "category", size: 120, cell: TextCell, }, { header: "In Stock", accessorKey: "inStock", size: 100, cell: ({ getValue }) => (getValue() ? "✅ Yes" : "❌ No"), }, { header: "Rating", accessorKey: "rating", size: 100, cell: ({ getValue }) => `⭐ ${getValue()}`, }, ]; // ------------------------------ // Selection Support // ------------------------------ // Column used to display row selection checkboxes – adapted from project SpansTable const selectionColumn: ColumnDef<Person> = { id: "select", size: 30, maxSize: 30, header: ({ table }) => ( <IndeterminateCheckboxCell isSelected={table.getIsAllRowsSelected()} isIndeterminate={table.getIsSomeRowsSelected()} onChange={table.toggleAllRowsSelected} /> ), cell: ({ row }) => ( <IndeterminateCheckboxCell isSelected={row.getIsSelected()} isDisabled={!row.getCanSelect()} isIndeterminate={row.getIsSomeSelected()} onChange={row.toggleSelected} /> ), enableSorting: false, }; // Table component that prepends the selection column const PersonTableSelectable = (props: { enableResizing?: boolean; enableSorting?: boolean; data?: Person[]; onSelectionChange?: (count: number) => void; }) => { const columns = useMemo<ColumnDef<Person>[]>( () => [selectionColumn, ...personColumns], [] ); return ( <BaseTable columns={columns} data={props.data || mockPeople} enableResizing={props.enableResizing} enableSorting={props.enableSorting} onSelectionChange={props.onSelectionChange} /> ); }; // Story Components const PersonTable = (props: { enableResizing?: boolean; enableSorting?: boolean; data?: Person[]; }) => ( <BaseTable columns={personColumns} data={props.data || mockPeople} enableResizing={props.enableResizing} enableSorting={props.enableSorting} /> ); const ProductTable = (props: { enableResizing?: boolean; enableSorting?: boolean; data?: Product[]; }) => ( <BaseTable columns={productColumns} data={props.data || mockProducts} enableResizing={props.enableResizing} enableSorting={props.enableSorting} /> ); const meta: Meta<typeof PersonTable> = { title: "Table/Table", component: PersonTable, parameters: { docs: { description: { component: ` A flexible table component built with TanStack Table that supports: - **Column Resizing**: Drag column borders to resize - **Sorting**: Click column headers to sort data - **Row Selection**: Select rows for bulk actions - **Performance Optimized**: Uses CSS variables for efficient column sizing The table automatically handles empty states and provides a consistent API for different data types. `, }, }, }, argTypes: { enableResizing: { control: { type: "boolean" }, description: "Enable column resizing by dragging column borders", }, enableSorting: { control: { type: "boolean" }, description: "Enable sorting by clicking column headers", }, }, }; export default meta; type Story = StoryObj<typeof PersonTable>; /** * Basic table with resizable columns. * Drag the column borders to resize columns. */ export const Basic: Story = { args: { enableResizing: true, enableSorting: true, }, }; /** * Table with product data showing different cell types. */ export const Product = { render: () => <ProductTable enableResizing={true} enableSorting={true} />, }; /** * Table with no data to demonstrate empty state. */ export const Empty: Story = { args: { enableResizing: true, enableSorting: true, data: [], }, }; const SelectableStoryComponent = () => { const [selectedCount, setSelectedCount] = useState(0); return ( <div style={{ position: "relative" }}> {selectedCount > 0 && ( <FloatingToolbarContainer> <Toolbar> <TooltipTrigger delay={0}> <IconButton onPress={() => setSelectedCount(0)}> <Icon svg={<Icons.CloseOutline />} /> </IconButton> <Tooltip>Clear selection</Tooltip> </TooltipTrigger> <View paddingEnd="size-100"> <Text>{`${selectedCount} row${selectedCount > 1 ? "s" : ""} selected`}</Text> </View> <Button variant="danger" size="M" leadingVisual={<Icon svg={<Icons.TrashOutline />} />} > Delete </Button> </Toolbar> </FloatingToolbarContainer> )} <PersonTableSelectable enableResizing={true} enableSorting={true} onSelectionChange={setSelectedCount} /> </div> ); }; export const Selectable = { render: () => <SelectableStoryComponent />, };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Arize-ai/phoenix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server