Data table page
A page whose main block is a table, with filters, saved views and the table taking the rest of the height.
A data table page is an app page whose main block is a table of records, such as orders: the page header, saved views as tabs, the filters, then the table filling the rest of the height. Build it from AppPage fill, SavedViews, FilterBar and DataTable; it adds no component of its own.
How it fits together
Room for the table
AppPage fillgives the page's last block the height left under the header, tabs and filters, so the table scrolls inside it with its header row sticky, and the page itself doesn't scroll.Saved views
SavedViewskeeps named filter sets as tabs, the chosen one on a sliding raised pill, with every view and its actions in a menu.Filters
FilterBarholds the filters in force as pills, which wrap rather than fold, since a person has to see and remove them.Columns
ColumnSettingspicks which columns show, and reorders them by dragging or with Alt and the arrow keys.
Here is a whole orders page: two saved views, a status filter and a search in the filter bar, and a table that sorts by its total. The orders are written into the file to keep it short; an app's would come from its data.
import { useState } from "react";
import type { ReactElement } from "react";
import { Stack } from "@butteryui/core";
import {
AppPage,
Button,
DataTable,
FilterBar,
FilterPill,
PageHeader,
SavedViews,
SearchField,
StatusBadge,
} from "@butteryui/react";
import type {
DataTableColumn,
DataTableSort,
SavedView,
} from "@butteryui/react";
const STATUSES = ["Paid", "Unpaid", "Refunded"] as const;
type Status = (typeof STATUSES)[number];
interface Order {
readonly id: string;
readonly customer: string;
readonly status: Status;
readonly total: number;
}
const ORDERS: readonly Order[] = [
{ id: "ORD-3101", customer: "Larkspur Café", status: "Paid", total: 64 },
{ id: "ORD-3102", customer: "Northgate Bakery", status: "Unpaid", total: 38 },
{ id: "ORD-3103", customer: "Wren Street Deli", status: "Paid", total: 112 },
{
id: "ORD-3104",
customer: "Saltmarsh Kitchen",
status: "Refunded",
total: 21,
},
{ id: "ORD-3105", customer: "Larkspur Café", status: "Unpaid", total: 47 },
];
const VIEWS: readonly SavedView[] = [
{ id: "all", label: "All orders" },
{ id: "open", label: "Waiting on payment" },
];
const TONES = {
Paid: "success",
Unpaid: "warning",
Refunded: "neutral",
} as const;
const COLUMNS: readonly DataTableColumn<Order>[] = [
{
id: "id",
header: "Order",
cell: (order) => <DataTable.Id value={order.id} />,
},
{
id: "customer",
header: "Customer",
minWidth: "md",
grow: 2,
cell: (order) => order.customer,
},
{
id: "status",
header: "Status",
skeleton: "badge",
cell: (order) => (
<StatusBadge tone={TONES[order.status]}>{order.status}</StatusBadge>
),
},
{
id: "total",
header: "Total",
align: "end",
sortable: true,
cell: (order) => <DataTable.Amount value={order.total} currency="USD" />,
},
];
export function OrdersPage(): ReactElement {
const [view, setView] = useState("all");
const [status, setStatus] = useState<Status | undefined>(undefined);
const [picking, setPicking] = useState(false);
const [query, setQuery] = useState("");
const [sort, setSort] = useState<DataTableSort | null>(null);
const clearAll = (): void => {
setStatus(undefined);
setQuery("");
};
const search = query.trim().toLowerCase();
const rows = ORDERS.filter(
(order) =>
(view === "all" || order.status === "Unpaid") &&
(status === undefined || order.status === status) &&
order.customer.toLowerCase().includes(search),
).sort((a, b) =>
sort === null
? 0
: (a.total - b.total) * (sort.direction === "ascending" ? 1 : -1),
);
return (
<AppPage header={<PageHeader title="Orders" />} fill>
<AppPage.Section>
<SavedViews
label="Order views"
views={VIEWS}
value={view}
onValueChange={setView}
/>
<FilterBar
onClearAll={clearAll}
actions={
<SearchField
label="Search orders"
placeholder="Customer"
variant="outline"
value={query}
onValueChange={setQuery}
/>
}
>
<FilterPill
label="Status"
value={status}
open={picking}
onOpenChange={setPicking}
onRemove={() => {
setStatus(undefined);
}}
>
<Stack gap={1}>
{STATUSES.map((choice) => (
<Button
key={choice}
variant="ghost"
size="sm"
onClick={() => {
setStatus(choice);
setPicking(false);
}}
>
{choice}
</Button>
))}
</Stack>
</FilterPill>
</FilterBar>
<DataTable
label="Orders"
columns={COLUMNS}
rows={rows}
rowKey={(order) => order.id}
rowLabel={(order) => order.id}
sort={sort}
onSortChange={setSort}
onClearAll={clearAll}
/>
</AppPage.Section>
</AppPage>
);
}Two places show it working:
- The FilterBar examples show the filters and the table working together.
- The roastery demo's orders page is the full version, with saved views you can add and rename,
ColumnSettings, row actions and pages of results.
See DataTable, FilterBar and SavedViews for each part.
Related
- DataTable
A table of records: columns with minimum widths and shares, selection, sorting, row actions, a sticky header and first column, skeleton rows and pagination.
- FilterBar
A row of filter pills over a list that wrap as they're applied, with Clear filters and actions, folding into one button on a phone.
- SavedViews
A track of saved views of a list, the chosen one on a sliding raised pill, with every view and its actions in a menu.
- ColumnSettings
A button that opens a list of a table's columns to show, hide and reorder by dragging or with Alt and the arrow keys.