Skip to content

ENG-3461: migrate admin-ui to RouterLink and remove legacyBehavior#7942

Merged
gilluminate merged 7 commits intomainfrom
gill/ENG-3461/link-legacy-behavior
Apr 16, 2026
Merged

ENG-3461: migrate admin-ui to RouterLink and remove legacyBehavior#7942
gilluminate merged 7 commits intomainfrom
gill/ENG-3461/link-legacy-behavior

Conversation

@gilluminate
Copy link
Copy Markdown
Contributor

@gilluminate gilluminate commented Apr 15, 2026

Ticket ENG-3461

Description Of Changes

Next.js 16 dropped passHref and legacyBehavior from the public <Link> API, and Next.js 15 emits a runtime deprecation warning for legacyBehavior. The Admin UI still relied on that pattern in 20 places to wrap a Typography.Link or Button inside a next/link.

This PR introduces a shared RouterLink component (clients/admin-ui/src/features/common/nav/RouterLink.tsx) that replaces every <Link passHref legacyBehavior> call site in admin-ui. It detects whether its only child is an antd Button and either:

  • wraps the button in next/link so Next renders its own <a> around the button (preserves prefetch + modifier-click handling), or
  • renders a Typography.Link-styled anchor that intercepts plain left-clicks and calls router.push for SPA navigation, while letting modifier / middle / right clicks fall through to the browser for new-tab / copy-link behaviour.

Also forwards target and rel to both paths so callers can still open a link in a new tab (bypassing SPA interception when target="_blank").

The result is a single anchor per link (no more nested <a> hack), no deprecation warnings, and consistent SPA navigation across the app. Unit tests cover both branches of the component.

After this PR, git grep 'passHref\|legacyBehavior' in clients/admin-ui returns zero hits. Privacy Center, fides-js, and fidesui already had zero deprecated Link usages and did not need changes.

Code Changes

  • Added clients/admin-ui/src/features/common/nav/RouterLink.tsx + jest coverage in RouterLink.test.tsx (10 tests: Button-wrap path, text path, URL-object serialisation, modifier-click pass-through, target="_blank" pass-through, target/rel forwarding, custom onClick + preventDefault, fragment fall-through).
  • Migrated 20 call sites off <Link passHref legacyBehavior>:
    • features/access-policies/PolicyCard.tsx
    • features/common/table/cells/LinkCell.tsx
    • features/consent-settings/tcf/PublisherRestrictionsTable.tsx
    • features/data-discovery-and-detection/action-center/ConfidenceCard.tsx
    • features/data-discovery-and-detection/action-center/EmptyMonitorsResult.tsx
    • features/data-discovery-and-detection/action-center/MonitorResult.tsx
    • features/data-discovery-and-detection/action-center/fields/page.tsx
    • features/datamap/datamap-drawer/SystemInfo.tsx
    • features/integrations/IntegrationLinkedSystems.tsx
    • features/privacy-assessments/AssessmentSettingsModal.tsx (uses target="_blank")
    • features/privacy-requests/dashboard/DuplicateRequestsButton.tsx
    • features/user-management/RolesForm.tsx
    • features/user-management/UserManagementTable.tsx
    • home/SystemCoverageCard.tsx (text-mode link)
    • pages/access-policies/index.tsx
    • pages/add-systems/multiple.tsx
    • pages/dataset/index.tsx
    • pages/privacy-assessments/[id].tsx
    • pages/settings/rbac/index.tsx
    • pages/settings/rbac/roles/new.tsx

Steps to Confirm

All of the below were verified manually against a local npm run dev admin-ui. After each, confirm the URL changes to the expected destination and no validateDOMNesting / nested-anchor / legacyBehavior / passHref warnings appear in the console.

  1. Go to /dataset → click the + Add dataset button (top right) → lands on /dataset/new.
  2. Go to /add-systems/multiple → click the inline Add a system link in the description → lands on /add-systems/manual.
  3. Go to /data-discovery/action-center → on any monitor card click Review → lands on /data-discovery/action-center/{website|datastore|infrastructure}/{monitorId}.
  4. Go to /user-management → click Add new user (top right) → lands on /user-management/new.
  5. Go to /integrations → on any row, click the integration name in the first column (should be a single blue link, ellipsis-truncated, and the row itself should NOT also navigate) → lands on /integrations/{id}.
  6. On an integration detail page that has a linked system (e.g. the BigQuery Connection seed data), open the Linked system tab → click the system name in the list → lands on /systems/configure/{systemKey}.
  7. Go to /reporting/datamap → click a system cell in the table to open the drawer → click View more in the drawer header → lands on /systems/configure/{systemKey}.
  8. Go to /settings/consent on a TCF config with restrictions → click any Edit button in the restrictions table → lands on /settings/consent/{configId}/{restrictionId}.
  9. Go to / (home dashboard) → on the System Coverage card, click Connect more systems in the card header (text link, Typography.Link style) → lands on /add-systems/manual.

Modifier-click check (optional but worth it): on any of the above links, Cmd/Ctrl-click should open the target in a new tab (not SPA-navigate in place).

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:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • 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 15, 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 Apr 16, 2026 4:32pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored Apr 16, 2026 4:32pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 15, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 8%
6.17% (2707/43826) 5.37% (1337/24863) 4.24% (550/12964)
fides-js Coverage: 78%
78.98% (1962/2484) 65.55% (1214/1852) 72.57% (336/463)
privacy-center Coverage: 88%
85.97% (331/385) 81.36% (179/220) 78.87% (56/71)

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: ENG-3461 — RouterLink / legacyBehavior migration

This is a well-scoped, well-executed cleanup PR. The motivation is clear, the new RouterLink component is documented, and the 10-test suite covers all meaningful branches. The changelog entry and manual testing steps are thorough. Overall the change is in good shape.

Summary of findings

⚠️ Notable (worth fixing before merge):

  1. Private Next.js internal import (next/dist/shared/lib/router/utils/format-url) — The formatUrl function is pulled from Next.js's internal dist path, which is not part of the public API and can break on any Next.js update. A small inline formatHref helper using URLSearchParams would remove this fragility entirely. See inline comment on RouterLink.tsx:2.

ℹ️ Informational (no action required, just noting):

  1. Silent prop drop for replace/scroll/prefetch in text mode — These props are only forwarded on the Button path; they silently no-op in text mode. The JSDoc partially covers this but "no-op in text mode" could be called out more explicitly. No current call site passes these in text mode, so there's no active bug.

  2. Button detection via reference equalityonly.type === Button correctly identifies direct Button usages but won't catch HOC-wrapped or memoized variants. The JSDoc already documents the limitation.

  3. MonitorResult.tsxNextLink import retained intentionally — Confirmed: there is a second <NextLink> usage in that file (the monitor name link) that uses the modern non-legacyBehavior form, so the import is not orphaned.

  4. Cypress test uses .parent("a") — Functionally correct but slightly fragile compared to .closest("a"). Minor nit.

What's done well

  • All 20 legacyBehavior/passHref call sites migrated; git grep 'passHref\|legacyBehavior' returns zero hits after this PR.
  • Both render paths (Button-wrap via next/link, text-mode via Typography.Link + router.push) are correctly implemented with proper modifier-click pass-through.
  • target="_blank" correctly bypasses SPA interception on both paths, and rel="noopener noreferrer" is added where needed (AssessmentSettingsModal).
  • The AssessmentSettingsModal.test.tsx next/router mock addition prevents a previously-latent test breakage.
  • The key prop is preserved on migrated list-rendering usages (e.g., ConfidenceCard, MonitorResult).

The only item worth addressing before merge is the next/dist internal import on line 2 of RouterLink.tsx.

🔬 Codegraph: unavailable


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

Comment thread clients/admin-ui/src/features/common/nav/RouterLink.tsx Outdated
Comment thread clients/admin-ui/src/features/common/nav/RouterLink.tsx Outdated
Comment thread clients/admin-ui/src/features/common/nav/RouterLink.tsx
Comment thread clients/admin-ui/cypress/e2e/action-center/aggregate-results.cy.ts Outdated
Copy link
Copy Markdown
Contributor

@rayharnett rayharnett 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 7942

Branch: gill/ENG-2987/link-legacy-behavior
Status: ✅ LGTM (Looks Good To Me)

Summary of Changes

The PR introduces a new shared RouterLink component in the Admin UI to replace usage of next/link with legacyBehavior and passHref. This change aims to unify internal navigation and simplify how components like Ant Design's Button are wrapped for client-side routing.

Key Improvements

1. Unified Navigation Logic

The new RouterLink handles both plain text (using Typography.Link) and component wrapping (specifically detecting single Ant Design Button children) to ensure correct HTML structure without nested anchors. This solves the issue where Next.js and custom components would produce invalid nested <a> tags.

2. Improved Developer Experience

Developers no longer need to remember to use legacyBehavior or passHref when wrapping custom components. The component intelligently detects its children to apply the correct rendering strategy.

3. Enhanced Functionality

  • URL Serialization: Built-in support for UrlObject serialization via a robust formatHref utility that handles complex query objects, including arrays and null/undefined values.
  • Intelligent Prefetching: Supports prefetching on mount (in production) and provides an optimized onMouseEnter prefetch strategy for non-production environments to improve perceived performance.
  • Native Browser Compatibility: Correctly handles modifier keys (meta, ctrl, shift, alt) and middle clicks, allowing standard browser features like "Open in new tab" or "Copy link" to function as expected.

4. Comprehensive Testing

A robust test suite (RouterLink.test.tsx) covers:

  • formatHref edge cases (query serialization, hash handling, encoding).
  • RouterLink interaction patterns (Button wrapping, text mode, prefetching, and modifier key interception).

Detailed Review Findings

🟢 Pass: Technical Correctness & Architecture

  • Structural Detection: The use of isAntButtonChild to decide between wrapping with NextLink or using a styled anchor is an effective way to avoid the "nested anchor" issue in modern Next.js versions.
  • Event Handling: The implementation of onClick within RouterLink correctly intercepts only standard left-clicks while allowing native browser navigation for modifier keys. This is crucial for accessibility and UX.
  • Refactoring Scope: The replacement of NextLink across multiple features (access-policies, consent-settings, data-discovery, etc.) is thorough and consistent.

🟡 Minor Observations / Suggestions

  • Complexity in Detection: The isAntButtonChild logic relies on a strict structural check (a single child that is exactly the Button component). While effective, this is "magic" behavior. It's recommended to ensure any future custom components intended for use here are either wrapped in a fragment or handled via an explicit prop if they don't meet this specific criteria.
  • Prefetching Granularity: The onMouseEnter prefetching is a great optimization. As the application grows, we should monitor that large tables with many links do not cause excessive network noise during rapid mouse movement.

🔴 Security & Compliance (Zero Tolerance Check)

  • Sensitive Data: Verified that no real customer data, internal URLs (*.ethyca.com), or private credentials were introduced in the code or test data.
  • Compliance: The PR adheres to all project security guidelines regarding placeholder usage and generic error messages.

Final Verdict

The refactor is well-architected, thoroughly tested, and provides a significant improvement to the codebase's maintainability and consistency by abstracting away the complexities of Next.js link behavior.

Copy link
Copy Markdown
Contributor

@rayharnett rayharnett left a comment

Choose a reason for hiding this comment

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

Approved, see prior review.

@gilluminate gilluminate enabled auto-merge April 16, 2026 16:11
gilluminate and others added 6 commits April 16, 2026 10:15
Next 16 has dropped passHref/legacyBehavior from the public Link API.
Introduces a shared RouterLink (admin-ui/features/common/nav) that
auto-detects antd Button children vs Typography.Link-styled text, so
callers no longer need to hand-wire next/link + passHref + legacyBehavior.

Migrates all 12 legacyBehavior usages to RouterLink and adds tests for
both rendering modes and modifier-click fall-through.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Completes the Next 16 Link migration by sweeping the 8 sites that
still used `<NextLink passHref>` on the merge base. Adds `target`
and `rel` forwarding to RouterLink so the Slack-config link in
AssessmentSettingsModal can keep its `target="_blank"` behaviour,
and bails out of SPA interception in text mode when the caller
opts out with `target="_blank"`.

Migrated:
  pages/access-policies/index.tsx (New policy button)
  pages/privacy-assessments/[id].tsx (Back to list button)
  pages/settings/rbac/index.tsx (Create role button)
  pages/settings/rbac/roles/new.tsx (Cancel button)
  home/SystemCoverageCard.tsx (Connect more systems text link)
  features/privacy-assessments/AssessmentSettingsModal.tsx (Configure Slack, target=_blank)
  features/user-management/RolesForm.tsx (Cancel button)
  features/data-discovery-and-detection/action-center/ConfidenceCard.tsx (Review button)

After this commit `git grep passHref\|legacyBehavior` in clients/admin-ui
returns zero hits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Drop next/dist private import; add inline formatHref helper with 12 unit tests
- Honour replace/scroll/prefetch in text mode (router.replace, scroll option, mount/hover prefetch matching next/link semantics)
- Switch Cypress assertions from .parent("a") to .closest("a") for resilience

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ControlForm.tsx and pages/access-policies/ gained passHref usages
via PR #7918 merging into main. Migrated to RouterLink for consistency.
git grep 'passHref|legacyBehavior' in admin-ui/src now returns zero hits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gilluminate gilluminate added this pull request to the merge queue Apr 16, 2026
Merged via the queue into main with commit 0c5231c Apr 16, 2026
50 of 51 checks passed
@gilluminate gilluminate deleted the gill/ENG-3461/link-legacy-behavior branch April 16, 2026 16:46
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.

2 participants