You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
paginate the merge candidate listing endpoint, reusing the same tools are resource list, and similarly adapt frontend to make use of it.
Note this adds some basic query params (sorting, filtering), and adjusts the frontend to make use of those (for not showing the approved/denied merges)
Refactor note: This adds a calculated name field to the candidate response model, and lets the frontend read from that, rather than fetching values for name in a separate query (fan-out).
Why
GET /oil-gas-fields/merge-candidates returned every candidate of every status as one bare array, and the Merge Review page rendered all of them. Each queue row also fetched the full detail of every resource in its candidate just to show a name. After one link-all pass on production data (~750 candidates), opening the page meant one large response plus ~1,500 detail requests.
Closes STIT-786. Partly covers [[STIT-795]] (date sorts; alphabetical sort is a follow-up) and STIT-745 (paging and status filter).
What changed
API: GET /oil-gas-fields/merge-candidates (breaking response shape)
The endpoint now returns a page (items, total_count, page, page_size, total_pages) instead of an array. It takes:
page / page_size, using the shared PaginationParams.
status, which can be repeated. With no status, every status is returned.
sort_by (created | reviewed_at) and sort_order, always with an id tiebreak so candidates sharing a timestamp (bulk creation) stay on one page. Unreviewed rows sort last.
The response also includes:
status_counts: per-status totals across the whole queue, regardless of the filter.
A server-computed name per candidate on list and detail responses. It's the merged resource's name once approved, otherwise the best-ranked source's name across the candidate's resources (the rule the frontend used to apply), and it respects the viewer's licensed sources. Create/approve/deny responses don't carry it.
Callers
stitch-client:list_merge_candidates() is replaced by list_merge_candidates_page() and iter_merge_candidates().
entity-linkage: still collects every candidate of every status, page by page, so de-duplication keeps seeing reviewed candidates.
Merge Review page
Status filter: a multi-select dropdown (default: Pending), matching the Resource List's filters. It replaces "Show approved". Clearing every status shows all of them.
Sort: a plain-language select (newest/oldest first, recently/earliest reviewed).
Paging: the shared Pagination control under the sticky queue, 25 per page by default.
URL state: page, page size, status and sort live in the URL, so links and Back/Forward restore the view. A bare /merge-candidate-review is the pending queue. The params are documented in the frontend README under "Merge review URLs".
Names: queue rows and the panel heading use the API's name, so the queue makes no per-resource requests.
Selection: the first pending candidate on the page is selected when the current one isn't on it. A page past the end (e.g. after reviewing the last candidate on it) steps back to the last page.
Cleanup: removes the browser-side naming code this left unused (useMergeCandidateName, useMergeSourceDetails, utils/mergeCandidateName, pickCompareName).
Behavior changes to be aware of
Denied candidates are hidden by default. The queue used to hide only approved candidates; it now shows only pending ones until you choose other statuses.
The panel heading's name now comes from the server. It used to come from the detail's comparison data. The two can differ when a resource has a per-field source-priority override; the heading now always matches the queue row.
How to review
deployments/api/src/stitch/api/entities.py: the new contract.
deployments/api/src/stitch/api/db/merge_candidate_actions.py: list_merge_candidates (query, ordering, counts) and _candidate_name.
packages/stitch-client and deployments/entity-linkage/.../client.py: the callers.
deployments/stitch-frontend/src/config/mergeReviewParams.js, then pages/MergeCandidateReviewPage.jsx.
Commits are split API-first, then frontend, so each can be reviewed separately.
Known gaps / follow-ups
Pagination in the narrow sidebar:Pagination keeps 7 page slots plus 2 arrows (~356px) even when few pages exist, wider than the queue column (~232–296px), so it likely wraps to two lines. I haven't verified it visually yet; a compact option may follow.
Alphabetical sort (STIT-795): needs a follow-up ticket. It requires ranking coalesced names in SQL across all candidates, per viewer's licensed sources.
API reference:API_REFERENCE.md was hand-patched for this endpoint only. The committed file was already out of date (missing NORSource), so regenerating it would have pulled unrelated changes into this PR.
No new index: none on merge_candidates(status, created, id); at ~750 rows it isn't needed. Revisit if the merge_candidates.list* query timings say otherwise.
Pre-existing dead code:compareEntry and valueEntryForResource in utils/candidateCompare.js are only used by their own tests. Left in place.
Testing
make check passes.
API unit: paging, status_counts with every status present, name selection (source rank, tie on position, merged resource, no licensed name).
API integration (SQLite):
stable paging with identical created timestamps
repeated status params versus unfiltered counts
names before and after approval
reviewed_at sort with unreviewed last
422s for bad status / sort_by / sort_order / page / page_size
Client / entity-linkage: request params, page-size validation, the iterator following total_pages, the wrapper gathering every page with no status filter.
Frontend:
URL schema parse/serialize/round-trip
status filter, sort, paging, URL-driven views
clamping to the last page
selection on page change
header counts
no per-resource requests from the queue
Manual verification:
AI assistance
Claude Code was used to explore the codebase, draft the plan, and write the implementation, tests and docs. Design decisions were made by the author during planning: paging style, status filter semantics, server-side names, sort scope.
GET /oil-gas-fields/merge-candidates returned every candidate of every
status as a bare array, so the Merge Review page loaded the whole queue
(~750 candidates after one link-all pass) and every row fetched each of
its resources' details just to show a name.
The endpoint now returns a page:
- page / page_size, using the shared PaginationParams
- a repeatable `status` filter; omitted, every status is returned
- sort_by (created | reviewed_at) and sort_order, always with an id
tiebreak so rows stay on one page when candidates share a timestamp
- status_counts over the whole queue, ignoring the filter, so the
header keeps reporting the full workload
- a server-computed `name` per candidate: the merged resource's name
once approved, otherwise the best-ranked source's name across its
resources (the rule the frontend applied). Only list and detail
responses carry it.
Callers updated for the new shape:
- stitch-client: list_merge_candidates() is replaced by
list_merge_candidates_page() and iter_merge_candidates(); the
now-unused _expect_list helper is removed
- entity-linkage: still gathers every candidate of every status, page
by page, so de-duplication keeps seeing reviewed candidates
- frontend: reads the paged response, applies the "Show approved"
filter on the server, and takes header counts from status_counts.
It fetches a single page of 200 until the queue gets paging controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The queue fetched a single 200-candidate page with a "Show approved"
checkbox, and every row fetched each of its resources' details to show
a name. It now uses the paged list endpoint end to end.
- Status filter: a multi-select dropdown (Pending / Approved / Denied)
replaces the checkbox. It defaults to Pending and shows each status's
queue-wide count. Clearing every status shows all of them.
- Sort: one plain-language select (newest/oldest first, recently/earliest
reviewed), mapped to the API's sort_by + sort_order.
- Paging: the shared Pagination control sits under the sticky queue,
25 per page by default; changing page scrolls the list to the top.
- URL state: page, page_size, status and sort are serialized in the
query string (config/mergeReviewParams.js, useMergeReviewState), so
links and Back/Forward restore the view. A bare
/merge-candidate-review is the pending queue; selecting no statuses is
written as status=all. Documented in the README.
- Names: queue rows and the panel heading use the API's `name`, so the
queue makes no per-resource requests and the two always agree.
- Selection: the first pending candidate on the page is selected
whenever the current one is not on it, but not while the next page is
still loading. A page past the end steps back to the last page.
Removes the browser-side naming code this leaves unused:
useMergeCandidateName, useMergeSourceDetails, utils/mergeCandidateName
and pickCompareName.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared Pagination control always lays out seven page slots plus two
arrows (~356px), wider than the queue column (~230–295px), so its page
buttons wrapped onto a second row. Add a `compact` option that shows
"Page X of Y" between the arrows, and use it for the queue. The
Resource List keeps its page-number buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No other dropdown in the app shows counts, so the Status filter
introduced a new UI pattern. The header's Pending / Reviewed / Total
counts still come from status_counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty result reports total_pages 0, so a page past the end was only
corrected when there were results: a link to ?page=4 with no matches
stayed on page 4. Treat page 1 as the last page of an empty result.
An empty page also left the previous selection in the decision panel,
offering review actions for a candidate the queue was not showing.
Clear the selection when a loaded page is empty.
Addresses Copilot review feedback on #310.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This test leaves the newly created async HTTP client open, unlike the neighboring tests, so its transport resources can leak across the test suite. Close it after the assertions.
The Merge Review sort select and the pager's per-page select were native
controls (browser chevron, regular weight, and a shorter per-page box)
next to the Status FilterDropdown button. Add a Select component that
keeps the native <select> but matches the FilterDropdown button: same
height, weight, border, hover and ▼ glyph. The shared Pagination uses
it, so the Resource List's per-page select changes too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ▼ glyph's 32px gutter made the sort select 169px, 1px more than the
room beside the Status filter in the 20rem queue column, so it wrapped.
Use 24px with the glyph 10px from the edge: 161px, and the same 6px
text-to-glyph gap as the FilterDropdown button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hey @AlexAxthelm, I've taken the liberty of putting this PR in draft mode.
It's no longer necessary to get this in ahead of the Trace demo, and I want to consider this one and #313 a bit more carefully before merging.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
paginate the merge candidate listing endpoint, reusing the same tools are resource list, and similarly adapt frontend to make use of it.
Note this adds some basic query params (sorting, filtering), and adjusts the frontend to make use of those (for not showing the approved/denied merges)
Refactor note: This adds a calculated
namefield to the candidate response model, and lets the frontend read from that, rather than fetching values for name in a separate query (fan-out).Why
GET /oil-gas-fields/merge-candidatesreturned every candidate of every status as one bare array, and the Merge Review page rendered all of them. Each queue row also fetched the full detail of every resource in its candidate just to show a name. After one link-all pass on production data (~750 candidates), opening the page meant one large response plus ~1,500 detail requests.Closes STIT-786. Partly covers [[STIT-795]] (date sorts; alphabetical sort is a follow-up) and STIT-745 (paging and status filter).
What changed
API:
GET /oil-gas-fields/merge-candidates(breaking response shape)The endpoint now returns a page (
items,total_count,page,page_size,total_pages) instead of an array. It takes:page/page_size, using the sharedPaginationParams.status, which can be repeated. With nostatus, every status is returned.sort_by(created|reviewed_at) andsort_order, always with anidtiebreak so candidates sharing a timestamp (bulk creation) stay on one page. Unreviewed rows sort last.The response also includes:
status_counts: per-status totals across the whole queue, regardless of the filter.nameper candidate on list and detail responses. It's the merged resource's name once approved, otherwise the best-ranked source's name across the candidate's resources (the rule the frontend used to apply), and it respects the viewer's licensed sources. Create/approve/deny responses don't carry it.Callers
list_merge_candidates()is replaced bylist_merge_candidates_page()anditer_merge_candidates().Merge Review page
Paginationcontrol under the sticky queue, 25 per page by default./merge-candidate-reviewis the pending queue. The params are documented in the frontend README under "Merge review URLs".name, so the queue makes no per-resource requests.useMergeCandidateName,useMergeSourceDetails,utils/mergeCandidateName,pickCompareName).Behavior changes to be aware of
How to review
deployments/api/src/stitch/api/entities.py: the new contract.deployments/api/src/stitch/api/db/merge_candidate_actions.py:list_merge_candidates(query, ordering, counts) and_candidate_name.packages/stitch-clientanddeployments/entity-linkage/.../client.py: the callers.deployments/stitch-frontend/src/config/mergeReviewParams.js, thenpages/MergeCandidateReviewPage.jsx.Commits are split API-first, then frontend, so each can be reviewed separately.
Known gaps / follow-ups
Paginationkeeps 7 page slots plus 2 arrows (~356px) even when few pages exist, wider than the queue column (~232–296px), so it likely wraps to two lines. I haven't verified it visually yet; a compact option may follow.API_REFERENCE.mdwas hand-patched for this endpoint only. The committed file was already out of date (missingNORSource), so regenerating it would have pulled unrelated changes into this PR.merge_candidates(status, created, id); at ~750 rows it isn't needed. Revisit if themerge_candidates.list*query timings say otherwise.compareEntryandvalueEntryForResourceinutils/candidateCompare.jsare only used by their own tests. Left in place.Testing
make checkpasses.status_countswith every status present, name selection (source rank, tie on position, merged resource, no licensed name).createdtimestampsstatusparams versus unfiltered countsreviewed_atsort with unreviewed laststatus/sort_by/sort_order/page/page_sizetotal_pages, the wrapper gathering every page with no status filter.Manual verification:
AI assistance
Claude Code was used to explore the codebase, draft the plan, and write the implementation, tests and docs. Design decisions were made by the author during planning: paging style, status filter semantics, server-side names, sort scope.
🤖 Generated with Claude Code