Skip to content

feat(web-ui): filter, search, and sort for proof evidence history table - #528

Merged
frankbria merged 2 commits into
mainfrom
feature/485-proof-evidence-filters
Apr 3, 2026
Merged

feat(web-ui): filter, search, and sort for proof evidence history table#528
frankbria merged 2 commits into
mainfrom
feature/485-proof-evidence-filters

Conversation

@frankbria

@frankbria frankbria commented Apr 3, 2026

Copy link
Copy Markdown
Owner

Closes #485

Summary

  • Gate filter: dropdown populated dynamically from unique gates in the evidence data
  • Result filter: pass / fail / all
  • Search: matches run_id and artifact_path substrings (case-insensitive)
  • Sortable columns: Gate, Result, Run ID, Timestamp, Artifact — click header to sort, click again to reverse; aria-sort for accessibility
  • Default sort: Timestamp descending (most recent first)
  • Reset button: appears only when any filter is active; clears all filters

Test plan

  • 21 new unit tests in ProofDetailPage.test.tsx covering all filter/sort/reset combinations
  • 8 existing tests in __tests__/app/proof/req_id/page.test.tsx still pass
  • All 56 proof-related tests pass
  • No regressions in other test suites (pre-existing failures unchanged)

Summary by CodeRabbit

  • New Features

    • Evidence table supports filtering by gate and result, with search and sortable columns (visual sort indicators)
    • Active filters persist across sessions and a Reset button clears them
  • Tests

    • Added comprehensive tests covering filtering, sorting, search, reset behavior, and accessibility of sortable headers

…table (#485)

- Gate dropdown filters evidence by gate name (dynamic options from data)
- Result dropdown filters by pass/fail
- Search input matches run_id and artifact_path substrings
- All five columns sortable by clicking headers (aria-sort for a11y)
- Default sort: timestamp descending (most recent first)
- Reset button clears all filters (only visible when filters active)
- 21 new unit tests covering all filter/sort/reset combinations
@coderabbitai

coderabbitai Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Added client-side filter, search, and sort UX for the proof evidence history table with session-persisted filter state and a comprehensive Jest + React Testing Library suite validating UI and table behaviors.

Changes

Cohort / File(s) Summary
Proof Evidence UI
web-ui/src/app/proof/[req_id]/page.tsx
Added Gate/Result dropdowns, free-text search, sessionStorage-backed filter persistence, memoized gateOptions and filteredEvidence, sortable columns via a local SortButton component, composite row keys, and guarded array checks.
Tests
web-ui/src/__tests__/components/proof/ProofDetailPage.test.tsx
New Jest + RTL test suite mocking SWR, route params and APIs; verifies filter/result dropdowns, search behavior, reset button visibility, default timestamp sort, gate/result/search filtering, sort toggles, and combined AND filtering.
Types
web-ui/src/types/index.ts
Added exported types `ProofEvidenceSortCol = 'gate'

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Page as ProofDetailPage
  participant Session as sessionStorage
  participant SWR as SWR/DataFetcher
  participant API as Backend/API

  User->>Page: Open /proof/[req_id]
  Page->>Session: loadSessionFilters(sessionKey)
  Page->>SWR: fetchEvidence(req_id)
  SWR->>API: GET /evidence?req_id
  API-->>SWR: evidence[]
  SWR-->>Page: evidence[]
  Page->>Page: compute gateOptions, filteredEvidence (apply gate/result/search + sort)
  Page-->>User: render filter controls + sorted table

  User->>Page: change filter / search / sort
  Page->>Page: update filter state, recompute filteredEvidence
  Page->>Session: saveSessionFilters(sessionKey)
  Page-->>User: update table view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through gates and sorted time,

I searched for runs and chased a rhyme.
Filters saved in session's nest,
Table tidy, rows at rest—
A rabbit cheers the UI's climb! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding filter, search, and sort capabilities to the proof evidence history table.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from #485: gate/result filters, search by run_id/artifact_path, sortable columns, timestamp descending default, session persistence, reset control, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #485 requirements. The new types, test suite, and component enhancements are in-scope and necessary for the filter/search/sort feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/485-proof-evidence-filters

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Apr 3, 2026

Copy link
Copy Markdown

Review: PR #528 — Proof Evidence Filter/Sort/Search

Clean implementation. The feature is well-scoped, the useMemo usage is correct, and the 21 new unit tests cover all the meaningful combinations. A few items worth addressing:


Bug: Index key on sorted/filtered rows

page.tsx — the row key:

{filteredEvidence.map((ev, i) => (
  <tr key={i} ...>

Using the array index as a key was harmless when the order was stable, but now that rows can be reordered or filtered, React will reuse DOM nodes incorrectly — cells may retain stale state (e.g., focus) when the list changes. Use a stable identifier:

{filteredEvidence.map((ev) => (
  <tr key={`${ev.run_id}-${ev.gate}-${ev.timestamp}`} ...>

A composite of run_id + gate + timestamp should be unique in practice. If the API ever guarantees a unique id per evidence record, prefer that.


UX gap: empty filtered-results state

When evidence exists but all rows are filtered out, the table renders with an empty <tbody> and no feedback. The "No evidence recorded yet" message is gated on evidence.length === 0 (raw data), so it won't show in this case. A one-liner inside the table body would close this:

{filteredEvidence.length === 0 && (
  <tr>
    <td colSpan={5} className="px-4 py-6 text-center text-sm text-muted-foreground">
      No evidence matches the current filters.
    </td>
  </tr>
)}

Accessibility: search input missing aria-label

The placeholder is not a substitute for a label; screen readers may not announce the field purpose:

<input
  type="text"
  aria-label="Search run ID or artifact"
  placeholder="Search run ID or artifact…"
  ...
/>

Minor: result sort direction

Ascending sorts fail before pass (false=0 < true=1). That's defensible, but users on a PROOF page likely want passing results to surface first in ascending order. No change strictly required — just worth a conscious decision either way.


Overall: The logic is solid, useMemo dependencies are correct, ISO 8601 string comparison for timestamps works correctly, and gate options deduplicate and sort cleanly. The key-index issue is the only real correctness concern; the empty-state and aria-label items are UX/a11y polish worth a quick follow-up.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
web-ui/src/app/proof/[req_id]/page.tsx (1)

316-317: Use a stable row key instead of the filtered index.

This table is explicitly re-sorted and re-filtered, so key={i} changes meaning on every interaction. A key derived from immutable evidence fields will keep row identity stable as the order changes.

♻️ One possible fix
-                      {filteredEvidence.map((ev, i) => (
-                        <tr key={i} className="border-b last:border-0">
+                      {filteredEvidence.map((ev) => (
+                        <tr
+                          key={`${ev.req_id}:${ev.run_id}:${ev.timestamp}:${ev.gate}:${ev.artifact_path}`}
+                          className="border-b last:border-0"
+                        >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/app/proof/`[req_id]/page.tsx around lines 316 - 317, The row key
currently uses the mapped index (key={i}) inside the filteredEvidence.map call
which breaks identity when the list is re-sorted or filtered; replace it with a
stable, immutable identifier from each evidence object (e.g., ev.id or a
deterministic combination like `${ev.source}-${ev.offset}`) in the <tr>
generated in the filteredEvidence.map so row identity remains stable across
reorder/filter operations; if an immutable id doesn't exist on the evidence
objects, add one when the evidence is created/loaded (e.g., populate an `id`
field) and use that `id` as the key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-ui/src/app/proof/`[req_id]/page.tsx:
- Around line 49-56: The filter and search state (filterGate, filterResult,
search and their setters setFilterGate, setFilterResult, setSearch) are only
stored in component state and are lost when navigating away; persist them to
session-level storage by syncing with next/router searchParams or
window.sessionStorage: on mount initialize the three values from
searchParams/sessionStorage and whenever setFilterGate/setFilterResult/setSearch
are called update the searchParams and/or sessionStorage accordingly so state is
restored when returning to the page.
- Around line 13-14: Move the new TypeScript types SortCol and SortDir out of
page.tsx and into the centralized types module: add their definitions to
web-ui/src/types/index.ts, export them, then import and use them in
web-ui/src/app/proof/[req_id]/page.tsx (replace the inline type declarations
with imports of SortCol and SortDir). Ensure the exported names match exactly
and update any references in page.tsx to use the imported types.
- Around line 16-39: Replace raw HTML controls with Shadcn/UI components: update
the SortButton component to render the Shadcn Button (use the Button import)
instead of a plain <button>, swap the plain <select> elements used for Gate and
Result filtering to Shadcn Select, and replace the raw search <input> with the
Shadcn Input component; ensure you wire their props to the existing handlers
(retain SortButton props col, label, current, dir, onSort and call onSort(col)),
keep the Gate/Result select change handlers and values, and keep search
value/handler but add aria-label="Search run ID or artifact" to the search Input
so it is programmatically labeled. Use the Nova gray color styling conventions
from the Shadcn preset and Hugeicons for any icons (do not use lucide-react).
Ensure imports for Button, Select, Input (and Hugeicons) are added and
remove/replace any direct HTML controls in SortButton and the select/input
locations.

---

Nitpick comments:
In `@web-ui/src/app/proof/`[req_id]/page.tsx:
- Around line 316-317: The row key currently uses the mapped index (key={i})
inside the filteredEvidence.map call which breaks identity when the list is
re-sorted or filtered; replace it with a stable, immutable identifier from each
evidence object (e.g., ev.id or a deterministic combination like
`${ev.source}-${ev.offset}`) in the <tr> generated in the filteredEvidence.map
so row identity remains stable across reorder/filter operations; if an immutable
id doesn't exist on the evidence objects, add one when the evidence is
created/loaded (e.g., populate an `id` field) and use that `id` as the key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b15b085-f660-41ae-b1b3-aa4a19c86825

📥 Commits

Reviewing files that changed from the base of the PR and between b60a53d and bab0867.

📒 Files selected for processing (2)
  • web-ui/src/__tests__/components/proof/ProofDetailPage.test.tsx
  • web-ui/src/app/proof/[req_id]/page.tsx

Comment thread web-ui/src/app/proof/[req_id]/page.tsx Outdated
Comment on lines +16 to +39
function SortButton({
col,
label,
current,
dir,
onSort,
}: {
col: SortCol;
label: string;
current: SortCol;
dir: SortDir;
onSort: (col: SortCol) => void;
}) {
return (
<button
aria-label={`Sort by ${label}`}
onClick={() => onSort(col)}
className="flex items-center gap-1 font-medium hover:text-foreground"
>
{label}
{current === col && <span className="text-xs">{dir === 'asc' ? '↑' : '↓'}</span>}
</button>
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full file to understand context and verify the code locations
wc -l web-ui/src/app/proof/\[req_id\]/page.tsx

Repository: frankbria/codeframe

Length of output: 105


🏁 Script executed:

# Read the file to see the actual code at lines 16-39 and 223-259
cat -n web-ui/src/app/proof/\[req_id\]/page.tsx | sed -n '1,50p'

Repository: frankbria/codeframe

Length of output: 1844


🏁 Script executed:

# Check lines around 223-259
cat -n web-ui/src/app/proof/\[req_id\]/page.tsx | sed -n '200,270p'

Repository: frankbria/codeframe

Length of output: 3620


🏁 Script executed:

# Check if Shadcn/UI components are available in the project
ls -la web-ui/src/components/ui/ | head -20

Repository: frankbria/codeframe

Length of output: 981


🏁 Script executed:

# Verify the dependencies in package.json to confirm Shadcn/UI setup
cat web-ui/package.json | grep -A 20 -B 5 "shadcn"

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Check package.json more carefully
cat web-ui/package.json | grep -E "(shadcn|hugeicons|lucide)" | head -20

Repository: frankbria/codeframe

Length of output: 96


🏁 Script executed:

# Let's also check if there are any imports of lucide-react in the file
rg "lucide-react" web-ui/src/app/proof/\[req_id\]/page.tsx

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Check if the file imports hugeicons
rg "hugeicons" web-ui/src/app/proof/\[req_id\]/page.tsx

Repository: frankbria/codeframe

Length of output: 45


🏁 Script executed:

# Look at the Shadcn Input and Select components to see how they should be used
cat web-ui/src/components/ui/input.tsx

Repository: frankbria/codeframe

Length of output: 841


🏁 Script executed:

# Check the Select component implementation
head -50 web-ui/src/components/ui/select.tsx

Repository: frankbria/codeframe

Length of output: 1817


🏁 Script executed:

# Check if there's a Label component available
ls -la web-ui/src/components/ui/label.tsx

Repository: frankbria/codeframe

Length of output: 144


🏁 Script executed:

# Check if there's a label component anywhere in the project
rg -i "label" web-ui/src/components/ui/ --type tsx

Repository: frankbria/codeframe

Length of output: 90


🏁 Script executed:

# Let's see how the Button component is used elsewhere in the file
rg "Button" web-ui/src/app/proof/\[req_id\]/page.tsx -A 3

Repository: frankbria/codeframe

Length of output: 1934


🏁 Script executed:

# Let's check the full context around the search input to see if it's wrapped in a label
sed -n '250,265p' web-ui/src/app/proof/\[req_id\]/page.tsx

Repository: frankbria/codeframe

Length of output: 644


🏁 Script executed:

# Check if there are other uses of Input component in the project to see best practices
rg "Input" web-ui/src --type tsx | head -10

Repository: frankbria/codeframe

Length of output: 90


🏁 Script executed:

# Let's check another page that might use Shadcn Input and Select properly
find web-ui/src/app -name "*.tsx" | head -10

Repository: frankbria/codeframe

Length of output: 403


🏁 Script executed:

# Check if there are other examples of Shadcn Select usage in the codebase
rg "from.*@/components/ui/select" web-ui/src --type ts -A 5 | head -40

Repository: frankbria/codeframe

Length of output: 1018


🏁 Script executed:

# Check if Input is used elsewhere
rg "from.*@/components/ui/input" web-ui/src --type ts -A 10 | head -50

Repository: frankbria/codeframe

Length of output: 3912


Replace raw HTML controls with Shadcn/UI components and add proper labeling for the search field.

The sort button (lines 16–39), filter selects (lines 223–251), and search input (lines 253–259) all use raw HTML elements instead of Shadcn/UI. Replace SortButton to use the Shadcn Button component, the <select> elements with Shadcn Select, and the <input> with Shadcn Input to maintain consistent styling and design system compliance.

Additionally, the search input currently has no programmatic label—add aria-label="Search run ID or artifact" to it so screen readers can identify it properly. The Gate and Result selects already have aria-labels, but the search field needs one.

Per coding guidelines: "Web UI must use Shadcn/UI (Nova preset) with gray color scheme and Hugeicons (@hugeicons/react); never use lucide-react."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/app/proof/`[req_id]/page.tsx around lines 16 - 39, Replace raw
HTML controls with Shadcn/UI components: update the SortButton component to
render the Shadcn Button (use the Button import) instead of a plain <button>,
swap the plain <select> elements used for Gate and Result filtering to Shadcn
Select, and replace the raw search <input> with the Shadcn Input component;
ensure you wire their props to the existing handlers (retain SortButton props
col, label, current, dir, onSort and call onSort(col)), keep the Gate/Result
select change handlers and values, and keep search value/handler but add
aria-label="Search run ID or artifact" to the search Input so it is
programmatically labeled. Use the Nova gray color styling conventions from the
Shadcn preset and Hugeicons for any icons (do not use lucide-react). Ensure
imports for Button, Select, Input (and Hugeicons) are added and remove/replace
any direct HTML controls in SortButton and the select/input locations.

Comment thread web-ui/src/app/proof/[req_id]/page.tsx Outdated
…lters

- Use stable composite row key (req_id:run_id:timestamp:gate:artifact) instead of index
- Move ProofEvidenceSortCol and SortDir types to types/index.ts
- Replace SortButton plain <button> with Shadcn Button (ghost variant)
- Replace search <input> with Shadcn Input component
- Persist filter state to per-requirement sessionStorage key (survives navigation)
- Clear sessionStorage between tests to prevent filter state leakage

Note: kept native <select> for Gate/Result filters to maintain consistency
with the established codebase pattern (ProofPage #483 also uses native selects)
and compatibility with the fireEvent.change test interaction model.
@claude

claude Bot commented Apr 3, 2026

Copy link
Copy Markdown

Follow-up Review

Both correctness items from my prior review have been addressed in the latest commit:

  • Stable row key: composite key (req_id:run_id:timestamp:gate:artifact_path) is in place; no index-based re-render issue.
  • aria-label on search input: present on the Input component.

One open item remains:

Still open: empty filtered-results state

When evidence exists but all rows are filtered out, filteredEvidence is empty while evidence.length > 0, so neither the loading/error messages nor the 'No evidence recorded yet' copy fires. The table renders with a silent empty tbody. A single row inside the body closes this:

{filteredEvidence.length === 0 && (
  <tr>
    <td colSpan={5} className='px-4 py-6 text-center text-sm text-muted-foreground'>
      No evidence matches the current filters.
    </td>
  </tr>
)}

This is the only remaining item. Everything else (filter logic, sort logic, session persistence, test coverage) looks good. Happy to approve once the empty-state case is handled.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-ui/src/app/proof/`[req_id]/page.tsx:
- Around line 80-87: The effect hooked by useEffect reads reqId (via
loadSessionFilters(reqId)) but has an empty dependency array, so it should
include reqId to re-run when the route param changes; update the dependency
array for the useEffect that calls setWorkspacePath(getSelectedWorkspacePath()),
setWorkspaceReady(true), loadSessionFilters(reqId), setFilterGate(...),
setFilterResult(...), and setSearch(...) to include reqId (and any other stable
values like getSelectedWorkspacePath if it's not stable) so the filters are
reloaded when reqId changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 890052e7-da90-41b3-a28b-946c6af7fb3f

📥 Commits

Reviewing files that changed from the base of the PR and between bab0867 and c8cae44.

📒 Files selected for processing (3)
  • web-ui/src/__tests__/components/proof/ProofDetailPage.test.tsx
  • web-ui/src/app/proof/[req_id]/page.tsx
  • web-ui/src/types/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web-ui/src/tests/components/proof/ProofDetailPage.test.tsx

Comment on lines 80 to 87
useEffect(() => {
setWorkspacePath(getSelectedWorkspacePath());
setWorkspaceReady(true);
const saved = loadSessionFilters(reqId);
setFilterGate(saved.gate);
setFilterResult(saved.result);
setSearch(saved.search);
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing reqId in the dependency array.

The useEffect references reqId (via loadSessionFilters(reqId)) but the dependency array is empty. If the user navigates between different requirements via client-side routing without a full remount, the effect won't re-run and filters from the previous requirement will persist incorrectly.

🐛 Proposed fix
   useEffect(() => {
     setWorkspacePath(getSelectedWorkspacePath());
     setWorkspaceReady(true);
     const saved = loadSessionFilters(reqId);
     setFilterGate(saved.gate);
     setFilterResult(saved.result);
     setSearch(saved.search);
-  }, []);
+  }, [reqId]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
setWorkspacePath(getSelectedWorkspacePath());
setWorkspaceReady(true);
const saved = loadSessionFilters(reqId);
setFilterGate(saved.gate);
setFilterResult(saved.result);
setSearch(saved.search);
}, []);
useEffect(() => {
setWorkspacePath(getSelectedWorkspacePath());
setWorkspaceReady(true);
const saved = loadSessionFilters(reqId);
setFilterGate(saved.gate);
setFilterResult(saved.result);
setSearch(saved.search);
}, [reqId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/app/proof/`[req_id]/page.tsx around lines 80 - 87, The effect
hooked by useEffect reads reqId (via loadSessionFilters(reqId)) but has an empty
dependency array, so it should include reqId to re-run when the route param
changes; update the dependency array for the useEffect that calls
setWorkspacePath(getSelectedWorkspacePath()), setWorkspaceReady(true),
loadSessionFilters(reqId), setFilterGate(...), setFilterResult(...), and
setSearch(...) to include reqId (and any other stable values like
getSelectedWorkspacePath if it's not stable) so the filters are reloaded when
reqId changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UX: Add filter and search controls to proof evidence history table

1 participant