Features
Each example turns on one feature of the Table. They all show the same pipeline runs.
Layout#
Column resizing#
Drag the right edge of a column header to resize the column. Double-click the edge to fit the widest visible cell. Keep the widths in state with columnWidths and onColumnWidthChanged, or they reset when the table remounts. minColumnWidth and maxColumnWidth set the limits. Rows resize the same way from the row header.
import { Cell, Column, Table } from "@clawscale/react/table";
import { useState } from "react";
import { RUNS } from "../_data";
export default function TableResizing() {
const [widths, setWidths] = useState([180, 130, 130]);
return (
<div style={{ height: 240 }}>
<Table
numRows={RUNS.length}
columnWidths={widths}
minColumnWidth={80}
onColumnWidthChanged={(index, size) =>
setWidths((current) => current.map((width, i) => (i === index ? size : width)))
}
>
<Column name="Pipeline" cellRenderer={(row) => <Cell>{RUNS[row]?.pipeline}</Cell>} />
<Column name="Region" cellRenderer={(row) => <Cell>{RUNS[row]?.region}</Cell>} />
<Column name="Owner" cellRenderer={(row) => <Cell>{RUNS[row]?.owner}</Cell>} />
</Table>
</div>
);
}Column reordering#
enableColumnReordering shows a drag handle in each column header. Reorder your column list in onColumnsReordered, where Utils.reorderArray does the index math. Give each column a stable id, so its width moves with it, and pass the list in cellRendererDependencies, so the cells re-render.
import { Cell, Column, Table, Utils } from "@clawscale/react/table";
import { useState } from "react";
import { RUNS } from "../_data";
type Field = "pipeline" | "status" | "region" | "owner";
const COLUMNS: { field: Field; name: string }[] = [
{ field: "pipeline", name: "Pipeline" },
{ field: "status", name: "Status" },
{ field: "region", name: "Region" },
{ field: "owner", name: "Owner" },
];
export default function TableReordering() {
const [columns, setColumns] = useState(COLUMNS);
return (
<div style={{ height: 240 }}>
<Table
numRows={RUNS.length}
enableColumnReordering
cellRendererDependencies={[columns]}
onColumnsReordered={(from, to, length) =>
setColumns((current) => Utils.reorderArray(current, from, to, length) ?? current)
}
>
{columns.map(({ field, name }) => (
<Column key={field} id={field} name={name} cellRenderer={(row) => <Cell>{RUNS[row]?.[field]}</Cell>} />
))}
</Table>
</div>
);
}Frozen columns and rows#
numFrozenColumns pins columns to the left edge and numFrozenRows pins rows to the top. Scroll the table: the pipeline name and the first two runs stay in place.
import { Cell, Column, Table } from "@clawscale/react/table";
import { formatDuration, formatRows, type PipelineRun, RUNS } from "../_data";
const COLUMNS: [name: string, value: (run: PipelineRun) => string][] = [
["Pipeline", (run) => run.pipeline],
["Status", (run) => run.status],
["Rows", (run) => formatRows(run.rows)],
["Duration", (run) => formatDuration(run.duration)],
["Region", (run) => run.region],
["Owner", (run) => run.owner],
["Started", (run) => run.started],
["Run", (run) => run.id],
];
export default function TableFrozen() {
return (
<div style={{ height: 220 }}>
<Table numRows={RUNS.length} numFrozenColumns={1} numFrozenRows={2}>
{COLUMNS.map(([name, value]) => (
<Column
key={name}
name={name}
cellRenderer={(row) => {
const run = RUNS[row];
return <Cell>{run && value(run)}</Cell>;
}}
/>
))}
</Table>
</div>
);
}Interaction#
Selection modes#
selectionModes sets what a click or a drag selects: cells, rows, columns or the whole table. SelectionModes holds the common sets, and an array of RegionCardinality values builds any other. Read the selection with onSelection, or control it with selectedRegions.
import { SegmentedControl } from "@clawscale/react";
import { Cell, Column, RegionCardinality, SelectionModes, Table } from "@clawscale/react/table";
import { useState } from "react";
import { RUNS } from "../_data";
const MODES = {
All: SelectionModes.ALL,
Rows: SelectionModes.ROWS_ONLY,
Columns: SelectionModes.COLUMNS_ONLY,
Cells: [RegionCardinality.CELLS],
None: SelectionModes.NONE,
};
type Mode = keyof typeof MODES;
export default function TableSelection() {
const [mode, setMode] = useState<Mode>("Rows");
return (
<>
<div>
<SegmentedControl
inline
size="small"
options={Object.keys(MODES).map((label) => ({ label, value: label }))}
value={mode}
onValueChange={(value) => setMode(value as Mode)}
/>
</div>
<div style={{ height: 240 }}>
<Table numRows={RUNS.length} selectionModes={MODES[mode]}>
<Column name="Pipeline" cellRenderer={(row) => <Cell>{RUNS[row]?.pipeline}</Cell>} />
<Column name="Status" cellRenderer={(row) => <Cell>{RUNS[row]?.status}</Cell>} />
<Column name="Owner" cellRenderer={(row) => <Cell>{RUNS[row]?.owner}</Cell>} />
</Table>
</div>
</>
);
}Editable cells#
Return an EditableCell from cellRenderer. Double-click an owner to edit it: Enter confirms and Escape cancels. onConfirm receives the new value. Pass the edited state in cellRendererDependencies, so the cells re-render.
import { Cell, Column, EditableCell, Table } from "@clawscale/react/table";
import { useState } from "react";
import { RUNS } from "../_data";
export default function TableEditable() {
const [owners, setOwners] = useState(() => RUNS.map((run) => run.owner));
const rename = (row: number, owner: string) =>
setOwners((current) => current.map((value, index) => (index === row ? owner : value)));
return (
<div style={{ height: 240 }}>
<Table numRows={RUNS.length} cellRendererDependencies={[owners]}>
<Column name="Pipeline" cellRenderer={(row) => <Cell>{RUNS[row]?.pipeline}</Cell>} />
<Column
name="Owner"
cellRenderer={(row) => <EditableCell value={owners[row]} onConfirm={(owner) => rename(row, owner)} />}
/>
</Table>
</div>
);
}Display#
Loading states#
loadingOptions on the table replaces cells, column headers and row headers with skeletons. Column accepts loadingOptions for one column, and Cell accepts loading for one cell. The most specific setting wins.
import { Switch } from "@clawscale/react";
import { Cell, Column, Table, TableLoadingOption } from "@clawscale/react/table";
import { useState } from "react";
import { formatRows, RUNS } from "../_data";
const EVERYTHING = [TableLoadingOption.CELLS, TableLoadingOption.COLUMN_HEADERS, TableLoadingOption.ROW_HEADERS];
export default function TableLoading() {
const [loading, setLoading] = useState(true);
return (
<>
<Switch checked={loading} label="Loading" onChange={(event) => setLoading(event.currentTarget.checked)} />
<div style={{ height: 240 }}>
<Table numRows={RUNS.length} loadingOptions={loading ? EVERYTHING : []}>
<Column name="Pipeline" cellRenderer={(row) => <Cell>{RUNS[row]?.pipeline}</Cell>} />
<Column name="Region" cellRenderer={(row) => <Cell>{RUNS[row]?.region}</Cell>} />
<Column name="Rows" cellRenderer={(row) => <Cell>{formatRows(RUNS[row]?.rows)}</Cell>} />
</Table>
</div>
</>
);
}Truncated and JSON cells#
TruncatedFormat cuts long text to fit its cell, and JSONFormat does the same for objects. With detectTruncation, a button appears at the right edge of a cut cell. Click it to read the full value in a popover.
import { Cell, Column, JSONFormat, Table, TruncatedFormat } from "@clawscale/react/table";
import { RUNS } from "../_data";
export default function TableFormats() {
return (
<div style={{ height: 240 }}>
<Table numRows={RUNS.length} columnWidths={[140, 280, 240]}>
<Column name="Pipeline" cellRenderer={(row) => <Cell>{RUNS[row]?.pipeline}</Cell>} />
<Column
name="Message"
cellRenderer={(row) => (
<Cell>
<TruncatedFormat detectTruncation>{RUNS[row]?.message}</TruncatedFormat>
</Cell>
)}
/>
<Column
name="Record"
cellRenderer={(row) => (
<Cell>
<JSONFormat detectTruncation>{RUNS[row]}</JSONFormat>
</Cell>
)}
/>
</Table>
</div>
);
}Striped rows#
Pass className="bp6-table-striped" to the table to shade every other row.
import { Cell, Column, Table } from "@clawscale/react/table";
import { formatRows, RUNS } from "../_data";
export default function TableStriped() {
return (
<div style={{ height: 240 }}>
<Table className="bp6-table-striped" numRows={RUNS.length}>
<Column name="Pipeline" cellRenderer={(row) => <Cell>{RUNS[row]?.pipeline}</Cell>} />
<Column name="Region" cellRenderer={(row) => <Cell>{RUNS[row]?.region}</Cell>} />
<Column name="Rows" cellRenderer={(row) => <Cell>{formatRows(RUNS[row]?.rows)}</Cell>} />
</Table>
</div>
);
}