web/src/components/table/peek/README.md
This document describes how table state (filters, sorting, pagination, search) is managed and persisted in peek views when navigating between items using K/J keyboard shortcuts.
Peek views allow users to quickly preview table items in a side panel. When navigating between items using K/J shortcuts, tables inside the peek view remount, which would normally reset their state. The peek state management system prevents this by storing table state in a context that persists across navigation.
The peek state architecture separates the persisting context provider from the remounting content:
┌─────────────────────────────────────────────────────────────┐
│ TablePeekView (peek.tsx) │
│ │
│ <PeekTableStateProvider> ← Persists across itemId changes│
│ <div key={itemId}> ← Only this remounts │
│ {children} ← Tables remount here │
│ </div> │
│ </PeekTableStateProvider> │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────┐
│ Table Components (e.g., ScoresTable) │
│ │
│ Hooks automatically detect peek: │
│ • useOrderByState() │
│ • usePaginationState() │
│ • useFullTextSearch() │
│ │
│ Hooks requiring explicit wiring: │
│ • useSidebarFilterState() │
└───────────────────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Hook reads from peek context: │
│ │
│ const peekContext = usePeekTableState()│
│ if (peekContext) { │
│ useSidebarFilterState({ │
│ stateLocation: "peekContext", │
│ context: peekContext, │
│ }) │
│ return peekContext.tableState.X │
│ } │
│ return urlState │
└────────────────────────────────────────┘
Location: contexts/PeekTableStateContext.tsx
itemId changes during K/J navigationMost state management hooks automatically detect when they're running inside a
peek view and read/write state accordingly. useSidebarFilterState is the
exception and must be wired explicitly by the caller.
useSidebarFilterState - Manages filter state
web/src/features/filters/hooks/useSidebarFilterState.tsxhookOptions wiring:
stateLocation: "peekContext" with context: usePeekTableState()useOrderByState - Manages sorting state
web/src/features/orderBy/hooks/useOrderByState.tsusePaginationState - Manages pagination state
web/src/hooks/usePaginationState.tspage/limit and pageIndex/pageSize formatsuseFullTextSearch - Manages search query state
web/src/components/table/use-cases/useFullTextSearch.tsxWhen using K/J navigation in peek view for fully integrated tables:
PeekTableStateContextitemId changes → peek content remountsPeekTableStateProvider does NOT remountFully Integrated Tables:
These tables don't currently have peek views but now use peek-aware hooks, making them ready if peek views are added:
The PeekTableState interface defines what state is persisted:
interface PeekTableState {
filters: FilterState;
sorting: OrderByState;
pagination: { pageIndex: number; pageSize: number };
search: { query: string | null; type: string[] };
}
To make a table work with peek state persistence:
Use peek-aware hooks instead of direct URL state management:
// ❌ Don't use useQueryParams directly
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
// ✅ Use usePaginationState (automatically peek-aware)
const [paginationState, setPaginationState] = usePaginationState(0, 50);
For search, use useFullTextSearch:
// ❌ Don't use useQueryParam directly
const [searchQuery, setSearchQuery] = useQueryParam("search", StringParam);
// ✅ Use useFullTextSearch (automatically peek-aware)
const { searchQuery, setSearchQuery } = useFullTextSearch();
For filters, use useSidebarFilterState:
const peekContext = usePeekTableState();
const queryFilterOptions: UseSidebarFilterStateOptions = useMemo(() => {
if (peekContext) {
return {
loading: isSidebarFilterLoading,
implicitDefaultConfig: DEFAULT_SIDEBAR_IMPLICIT_ENVIRONMENT_CONFIG,
stateLocation: "peekContext",
context: peekContext,
};
}
return {
loading: isSidebarFilterLoading,
implicitDefaultConfig: DEFAULT_SIDEBAR_IMPLICIT_ENVIRONMENT_CONFIG,
stateLocation: "urlAndSessionStorage",
sessionFilterContextId: projectId,
};
}, [isSidebarFilterLoading, peekContext, projectId]);
const queryFilter = useSidebarFilterState(
filterConfig,
filterOptions,
queryFilterOptions,
);
useSidebarFilterState no longer detects peek context internally. If the
table can render inside PeekTableStateProvider, the caller must pass
stateLocation: "peekContext" explicitly or filters will persist in URL or
session state instead of the in-memory peek state.
For sorting, use useOrderByState:
// Already peek-aware, no changes needed if already using this hook
const [orderByState, setOrderByState] = useOrderByState({
column: "createdAt",
order: "DESC",
});
Most peek-aware hooks follow this pattern internally:
export const useSomeState = () => {
const peekContext = usePeekTableState();
// URL-based state (fallback)
const [urlState, setUrlState] = useQueryParam(...);
if (peekContext) {
// In peek view: read/write from context
const value = peekContext.tableState.someProperty;
const setValue = (newValue) => {
peekContext.setTableState({
...peekContext.tableState,
someProperty: newValue,
});
};
return { value, setValue };
}
// Not in peek view: use URL state
return { value: urlState, setValue: setUrlState };
};
Table state persists during K/J keyboard navigation within the same peek view:
1. Open trace T1 → apply filter to ScoresTable
2. Press K/J → navigate to trace T2
3. ScoresTable in T2 retains the filter ✓
Reason: PeekTableStateProvider remains mounted during K/J navigation. Only the content with key={itemId} remounts, preserving user's filter/sort/pagination preferences across items of the same type.
Table state resets when the peek view closes:
1. Open trace T1 → apply filter
2. Close peek (X button/Escape/click outside)
3. Open observation O1 → fresh state ✓
Reason: Closing the peek removes the peek URL parameter. This causes the <Sheet> component to close and unmount <SheetContent>, which unmounts PeekTableStateProvider and destroys all state.
Risk Level: LOW (theoretical edge case)
Issue: All tables in the same peek view share a single PeekTableState object. If a peek view contains multiple independent paginated tables, they share pagination/filter/sort state.
Example:
Hypothetical: Trace peek with both Scores table AND Events table
→ Navigate to page 2 of Scores table
→ context.pagination = { pageIndex: 1, pageSize: 50 }
→ Events table also shows page 2 ❌
Current Reality:
disableUrlPersistence and scope data via props (traceId, observationId)Future Solution (if multiple independent table types are needed):
Any follow-up design for namespaced peek state still needs to keep the explicit
useSidebarFilterState wiring pattern:
const queryFilterOptions: UseSidebarFilterStateOptions = peekContext
? {
loading,
stateLocation: "peekContext",
context: peekContext,
}
: {
loading,
stateLocation: "urlAndSessionStorage",
sessionFilterContextId: projectId,
};
const filters = useSidebarFilterState(config, options, queryFilterOptions);
contexts/PeekTableStateContext.tsxweb/src/hooks/usePaginationState.tsuse-cases/useFullTextSearch.tsxweb/src/features/filters/hooks/useSidebarFilterState.tsxweb/src/features/orderBy/hooks/useOrderByState.ts