Skip to content

FRD-032: Resource Table

FieldValue
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
Target Releasev2.0.0
TypeWidget
SizeM
PriorityP1 — SaaS Widget

Document Summary

Build a generic Resource Table widget that provides CRUD table UX with sort, filter, paginate, bulk-select, search, and standardized empty/loading/error states. It builds on the DataGrid primitive and generalizes the patterns demonstrated by people-table.tsx and billing-history-table.tsx into a reusable, consumer-configurable widget.


Introduction

Overview

The design system has DataGrid as a powerful table primitive and two specialized table widgets (PeopleTable, BillingHistoryTable). However, every new CRUD resource list (projects, environments, deployments, documents) requires teams to re-compose DataGrid with search, filters, bulk actions, and state handling. A generic ResourceTable widget would provide this composition out of the box, configured via props rather than rebuilt from scratch.

Goals

  • Provide a ResourceTable widget composing DataGrid with ListToolbar, search, filter presets, bulk actions, and state handling.
  • Support consumer-defined column configurations using the existing DataGridColumn type.
  • Provide built-in search integration that filters across all searchable columns.
  • Handle loading (skeleton rows), empty (configurable message + CTA), and error (message + retry) states.
  • Support row-level actions via a configurable actions column.
  • Ship with Storybook stories demonstrating diverse resource types and unit tests.

Non-Goals

  • Server-side pagination/filtering (widget operates on client-side data; server-side is a consumer responsibility via callbacks).
  • Inline row editing (covered by InlineEdit in FRD-028).
  • Tree/hierarchical table display.

Scope

In Scope

ItemDescription
ResourceTable widgetComposed DataGrid with toolbar, search, filters, bulk actions, pagination, and state handling.
Column configurationConsumer passes DataGridColumn[] to define columns, including custom cell renderers.
Search integrationBuilt-in search that filters across columns marked as searchable.
Filter presetsConsumer-defined filter presets rendered as toolbar filter chips or a filter popover.
Bulk actionsCheckbox column with bulk action buttons (e.g., “Delete selected”, “Export selected”).
Row actionsPer-row action dropdown (consumer provides action definitions).
State handlingLoading skeleton, empty state with CTA, error state with retry.
PaginationBuilt-in pagination with configurable page sizes.
Stories and testsStorybook stories for various resource types; unit tests for search, filter, bulk selection.

Out of Scope

ItemReason
Server-side paginationConsumer manages via onPageChange callback and provides paginated data.
Inline editingSeparate FRD (FRD-028 InlineEdit).
Column reordering / drag-to-resizeDataGrid primitive concern; future enhancement.
Virtualized rowsDataGrid handles virtualization internally for large datasets.

Users and Pain Points

UserPain Point
SaaS developersRebuild DataGrid + toolbar + search + filters + pagination for every new resource type.
Product designersInconsistent table patterns across different resource pages.
QA teamsEach custom table implementation has different state handling bugs.
End usersInconsistent search, filter, and pagination UX across different resource lists.

Definitions

TermDefinition
ResourceAny CRUD entity displayed in a table: users, projects, deployments, keys, invoices, etc.
Bulk actionAn action applied to all currently selected rows (e.g., delete, export, archive).
Row actionAn action specific to a single row, rendered in an actions column dropdown.
Filter presetA named, pre-configured filter (e.g., “Active only”, “Created this week”) rendered as a toolbar chip.
Searchable columnA column whose accessor value is included in the full-text search.

Current State

  • DataGrid (src/components/ui/data-grid.tsx): Full-featured table with sorting, filtering, pagination, row selection via TanStack Table. Accepts ColumnDef[], data, sorting/filter/pagination state.
  • ListToolbar (src/components/ui/list-toolbar.tsx): Toolbar with search input, filter buttons, and action buttons. Used above DataGrid.
  • PeopleTable (src/components/widgets/people-table.tsx): Specialized DataGrid for team members with avatar, name, email, role, status columns.
  • BillingHistoryTable (src/components/patterns/billing-history-table.tsx): Specialized DataGrid for invoices.
  • StatusCard: Used for empty/error states.
  • No generic ResourceTable widget exists.

Proposed Solution

Build src/components/widgets/resource-table.tsx as a composed widget that wraps DataGrid, ListToolbar, and state handling into a single configurable component.

Architecture

ResourceTable
├── ListToolbar (search + filter chips + bulk action buttons)
├── DataGrid (columns + data + sort + paginate + select)
│ ├── Consumer-defined columns
│ └── Optional actions column (auto-appended)
├── Pagination (page controls + page size selector)
└── State overlays (loading skeleton / empty card / error card)

Configuration Model

Instead of rebuilding columns for every resource, consumers pass:

  1. columns — standard DataGridColumn[] with optional searchable: boolean flag.
  2. data — array of resource objects.
  3. rowActions — optional array of action definitions { label, icon, onClick, variant } rendered in per-row dropdown.
  4. bulkActions — optional array of bulk action definitions rendered in toolbar when rows are selected.
  5. filterPresets — optional named filters rendered as toolbar chips.
  6. emptyState / errorState — configurable content for empty and error displays.

State Handling

The widget reads loading, error, and data.length to determine which view to render:

  • loading === true → Skeleton rows (shape-matched to column count).
  • error is defined → StatusCard with error message and retry button.
  • data.length === 0 && !loading → Empty state card with configurable CTA.

Requirements

Requirement Priorities

  • Must Have: Column configuration, search, sort, paginate, empty/loading/error states.
  • Should Have: Bulk select + bulk actions, row actions dropdown, filter presets.
  • Could Have: Column visibility toggle, CSV export built-in, saved filter presets.

Functional Requirements

IDRequirementPriority
FR-01Widget accepts columns, data, and renders a DataGrid with sorting enabled on all columns.Must
FR-02Built-in search input filters rows across all columns marked searchable: true.Must
FR-03Pagination controls with configurable page sizes (default [10, 25, 50]).Must
FR-04Loading state renders skeleton rows matching the column count and configured page size.Must
FR-05Empty state renders a configurable StatusCard with title, description, and optional CTA button.Must
FR-06Error state renders a StatusCard with error message and retry button.Must
FR-07rowActions prop auto-appends an actions column with a dropdown menu per row.Should
FR-08bulkActions prop enables checkbox column; selected rows reveal bulk action buttons in the toolbar.Should
FR-09filterPresets prop renders filter chips in the toolbar; clicking a chip applies the filter.Should
FR-10onRowClick callback fires when a row is clicked (for navigation to detail view).Should
FR-11Widget exposes ref to the outer container and tableRef to the TanStack Table instance for advanced control.Could

Non-Functional Requirements

IDRequirementTarget
NFR-01Renders 100 rows with 8 columns in < 50ms.Measured via React Profiler.
NFR-02Bundle size< 5 KB gzipped (excluding shared DataGrid deps).
NFR-03Search debounce200ms debounce on search input to avoid excessive filtering.
NFR-04Dark modeFull token-based dark mode support.
NFR-05ResponsiveColumn hiding on narrow viewports via responsive column config.

API/Interface Requirements

interface ResourceTableColumn<T> extends DataGridColumn<T> {
searchable?: boolean;
}
interface RowAction<T> {
label: string;
icon?: ReactNode;
onClick: (row: T) => void;
variant?: "default" | "destructive";
disabled?: boolean | ((row: T) => boolean);
}
interface BulkAction<T> {
label: string;
icon?: ReactNode;
onClick: (selectedRows: T[]) => void;
variant?: "default" | "destructive";
}
interface FilterPreset {
id: string;
label: string;
filter: (row: any) => boolean;
}
interface ResourceTableProps<T extends { id: string }> {
columns: ResourceTableColumn<T>[];
data: T[];
loading?: boolean;
error?: string;
onRetry?: () => void;
rowActions?: RowAction<T>[];
bulkActions?: BulkAction<T>[];
filterPresets?: FilterPreset[];
onRowClick?: (row: T) => void;
pageSizes?: number[]; // default [10, 25, 50]
defaultPageSize?: number; // default 10
searchPlaceholder?: string;
emptyTitle?: string;
emptyDescription?: string;
emptyAction?: { label: string; onClick: () => void };
className?: string;
}

Accessibility Requirements

IDRequirement
A11Y-01DataGrid uses role="grid" with proper role="row" and role="gridcell" semantics.
A11Y-02Row action dropdown is keyboard accessible; opens on Enter/Space, navigates with arrows.
A11Y-03Bulk action buttons announce the number of selected rows via aria-label.
A11Y-04Search input has aria-label and clears on Escape.
A11Y-05Pagination controls announce current page and total pages.
A11Y-06All components pass axe-core automated checks with zero violations.

Content and Documentation Requirements

IDRequirement
DOC-01Storybook docs page with usage guidelines, prop table, and interactive examples.
DOC-02Recipe showing ResourceTable configured for 3+ different resource types (users, projects, invoices).
DOC-03Migration guide for teams using raw DataGrid + ListToolbar composition.

Dependencies

DependencyTypeRisk
src/components/ui/data-grid.tsxInternalLow — core table primitive.
src/components/ui/list-toolbar.tsxInternalLow — toolbar with search and filters.
src/components/ui/pagination.tsxInternalLow — page controls.
src/components/ui/status-card.tsxInternalLow — empty/error states.
src/components/ui/skeleton.tsxInternalLow — loading states.
@tanstack/react-tableExistingLow — already used by DataGrid.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
Widget is too opinionated; teams need more customizationMediumMediumExpose tableRef for advanced TanStack Table control; keep props composable.
Search performance degrades with large datasetsLowMedium200ms debounce; document that server-side search is preferred for 1000+ rows.
Generic typing makes TypeScript inference complexMediumLowUse constrained generic T extends { id: string }; provide concrete examples.

Open Questions

#QuestionOwnerStatus
OQ-01Should ResourceTable support server-side pagination via onPageChange callback?David HolmesOpen
OQ-02Should column visibility toggles be built in or deferred?David HolmesOpen
OQ-03Should the widget support “infinite scroll” as an alternative to pagination?David HolmesOpen

Acceptance Criteria

#Criterion
AC-01Widget renders a table with consumer-defined columns, sorting, and pagination.
AC-02Search input filters rows across searchable columns with 200ms debounce.
AC-03Loading state shows skeleton rows; empty state shows configurable card with CTA; error state shows retry.
AC-04Row actions render in a per-row dropdown menu.
AC-05Bulk selection enables bulk action buttons in the toolbar.
AC-06All components pass axe-core checks with zero violations.
AC-07Storybook stories demonstrate 3+ resource types (users, projects, invoices).
AC-08Unit tests cover search, sort, pagination, bulk selection, and state transitions.
AC-09pnpm typecheck and pnpm vitest run --project unit pass with zero errors.

LLM Handoff Instructions

When implementing this FRD:

  1. Create src/components/widgets/resource-table.tsx. Study people-table.tsx and billing-history-table.tsx for the composition pattern.
  2. Use DataGrid as the core table. Pass columns, data, sorting, pagination, and selection props through.
  3. Compose ListToolbar above the DataGrid. Wire the search input to DataGrid’s global filter. Render filter preset chips and bulk action buttons conditionally.
  4. Auto-append actions column: If rowActions is provided, append a final column with a DropdownMenu containing the action items.
  5. State handling: Check loading, error, data.length in order. Render skeleton/error/empty as appropriate. Empty state is gated on !loading && data.length === 0.
  6. Generic typing: ResourceTable&lt;T extends { id: string }&gt; — the id constraint is needed for row selection keys.
  7. Stories in src/components/widgets/resource-table.stories.tsx. Create stories for: Default (users), ProjectsExample, InvoicesExample, Loading, Empty, Error, BulkActions, WithFilters.
  8. Tests in src/components/widgets/resource-table.test.tsx.

Key files:

  • src/components/ui/data-grid.tsx — table primitive API.
  • src/components/ui/list-toolbar.tsx — toolbar API.
  • src/components/widgets/people-table.tsx — composition pattern reference.

Decision Log

DateDecisionRationale
2026-05-26Client-side data model; no built-in server-side pagination.Widget stays simple; server-side pagination is a consumer concern using onPageChange callback.
2026-05-26Require id: string on all resource objects.Needed for row selection, keying, and action targeting.
2026-05-26Auto-append actions column rather than requiring consumers to define it.Reduces boilerplate for the most common pattern.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.