Skip to content

Support filtering by duplicate request groups. Add duplicate request drawer UI.#7965

Merged
lucanovera merged 24 commits into
mainfrom
ENG-1806-FE-Add-DSR-duplication-detection-activity-log-link
May 15, 2026
Merged

Support filtering by duplicate request groups. Add duplicate request drawer UI.#7965
lucanovera merged 24 commits into
mainfrom
ENG-1806-FE-Add-DSR-duplication-detection-activity-log-link

Conversation

@lucanovera
Copy link
Copy Markdown
Contributor

@lucanovera lucanovera commented Apr 20, 2026

Ticket ENG-1806

Description Of Changes

Adds a "View related requests" drawer to the privacy request detail page so admins can see other requests submitted by the same data subject in one click — both from the duplicate-detection entry on the activity timeline and from a new search icon next to each identity in the right-hand details panel.

Why we abandoned the original "View duplicates" approach

The first iteration on this branch added a duplicate_request_group_id filter to the privacy-request search endpoint and used it to back the drawer. While reviewing it we found the group id is intentionally rule-version-bound by design:

  • DuplicateGroup.id = md5(rule_version + dedup_key) (duplicate_group.py:18-31)
  • rule_version is hashed from the duplicate-detection settings (enabled, time_window_days, match_identity_fields)
  • Detection also scopes by policy_id and a time_window_days cutoff

So filtering by duplicate_request_group_id would not return all requests by the same identity once settings, policy, or time-window changed. That's an "identity-group-under-this-rule-version-and-policy" id, not an identity id — which doesn't match what users want from this UI.

Switching to the existing PrivacyRequestFilter.identities filter (privacy_request_query_utils.py:117-132) gives us a pure identity match (OR semantics across populated fields), independent of duplicate-detection state, policy, or time window — which is the correct semantic for "related requests".

New UI

Captura de pantalla 2026-05-06 a la(s) 3 17 45 p  m Captura de pantalla 2026-05-06 a la(s) 3 18 00 p  m Captura de pantalla 2026-05-06 a la(s) 3 47 16 p  m

Code Changes

Backend — reverted prior-iteration additions (no backend changes ship in this PR):

  • Removed duplicate_request_group_id from PrivacyRequestFilter and its branch in filter_privacy_request_queryset.
  • Removed the index=True from PrivacyRequest.duplicate_request_group_id and the deferred entry in post_upgrade_index_creation.py.
  • Deleted alembic migration xx_2026_04_20_1200_e8f9a0b1c2d3_recreate_ix_privacyrequest_duplicate_request_group_id.py and its upgrade/downgrade test.
  • Deleted tests/ops/api/v1/endpoints/privacy_request/test_privacy_request_duplicate_group_filtering.py.
  • Reverted the TestDeferredDuplicateRequestGroupIdIndex block in tests/api/migrations/test_post_upgrade_index_creation.py.

Frontend — new related-requests drawer:

  • Added RelatedRequestsDrawer.tsx — calls useSearchPrivacyRequestsQuery({ identities }), lists results using the existing <ListItem> from the request manager, pins the current request to the top with a Current tag, and links open in new tabs.
  • Added relatedRequestsDrawerUtils.ts — pure helpers extractIdentityFields (builds the { filter, labels } payload from PrivacyRequestEntity.identity) and formatLabelList (Oxford-or list builder for the description copy). Unit tested in relatedRequestsDrawerUtils.test.ts — 15 cases covering 0/1/2/3+ identity fields including email, phone, external id, custom ids.

Frontend — entry points to the drawer:

  • Activity timeline link on the duplicate-detection log entry now reads · View related requests and opens the drawer when the request has any identity value. (ActivityTimeline.tsx, ActivityTimelineEntry.tsx)
  • Added a small search icon button next to each identity in the request details right rail with tooltip "View related requests for this subject". (RequestDetails.tsx)

Frontend — ListItem extensions (used by both dashboard and drawer):

  • New compact?: boolean prop. When true, the days-left/received-on cluster always stacks vertically regardless of viewport (the dashboard's 2xl:flex-row viewport-driven media query is misleading inside a 640px drawer).
  • New showActions?: boolean prop (default true). When false, the per-row RequestTableActions block is omitted.
  • New header?: { link?, extraTags? } nested-config prop, mirroring AntD patterns (Table.expandable, Table.pagination):
    • header.link.target / header.link.rel — when target is set, the title link bypasses the in-app router.push interceptor so the native anchor handles navigation. Drawer uses target="_blank".
    • header.extraTags — slot for an extra inline tag rendered alongside the status badge and rule tags. Drawer passes <Tag>Current</Tag> for the current row.
  • Inner Header now groups RequestStatusBadge, action-type tags, and extraTags in a single <Flex> so spacing is consistent.
  • The empty <div className="pr-4"> around checkbox now only renders when a checkbox is provided, so the drawer doesn't carry an unused 1rem left gap.
  • Existing 14 ListItem tests still pass; dashboard call site is behaviorally unchanged.

Frontend — type cleanup:

  • Removed duplicate_request_group_id from PrivacyRequestEntity (frontend types.ts) and from the auto-generated PrivacyRequestFilter.ts. Renamed hasDuplicateGrouphasRelatedRequests on ActivityTimelineItem.

Changelog: 7965-duplicate-request-drawer.yaml reworded to describe the related-requests drawer; db-migration label removed.

Steps to Confirm

  1. Pull the branch and run the admin-ui dev server (npm run dev in clients/admin-ui/) against a local backend.
  2. Submit two privacy requests with the same email under different policies and/or different submission times so they don't share a duplicate group.
  3. Open the detail page for one of them.
  4. Right rail entry point: click the search icon button next to the email. The drawer opens, the description reads Showing all requests that match this request's email, including any that were marked as duplicates, the current request is pinned to the top with a Current tag, and the other request is listed below.
  5. Timeline entry point: find the duplicate-detection log entry on the activity timeline and click · View related requests — same drawer opens.
  6. Click any other request in the list — it opens in a new tab.
  7. Resize the browser between narrow and 2XL widths and confirm the drawer rows always stack Days left / Received on vertically (no media-query flicker).
  8. Open a request with both email and phone populated — confirm the description reads … email or phone number, including any … and that requests sharing either value appear.
  9. Open a consent request (or one without identity fields) — the search icon does not appear and the timeline link is absent.
  10. Open the Privacy Request manager dashboard and confirm rows still navigate in-app on click and RequestTableActions still render at the row's right edge (regression check on ListItem).

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fides-plus-nightly Ready Ready Preview, Comment May 15, 2026 4:52pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored May 15, 2026 4:52pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 9%
6.93% (3173/45758) 6.32% (1676/26482) 4.8% (650/13537)
fides-js Coverage: 78%
79.17% (1977/2497) 66.25% (1249/1885) 73.31% (349/476)
privacy-center Coverage: 85%
82.53% (364/441) 79.74% (189/237) 74.07% (60/81)

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.60%. Comparing base (dc05748) to head (d12203f).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7965      +/-   ##
==========================================
- Coverage   85.61%   85.60%   -0.01%     
==========================================
  Files         658      658              
  Lines       42858    42859       +1     
  Branches     5016     5016              
==========================================
  Hits        36691    36691              
  Misses       5063     5063              
- Partials     1104     1105       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@lucanovera lucanovera added the db-migration This indicates that a change includes a database migration label Apr 20, 2026
@lucanovera
Copy link
Copy Markdown
Contributor Author

/code-review

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Code Review — PR #7965: Duplicate Request Group Filtering & UI Drawer

Overall this is a well-structured feature. The backend filter is minimal and correct, the migration includes a thoughtful large-table deferral path, and the new DuplicatesDrawer component is clean. A few things worth addressing before merge:

Summary of findings

Frontend

  • DuplicatesDrawer — silent truncation at 50 results (medium): The query is hardcapped at size: 50 with no pagination shown on the table. For large duplicate groups this silently truncates. Either raise the cap, add overflow messaging, or show a pagination control.
  • columns not memoized (minor): The columns array closes over currentRequestId but is recreated on every render. Wrapping in useMemo is consistent with the rows treatment above it.
  • Empty-state copy (minor): "No other duplicate requests in this group." is shown when the API returns zero items — but the table includes the current request, so an empty result means even it wasn't found. "No duplicate requests found." is more accurate.
  • Dual DUPLICATE_DETECTION_DATASET_NAME constant (medium): The same string is maintained in two places (Python + TypeScript). The comments document it, but there's no compile-time or test-time guard against the two drifting. Consider a lightweight contract test or alternative approach to surface the coupling.

Backend / Migration

  • Migration filename xx_ prefix (minor): Other migration files in the project don't use an xx_ prefix — this looks like a WIP marker that should be removed before merge.
  • Inconsistent DDL style in downgrade() (minor): The upgrade() block uses sa.text(...).bindparams(...) throughout, but downgrade() uses a bare f-string for the DROP INDEX. Stylistically inconsistent; consider wrapping in sa.text().

Tests

  • restore_head upgrades to REVISION not "head" (medium): If additional migrations land after e8f9a0b1c2d3, the teardown fixture leaves the database at an older revision rather than the true schema head, potentially breaking downstream tests in the same session. Changing to upgrade_db(alembic_config, "head") is safer.

🔬 Codegraph: connected (47570 nodes)


💡 Write /code-review in a comment to re-run this review.

@lucanovera lucanovera marked this pull request as ready for review April 28, 2026 16:10
@lucanovera lucanovera requested review from a team as code owners April 28, 2026 16:10
@lucanovera lucanovera requested review from JadeCara, gilluminate and vcruces and removed request for a team, gilluminate and vcruces April 28, 2026 16:10
Copy link
Copy Markdown
Contributor

@JadeCara JadeCara left a comment

Choose a reason for hiding this comment

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

BE elements look clean! Verified migration round trip and tested duplicates show up as expected in UI!

Very cool!

Image

@JadeCara JadeCara self-requested a review April 30, 2026 18:23
Copy link
Copy Markdown
Contributor

@JadeCara JadeCara left a comment

Choose a reason for hiding this comment

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

Found an issue - the de duplication group was originally just looking at identities - seeing if I can update to work for policies as well.

@lucanovera
Copy link
Copy Markdown
Contributor Author

lucanovera commented May 6, 2026

Found an issue - the de duplication group was originally just looking at identities - seeing if I can update to work for policies as well.

@JadeCara After our conversation last week I ended up simplifying everything and just showing related request by the same identity. There were just too many edge cases when trying to figure out all duplicates across time. Now it's just frontend changes that work with the existing identity filters.

@lucanovera lucanovera requested review from a team and kruulik and removed request for a team May 6, 2026 19:03
@lucanovera lucanovera requested review from a team and gilluminate and removed request for a team and kruulik May 14, 2026 20:55
Comment thread clients/admin-ui/src/features/privacy-requests/dashboard/list-item/ListItem.tsx Outdated
Comment on lines 53 to +68
<Typography.Link
href={`/privacy-requests/${privacyRequest.id}`}
variant="primary"
onClick={(e) => {
e.preventDefault();
router.push({
pathname: PRIVACY_REQUEST_DETAIL_ROUTE,
query: { id: privacyRequest.id },
});
}}
target={link?.target}
rel={link?.rel}
onClick={
useNativeNavigation
? undefined
: (e) => {
e.preventDefault();
router.push({
pathname: PRIVACY_REQUEST_DETAIL_ROUTE,
query: { id: privacyRequest.id },
});
}
}
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.

Some potential for clean up here. Should just use the new <RouterLink> component which eliminates the need for the href + onclick combo here. PRIVACY_REQUEST_DETAIL_ROUTE should be used in the href as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I tried this. There is a drawback to using RouterLink and it's that it doesn't fully support the query param route replacement. I don't really want to go back to using a .replace("[id] on each use. I think we can improve RouterLink and make it fully support this kind of dynamic path replacements.

Copy link
Copy Markdown
Contributor

@gilluminate gilluminate left a comment

Choose a reason for hiding this comment

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

FE tested in Vercel and looks good! Just a couple of nit-picks above

…item/ListItem.tsx

Co-authored-by: Jason Gill <jason.gill@ethyca.com>
Copy link
Copy Markdown
Contributor

@JadeCara JadeCara left a comment

Choose a reason for hiding this comment

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

🎉

@lucanovera lucanovera added this pull request to the merge queue May 15, 2026
Merged via the queue into main with commit 64eb144 May 15, 2026
76 of 78 checks passed
@lucanovera lucanovera deleted the ENG-1806-FE-Add-DSR-duplication-detection-activity-log-link branch May 15, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

db-migration This indicates that a change includes a database migration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants