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',
  }
}}

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:

search-roles.action.tsx
"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}
    />
  );
};

On this page