Data Table
A table component with sorting, filtering, and pagination compatible with VitNode API.
Preview
Usage
import { DataTable } from "@vitnode/core/components/table/data-table";<DataTable
id="users-table"
columns={[
{ id: "name", label: "Name" },
{ id: "email", label: "Email" },
{ id: "role", label: "Role" },
{ id: "status", label: "Status" },
{ id: "id", label: "Actions" },
]}
edges={data.edges}
pageInfo={data.pageInfo}
order={{
defaultOrder: {
column: "name",
order: "asc",
},
}}
/>Prop
Type
Cell Renderer
You can customize how each cell is rendered using the cell property. The renderer function receives the current row data and all table data as parameters.
<DataTable
id="users-table"
columns={[
{
id: "id",
label: "Id",
cell: ({ row, allData }) => (
<span>
{row.id} - all data {allData.length}
</span>
),
},
{ id: "createdAt", label: "Created at" },
]}
edges={data.edges}
pageInfo={data.pageInfo}
order={{
columns: ["createdAt", "id"],
defaultOrder: {
order: "desc",
},
}}
/>Order Configuration
If you want to enable sorting on specific columns, you can specify them in the columns property.
order={{
columns: ['createdAt', 'id', 'name'],
defaultOrder: {
column: 'createdAt',
order: 'desc',
}
}}Search
Set the search prop to true to render a search input above the table. You can optionally customize the placeholder with searchPlaceholder.
<DataTable
id="users-table"
search
searchPlaceholder="Search users..."
columns={[
{ id: "name", label: "Name" },
{ id: "email", label: "Email" },
]}
edges={data.edges}
pageInfo={data.pageInfo}
order={{
defaultOrder: {
column: "name",
order: "asc",
},
}}
/>The input writes the term to the ?search= query parameter (debounced) and reloads the page, so it works out of the box with server-side data fetching. The columns that are actually searched are defined on the API route.
Configure the API route
Enabling search only renders the input. You must also tell the backend which
columns to search across — see the Search guide.
Filters
Pass the filters prop to render one or more faceted, multi-select dropdowns above the table (next to the search input). Each filter controls its own URL query parameter, so it works out of the box with server-side data fetching. Multiple selected values are stored as a comma-separated list, e.g. ?roleId=1,3, and changing a filter resets the pagination cursor.
Prop
Type
Static filters
When the set of options is small and known ahead of time, pass them directly via options. The dropdown list is filtered on the client.
<DataTable
id="users-table"
filters={[
{
id: "status",
label: "Status",
options: [
{ value: "active", label: "Active" },
{ value: "banned", label: "Banned" },
],
},
]}
columns={[
{ id: "name", label: "Name" },
{ id: "email", label: "Email" },
]}
edges={data.edges}
pageInfo={data.pageInfo}
order={{
defaultOrder: {
column: "name",
order: "asc",
},
}}
/>Async filters
When the options come from the API (for example a large or searchable list), provide an onSearch callback instead of options. It runs — debounced — as the user types and should return results already filtered and capped by the server. Use a Server Action so the lookup runs server-side:
"use server";
import type { FilterOption } from "@vitnode/core/components/table/filters";
import { adminModule } from "@/api/modules/admin/admin.module";
import { RoleFormat } from "@vitnode/core/components/role-format";
import { fetcher } from "@vitnode/core/lib/fetcher";
export const searchRoles = async (search: string): Promise<FilterOption[]> => {
const res = await fetcher(adminModule, {
path: "/list",
method: "get",
module: "admin/roles",
args: {
query: { search, first: "20" },
},
withPagination: true,
});
if (res.status !== 200) {
return [];
}
const data = await res.json();
return data.edges.map(role => ({
value: String(role.id),
label: <RoleFormat role={role} />,
keywords: role.name.map(item => item.name),
}));
};filters={[
{
id: "roleId",
label: "Group",
onSearch: searchRoles,
},
]}Configure the API route
A filter only writes its values to the URL — the backend must read the query
parameter and apply the matching where clause. For a comma-separated
multi-select, split the value and use inArray:
const roleIds = (query.roleId?.split(",") ?? [])
.filter(Boolean)
.map(Number)
.filter((id) => !Number.isNaN(id));
// pass to withPagination:
where: roleIds.length ? inArray(core_users.roleId, roleIds) : undefined,Complete Example
Here's a complete example showing how to use the DataTable component in a page:
import {
DataTable,
SearchParamsDataTable,
} from "@vitnode/core/components/table/data-table";
import { userModule } from "@/api/modules/user/user.module";
import { fetcher } from "@vitnode/core/lib/fetcher";
export const UsersView = async ({
searchParams,
}: {
searchParams: Promise<SearchParamsDataTable>;
}) => {
const query = await searchParams;
const res = await fetcher(userModule, {
path: "/users",
method: "get",
module: "user",
args: {
query,
},
withPagination: true,
});
const data = await res.json();
return (
<DataTable
id="users-table"
columns={[
{
id: "id",
label: "ID",
},
{
id: "username",
label: "Username",
cell: ({ row }) => (
<span className="font-medium">{row.username}</span>
),
},
{ id: "email", label: "Email" },
{ id: "createdAt", label: "Created at" },
]}
edges={data.edges}
order={{
columns: ["id", "username", "email", "createdAt"],
defaultOrder: {
column: "createdAt",
order: "desc",
},
}}
pageInfo={data.pageInfo}
/>
);
};