Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions examples/ui/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ def _(table):
table.value
return

@app.cell
def _(mo):
# Demonstrate a long table with a sticky header and a custom max height
long_rows = [{"row": i, "first_name": f"First {i}", "last_name": f"Last {i}"} for i in range(200)]
long_table = mo.ui.table(
long_rows,
pagination=False,
max_height=300,
)
long_table
return (long_table,)



if __name__ == "__main__":
app.run()
13 changes: 10 additions & 3 deletions frontend/src/components/data-table/data-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import { getStableRowId } from "./utils";
interface DataTableProps<TData> extends Partial<DownloadActionProps> {
wrapperClassName?: string;
className?: string;
maxHeight?: number;
isSticky?: boolean;
columns: Array<ColumnDef<TData>>;
data: TData[];
// Sorting
Expand Down Expand Up @@ -95,6 +97,8 @@ interface DataTableProps<TData> extends Partial<DownloadActionProps> {
const DataTableInternal = <TData,>({
wrapperClassName,
className,
maxHeight,
isSticky,
columns,
data,
selection,
Expand Down Expand Up @@ -253,7 +257,10 @@ const DataTableInternal = <TData,>({
return (
<div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
<FilterPills filters={filters} table={table} />
<div className={cn(className || "rounded-md border overflow-hidden")}>
<div
className={cn(className || "rounded-md border overflow-hidden")}
style={maxHeight ? { maxHeight: `${maxHeight}px` } : undefined}
>
{onSearchQueryChange && enableSearch && (
<SearchBar
value={searchQuery || ""}
Expand All @@ -263,11 +270,11 @@ const DataTableInternal = <TData,>({
reloading={reloading}
/>
)}
<Table className="relative">
<Table className="relative" maxHeight={maxHeight}>
{showLoadingBar && (
<div className="absolute top-0 left-0 h-[3px] w-1/2 bg-primary animate-slide" />
)}
{renderTableHeader(table)}
{renderTableHeader(table, isSticky ?? true)}
<CellSelectionProvider>
<DataTableBody
table={table}
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/data-table/renderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useScrollIntoViewOnFocus } from "./range-focus/use-scroll-into-view";

export function renderTableHeader<TData>(
table: Table<TData>,
isSticky?: boolean,
): JSX.Element | null {
if (!table.getRowModel().rows?.length) {
return null;
Expand Down Expand Up @@ -57,7 +58,7 @@ export function renderTableHeader<TData>(
};

return (
<TableHeader>
<TableHeader sticky={Boolean(isSticky)}>
<TableRow>
{renderHeaderGroup(table.getLeftHeaderGroups())}
{renderHeaderGroup(table.getCenterHeaderGroups())}
Expand Down Expand Up @@ -106,9 +107,9 @@ export const DataTableBody = <TData,>({
const s = renderUnknownValue({ value: v, nullAsEmptyString: true });
idToValue.set(c.column.id, s);
}
return template.replace(variableRegex, (_substr, varName: string) => {
return template.replaceAll(variableRegex, (_substr, varName: string) => {
const val = idToValue.get(varName);
return val !== undefined ? val : `{{${varName}}}`;
return val === undefined ? `{{${varName}}}` : val;
});
}

Expand Down
56 changes: 35 additions & 21 deletions frontend/src/components/ui/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,44 @@ import * as React from "react";

import { cn } from "@/utils/cn";

const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto flex-1">
<table
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
maxHeight?: number;
}

const Table = React.forwardRef<HTMLTableElement, TableProps>(
({ className, maxHeight, ...props }, ref) => (
<div
className="w-full overflow-auto flex-1"
style={maxHeight ? { maxHeight: `${maxHeight}px` } : undefined}
>
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
),
);
Table.displayName = "Table";

interface TableHeaderProps
extends React.HTMLAttributes<HTMLTableSectionElement> {
sticky?: boolean;
}

const TableHeader = React.forwardRef<HTMLTableSectionElement, TableHeaderProps>(
({ className, sticky = false, ...props }, ref) => (
<thead
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
className={cn(
"[&_tr]:border-b bg-background",
sticky && "sticky top-0 z-10",
className,
)}
{...props}
/>
</div>
));
Table.displayName = "Table";

const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead
ref={ref}
className={cn("[&_tr]:border-b bg-background", className)}
{...props}
/>
));
),
);
TableHeader.displayName = "TableHeader";

const TableBody = React.forwardRef<
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/plugins/impl/DataTablePlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ interface Data<T> {
totalRows: number | TooManyRows;
pagination: boolean;
pageSize: number;
maxHeight?: number;
isSticky?: boolean;
selection: DataTableSelection;
showDownload: boolean;
showFilters: boolean;
Expand Down Expand Up @@ -253,6 +255,8 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
totalColumns: z.number(),
maxColumns: z.union([z.number(), z.literal("all")]).default("all"),
hasStableRowId: z.boolean().default(false),
maxHeight: z.number().optional(),
isSticky: z.boolean().optional(),
cellStyles: z.record(z.record(z.object({}).passthrough())).optional(),
hoverTemplate: z.string().optional(),
// Whether to load the data lazily.
Expand Down Expand Up @@ -649,6 +653,8 @@ export const LoadingDataTableComponent = memo(
toggleDisplayHeader={toggleDisplayHeader}
getRow={getRow}
cellId={cellId}
maxHeight={props.maxHeight}
isSticky={props.isSticky}
/>
);

Expand Down Expand Up @@ -715,6 +721,8 @@ const DataTableComponent = ({
preview_column,
getRow,
cellId,
isSticky,
maxHeight,
}: DataTableProps<unknown> &
DataTableSearchProps & {
data: unknown[];
Expand Down Expand Up @@ -894,6 +902,8 @@ const DataTableComponent = ({
data={data}
columns={columns}
className={className}
isSticky={isSticky}
maxHeight={maxHeight}
sorting={sorting}
totalRows={totalRows}
totalColumns={totalColumns}
Expand Down
25 changes: 21 additions & 4 deletions marimo/_plugins/ui/_impl/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@ def get_default_table_page_size() -> int:
except ContextNotInitializedError:
return 10
else:
return ctx.marimo_config["display"]["default_table_page_size"]
config = cast(Any, ctx.marimo_config)
value = config["display"]["default_table_page_size"]
return int(value)


def get_default_table_max_columns() -> int:
Expand All @@ -187,7 +189,9 @@ def get_default_table_max_columns() -> int:
except ContextNotInitializedError:
return DEFAULT_MAX_COLUMNS
else:
return ctx.marimo_config["display"]["default_table_max_columns"]
config = cast(Any, ctx.marimo_config)
value = config["display"]["default_table_max_columns"]
return int(value)


@mddoc
Expand Down Expand Up @@ -335,6 +339,9 @@ def style_cell(_rowId, _columnName, value):
hover_template (str, optional): A template for the hover text of the table.
max_columns (int, optional): Maximum number of columns to display. Defaults to the
configured default_table_max_columns (50 by default). Set to None to show all columns.
max_height (int, optional): Maximum height of the table body in pixels. When set,
the table becomes vertically scrollable and the header may be made sticky
in the UI to remain visible while scrolling. Defaults to None.
label (str, optional): A descriptive name for the table. Defaults to "".
"""

Expand Down Expand Up @@ -441,6 +448,7 @@ def __init__(
] = None,
style_cell: Optional[Callable[[str, str, Any], dict[str, Any]]] = None,
hover_template: Optional[str] = None,
max_height: Optional[int] = None,
# The _internal_* arguments are for overriding and unit tests
# table should take the value unconditionally
_internal_column_charts_row_limit: Optional[int] = None,
Expand Down Expand Up @@ -471,8 +479,9 @@ def __init__(

# Handle max_columns: use config default if not provided, None means "all"
if max_columns == MAX_COLUMNS_NOT_PROVIDED:
self._max_columns = get_default_table_max_columns()
max_columns_arg = self._max_columns
default_max_columns = get_default_table_max_columns()
self._max_columns = default_max_columns
max_columns_arg = default_max_columns
elif max_columns is None:
self._max_columns = None
max_columns_arg = "all"
Expand Down Expand Up @@ -671,6 +680,14 @@ def __init__(
"hover-template": hover_template,
"lazy": _internal_lazy,
"preload": _internal_preload,
**(
{
"max-height": int(max_height),
"is-sticky": True,
}
if max_height is not None
else {}
),
},
on_change=on_change,
functions=(
Expand Down
14 changes: 14 additions & 0 deletions tests/_plugins/ui/_impl/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -1919,6 +1919,20 @@ def test_default_table_max_columns():
assert get_default_table_max_columns() == DEFAULT_MAX_COLUMNS


def test_table_max_height_and_sticky_args():
table = ui.table(
[{"a": i} for i in range(100)], pagination=False, max_height=300
)
# Backend should expose optional UI hints when max_height is set
assert table._component_args["max-height"] == 300
assert table._component_args["is-sticky"] is True

table_no_height = ui.table([1, 2, 3])
# Keys may be absent when not configured
assert "max-height" not in table_no_height._component_args
assert "is-sticky" not in table_no_height._component_args


def test_calculate_top_k_rows():
table = ui.table({"A": [1, 3, 3, None, None]})
result = table._calculate_top_k_rows(
Expand Down
Loading