Skip to main content

Data Tables and Lists

The Ever Works dashboard uses several patterns for displaying collections of data: virtualized item lists for large datasets, HTML tables for tabular data, card grids for plugin browsing, and divided lists for member management. This page documents each pattern and the components that implement them.

Component Overview

ComponentPatternVirtualizedSource
ItemsListCard grid / list with search and filtersYescomponents/works/detail/items/ItemsList.tsx
HistoryTableHTML table with status badgesNocomponents/works/detail/history/HistoryTable.tsx
PluginGridCard grid with category groupingNocomponents/plugins/PluginGrid.tsx
WorkPluginsListCard list with capability selectorsNocomponents/works/detail/plugins/WorkPluginsList.tsx
MembersListDivided list with role badgesNocomponents/works/detail/members/MembersList.tsx

ItemsList -- Virtualized Item Grid

Source: apps/web/src/components/works/detail/items/ItemsList.tsx

The ItemsList component displays work items in a searchable, filterable, virtualized grid. It uses @tanstack/react-virtual to efficiently render hundreds or thousands of items without performance degradation.

Architecture

Props

interface ItemsListProps {
items: ItemData[];
addItemRef?: React.RefObject<((item: ItemData) => void) | null>;
}

The addItemRef allows parent components to imperatively add new items to the list (e.g., when a user creates an item through the UI).

Features

FeatureDescription
Text searchFilters items by name and description
Category filterDropdown to filter by category
View toggleSwitch between grid and list views
VirtualizationOnly renders visible rows plus 5 overscan rows
Responsive columns1 column on mobile, 2 on tablet, 3 on desktop
Memoized cardsReact.memo wrapper prevents unnecessary re-renders
Sort orderFeatured items first, then by order field, then alphabetical
Duplicate preventionNew items are checked by slug to prevent duplicates

Responsive Column Layout

The useColumnCount hook adjusts the grid column count based on viewport width:

Viewport WidthView ModeColumns
< 640pxGrid1
640px -- 1023pxGrid2
>= 1024pxGrid3
AnyList1

Virtualization Details

The virtualizer is configured with these parameters:

const rowVirtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () =>
viewMode === 'list'
? LIST_ITEM_HEIGHT + GAP // 80 + 16 = 96px
: GRID_ROW_HEIGHT + GAP, // 200 + 16 = 216px
overscan: 5,
scrollMargin
});
ParameterValueDescription
estimateSize96px (list) / 216px (grid)Estimated row height
overscan5Extra rows rendered above and below the viewport
scrollMarginCalculatedOffset from the scroll container to the list component

Items are grouped into rows based on the column count. In grid mode with 3 columns, every 3 items form one virtual row.

Sort Algorithm

Items are sorted in this priority order:

  1. Featured items first -- items with featured: true appear at the top
  2. Order field -- items with a numeric order value are sorted ascending
  3. Alphabetical -- remaining items are sorted by name using localeCompare
function sortItems(items: ItemData[]): ItemData[] {
return [...items].sort((a, b) => {
if (!!a.featured !== !!b.featured) return a.featured ? -1 : 1;
const orderA = typeof a.order === 'number' ? a.order : Infinity;
const orderB = typeof b.order === 'number' ? b.order : Infinity;
if (orderA !== orderB) return orderA - orderB;
return a.name.localeCompare(b.name);
});
}

HistoryTable -- Generation History

Source: apps/web/src/components/works/detail/history/HistoryTable.tsx

The HistoryTable displays work generation run history as an HTML table with status badges, duration formatting, and token usage.

Columns

ColumnFieldDescription
RunstatusStatus badge with color coding
Started AtstartedAt / createdAtTimestamp rendered by ShowDateTime
DurationdurationInSecondsFormatted as Xh Ym, Xm Ys, or Xs
New ItemsnewItemsCountItems created in this run
Updated ItemsupdatedItemsCountItems updated in this run
Total ItemstotalItemsCountTotal items after this run
Tokensmetrics.total_tokens_usedFormatted as X.XK or X.XM

Status Color Coding

StatusColorCSS Classes
generatingBluebg-blue-100 text-blue-800 / dark variants
generatedGreenbg-emerald-100 text-emerald-800 / dark variants
errorRedbg-red-100 text-red-800 / dark variants
cancelledGraybg-gray-200 text-gray-800 / dark variants

Formatting Utilities

The component includes three formatting functions:

// Duration: seconds -> human-readable
formatDuration(3661); // "1h 1m"
formatDuration(125); // "2m 5s"
formatDuration(45); // "45s"

// Tokens: number -> abbreviated
formatTokens(1500000); // "1.5M"
formatTokens(12500); // "12.5K"
formatTokens(500); // "500"

// Cost: number -> USD
formatCost(0.0523); // "$0.0523"

When an entry has a triggerRunId, the table shows a link to the Trigger.dev cloud dashboard for that run.

PluginGrid -- Plugin Browsing

Source: apps/web/src/components/plugins/PluginGrid.tsx

The PluginGrid renders plugin cards in a responsive grid layout with optional category grouping.

Props

interface PluginGridProps {
plugins: UserPlugin[];
grouped: boolean; // Group by category or flat grid
searchQuery: string; // Current search (for empty state)
onClearSearch: () => void;
}

Layout Modes

ModegroupedDescription
Flat gridfalseAll plugins in a single responsive grid
GroupedtruePlugins organized under category headings

Both modes use the same responsive grid: grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4.

Category Grouping

When grouped is true, plugins are organized using groupByCategory():

function groupByCategory(plugins: UserPlugin[]): Record<string, UserPlugin[]> {
return plugins.reduce(
(acc, plugin) => {
(acc[plugin.category] ??= []).push(plugin);
return acc;
},
{} as Record<string, UserPlugin[]>
);
}

Each category section renders a heading using getCategoryLabel() followed by a grid of PluginCard components.

Empty State

The grid shows contextual empty states:

  • With search query: Search icon, "No results" message, and "Clear search" button.
  • Without search: Simple "No plugins" message.

MembersList -- Work Members

Source: apps/web/src/components/works/detail/members/MembersList.tsx

The MembersList displays work collaborators in a divided list format with role badges and permission-based management controls.

Layout

Owner Display

The owner is always displayed first with:

  • An avatar circle showing the first letter of their username
  • A "Creator" role badge
  • Their email address

Member Management

Each MemberRow component receives a canManage prop determined by canManageMembers(work.userRole). When true, the row shows management controls (role change, remove member).

Props

interface MembersListProps {
work: Work;
members: WorkMember[];
owner: WorkOwner;
onMemberRemoved: (memberId: string) => void;
onMemberUpdated: (member: WorkMember) => void;
}

Pattern Comparison

ConcernItemsListHistoryTablePluginGridMembersList
Data volumeHundreds to thousandsTensTensSmall (< 20)
VirtualizationYes (@tanstack/react-virtual)NoNoNo
Search/filterYes (text + category)NoVia parentNo
View modesGrid + List toggleTable onlyFlat + GroupedList only
Responsive1/2/3 columnsHorizontal scroll1/2/3 columnsSingle column
MemoizationReact.memo on cardsNoNoNo
SortingFeatured > order > alphaChronological (server-side)By categoryOwner first

When to Use Each Pattern

  • Virtualized grid/list (ItemsList pattern): Use for collections that may grow to hundreds or thousands of items. Requires @tanstack/react-virtual and a scroll container reference.
  • HTML table (HistoryTable pattern): Use for tabular data with a fixed set of columns and moderate row counts (under ~100 rows).
  • Card grid (PluginGrid pattern): Use for browsable collections where each item needs a rich card display with images, descriptions, and actions.
  • Divided list (MembersList pattern): Use for small collections where each item needs inline management controls and the total count is predictable.