Skip to content

ENG-3001: Add OAuth API clients list page#7747

Open
tvandort wants to merge 4 commits intomainfrom
ENG-3001-ui-list
Open

ENG-3001: Add OAuth API clients list page#7747
tvandort wants to merge 4 commits intomainfrom
ENG-3001-ui-list

Conversation

@tvandort
Copy link
Contributor

@tvandort tvandort commented Mar 24, 2026

Ticket ENG-3001

Description Of Changes

Adds navigation, RTK Query slice, API types, table component and tests,
and the /api-clients index page so users can view all OAuth clients.

Code Changes

  • Implement API slices
  • Add OAuth client list page

Steps to Confirm

  1. Go to the /api-clients or navigate to the page through Settings > API clients
  2. Confirm that the page loads. (An api client may not exist yet)
  3. call /api/v1/oauth/client
  4. refresh /api-clients and check that the new client is listed

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

tvandort and others added 2 commits March 24, 2026 13:16
Adds navigation, RTK Query slice, API types, table component and tests,
and the /api-clients index page so users can view all OAuth clients.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tvandort tvandort requested a review from a team as a code owner March 24, 2026 18:05
@tvandort tvandort requested review from speaker-ender and removed request for a team March 24, 2026 18:05
@vercel
Copy link
Contributor

vercel bot commented Mar 24, 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 Mar 24, 2026 8:28pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
fides-privacy-center Ignored Ignored Mar 24, 2026 8:28pm

Request Review

Copy link

@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.

Overall this is a clean, well-scoped addition. The RTK Query slice is consistent with existing patterns, the types are properly generated/exported, the nav wiring is correct, and the test coverage is solid for a list component. A few things to address:

Suggestion (worth fixing before merge)

  • navigator.clipboard.writeText() in ClipboardButton.tsx returns a Promise whose rejection is silently swallowed. The tooltip will show "Copied!" even when the write failed. The fix is straightforward — move setTooltipText into .then() and add a .catch().
  • The error value from useOAuthClientsList is never rendered. A failed API call looks identical to an empty list from the user's perspective.

Nice to have

  • Empty state copy references a "Create API client" button that doesn't exist in this PR yet.
  • rotateOAuthClientSecret is the only mutation without invalidatesTags, which is inconsistent and could cause stale cache issues when a detail page is added.

<div>
<List
loading={isLoading}
itemLayout="horizontal"
Copy link

Choose a reason for hiding this comment

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

Suggestion: surface API errors to the user

error is returned from useOAuthClientsList but never destructured or rendered in OAuthClientsList. If the API call fails, the component shows an empty list with no feedback — the user has no way to distinguish a genuine "no clients" state from a failed request.

Consider adding a simple error banner or inline message, e.g.:

const { data, total, isLoading, error, page, pageSize, setPage, setPageSize } =
  useOAuthClientsList();

if (error) {
  return <Alert type="error" message="Failed to load API clients." />;
}

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR adds the OAuth API clients list page (/api-clients) to the admin UI, including an RTK Query slice with full CRUD + secret rotation endpoints, a list component with permission-aware linking, pagination, clipboard copy of client IDs, navigation config, and API type definitions. The feature is well-structured and the test coverage is solid.

Key findings:

  • P1 – Unhandled clipboard promise: navigator.clipboard.writeText() in ClipboardButton.tsx returns a promise that is not caught. If the write fails the tooltip still shows "Copied!" even though nothing was copied.
  • P2 – Custom pagination state bypasses URL sync: useOAuthClientsList uses plain useState for page/pageSize instead of the existing usePagination/useAntPagination hook, so the current page is lost on refresh and browser back/forward history won't reflect pagination.
  • P2 – Missing invalidateTags on rotate mutation: The secret-rotation mutation in oauth-clients.slice.ts does not invalidate any cache tags, inconsistent with every other mutation in the same slice.
  • P2 – useHasPermission called per list item: The permission check is performed once per rendered ClientListItem; it should be hoisted to the parent component and passed as a prop.
  • P2 – Bare div wrapper: The outer container in OAuthClientsList is a plain <div> rather than the preferred <Flex vertical> component.

Confidence Score: 3/5

  • Safe to use once the clipboard promise is handled and pagination is wired to the URL-sync hook; the remaining items are clean-up.
  • The P1 clipboard issue means the UI silently misreports success to users when a clipboard write fails — a real UX bug on unsupported or unfocused contexts. The pagination URL-sync omission is a meaningful regression (page state lost on refresh) given the project has a ready-made hook for exactly this purpose. Both are straightforward fixes, but they should land before the feature ships.
  • clients/admin-ui/src/features/common/ClipboardButton.tsx (unhandled promise), clients/admin-ui/src/features/oauth/OAuthClientsTable.tsx (pagination state + permission hook placement + div wrapper)

Important Files Changed

Filename Overview
clients/admin-ui/src/features/oauth/OAuthClientsTable.tsx New list component for OAuth clients. Uses custom useState-based pagination instead of the existing usePagination hook (URL state won't persist on refresh), calls useHasPermission per-item instead of once, and uses a bare div wrapper instead of Flex.
clients/admin-ui/src/features/common/ClipboardButton.tsx Replaced useChakraClipboard with navigator.clipboard.writeText, but the returned promise is not handled — if the clipboard write fails the tooltip still shows "Copied!" misleading users.
clients/admin-ui/src/features/oauth/oauth-clients.slice.ts New RTK Query slice for OAuth clients. Well-structured with proper providesTags/invalidateTags on most mutations; the rotate-credentials mutation is missing invalidateTags.
clients/admin-ui/src/features/oauth/OAuthClientsTable.test.tsx Good test coverage for the list component, covering rendering, empty/loading states, permission-based link rendering, and pagination.
clients/admin-ui/src/pages/api-clients/index.tsx New Next.js page for the API clients list. Clean, minimal page component using existing layout primitives.

Reviews (1): Last reviewed commit: "Remove create flow from list page (added..." | Re-trigger Greptile

Comment on lines +68 to +87
const useOAuthClientsList = () => {
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);

const { data, isLoading, error } = useListOAuthClientsQuery({
page,
size: pageSize,
});

return {
data: data?.items ?? [],
total: data?.total ?? 0,
isLoading,
error,
page,
pageSize,
setPage,
setPageSize,
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Custom pagination state bypasses URL sync hook

The useOAuthClientsList hook manages page and pageSize with plain useState, but the project provides a dedicated usePagination (or its Ant Design wrapper useAntPagination) hook in src/features/common/pagination/ that synchronises pagination state with URL query parameters. Using local state means the current page is lost on refresh or navigation, and back/forward browser history won't work correctly.

Consider using useAntPagination from ~/features/common/pagination/useAntPagination:

import { useAntPagination } from "~/features/common/pagination/useAntPagination";

const useOAuthClientsList = () => {
  const { pageIndex, pageSize, paginationProps } = useAntPagination();

  const { data, isLoading, error } = useListOAuthClientsQuery({
    page: pageIndex,
    size: pageSize,
  });

  return {
    data: data?.items ?? [],
    total: data?.total ?? 0,
    isLoading,
    error,
    paginationProps,
  };
};

Rule Used: Use the existing usePagination hook for paginati... (source)

Learnt From
ethyca/fides#6594

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +18 to +19
const ClientListItem = ({ client }: { client: ClientResponse }) => {
const canUpdate = useHasPermission([ScopeRegistryEnum.CLIENT_UPDATE]);
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 useHasPermission called per list item instead of once

useHasPermission is invoked inside ClientListItem, which is rendered once for every client in the list. The permission result is the same for all items in the list (it doesn't depend on the client prop), so calling the hook once at the parent level (OAuthClientsList) and passing the result as a prop would be more efficient and cleaner.

// In OAuthClientsList
const canUpdate = useHasPermission([ScopeRegistryEnum.CLIENT_UPDATE]);
// ...
renderItem={(client) => <ClientListItem client={client} canUpdate={canUpdate} />}
// ClientListItem receives it as a prop
const ClientListItem = ({ client, canUpdate }: { client: ClientResponse; canUpdate: boolean }) => {

useOAuthClientsList();

return (
<div>
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Prefer Flex over bare div wrapper

The outer <div> on line 94 is the layout wrapper for the list and pagination. Per project style, prefer Ant Design's Flex (vertical) component over plain div elements when possible.

Suggested change
<div>
<Flex vertical>

Rule Used: Avoid using div elements when possible. Use sema... (source)

Learnt From
ethyca/fides#6763

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +69 to +76
rotateOAuthClientSecret: build.mutation<ClientSecretRotateResponse, string>(
{
query: (clientId) => ({
url: `oauth/client/${clientId}/secret`,
method: "POST",
}),
},
),
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Missing invalidateTags on the rotate mutation

Every other mutation in this slice (createOAuthClient, updateOAuthClient, deleteOAuthClient) includes invalidateTags so related cache entries refresh automatically. The rotation mutation has no invalidateTags, meaning that if a future detail page shows metadata that changes after rotation (e.g. a last-rotated timestamp), the view will stay stale until a manual refetch.

Add invalidateTags targeting the specific client's cache entry:

invalidatesTags: (_result, _error, clientId) => [
  { type: "OAuth Client", id: clientId },
],

Rule Used: When adding new RTK Query mutation endpoints, incl... (source)

Learnt From
ethyca/fides#6711

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.

1 participant