From ef0a4bb479a085c4accfa402463733c563c2aaca Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:28:54 -0700 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Group=20Scope=20Picke?= =?UTF-8?q?r=20and=20Name=20Resolution=20Past=20200=20Groups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scope picker's create view fetched a single list capped at the backend's 200 per request maximum and filtered it client side via cmdk, so in deployments with more groups (issue #96 reports 1500+ from Entra SSO sync) most groups could never be found or selected. The groups section now searches server side with a debounced query and offset pagination through a new useGroupSearch hook built on the existing getGroupsFn search/limit/offset support, mirroring the GroupsTab pattern. getAvailableScopesFn built its group name map from that same first page of 200 groups, so existing group scopes outside the window displayed their raw Mongo ObjectId instead of the group name. It now resolves names for exactly the group principalIds that have config overrides via GET /api/admin/groups/:id, fetched in small parallel batches, and falls back to the principalId when an individual group cannot be fetched. --- .../configuration/ScopeSelector.tsx | 120 ++++++++----- src/hooks/index.ts | 1 + src/hooks/useGroupSearch.test.tsx | 131 ++++++++++++++ src/hooks/useGroupSearch.ts | 45 +++++ src/locales/en/translation.json | 1 + src/server/groups.ts | 27 +++ src/server/scopes.names.test.ts | 168 ++++++++++++++++++ src/server/scopes.ts | 29 ++- 8 files changed, 463 insertions(+), 59 deletions(-) create mode 100644 src/hooks/useGroupSearch.test.tsx create mode 100644 src/hooks/useGroupSearch.ts create mode 100644 src/server/scopes.names.test.ts diff --git a/src/components/configuration/ScopeSelector.tsx b/src/components/configuration/ScopeSelector.tsx index 9af2d913..aa69ccf3 100644 --- a/src/components/configuration/ScopeSelector.tsx +++ b/src/components/configuration/ScopeSelector.tsx @@ -10,12 +10,12 @@ import type * as t from '@/types'; import { availableScopesOptions, allRolesQueryOptions, - allGroupsQueryOptions, createScopeFn, deleteScopeFn, } from '@/server'; +import { SearchInput, Pagination } from '@/components/shared'; +import { useGroupSearch, useLocalize } from '@/hooks'; import { getScopeTypeConfig } from '@/constants'; -import { useLocalize } from '@/hooks'; import { cn } from '@/utils'; // ── Main selector ─────────────────────────────────────────────────── @@ -47,10 +47,8 @@ export function ScopeSelector({ enabled: open && showCreate, }); - const { data: allGroups = [] } = useQuery({ - ...allGroupsQueryOptions, - enabled: open && showCreate, - }); + const groupSearch = useGroupSearch(open && showCreate); + const { onSearchChange: onGroupSearchChange } = groupSearch; const handleSearchChange = useCallback((value: string) => { setSearch(value); @@ -62,7 +60,8 @@ export function ScopeSelector({ setCreating(false); setDeleteTarget(null); setDeleting(false); - }, []); + onGroupSearchChange(''); + }, [onGroupSearchChange]); const close = useCallback(() => { onOpenChange(false); @@ -104,8 +103,9 @@ export function ScopeSelector({ ); const availableGroups = useMemo( - () => allGroups.filter((g) => !existingScopeKeys.has(`${PrincipalType.GROUP}:${g.id}`)), - [allGroups, existingScopeKeys], + () => + groupSearch.groups.filter((g) => !existingScopeKeys.has(`${PrincipalType.GROUP}:${g.id}`)), + [groupSearch.groups, existingScopeKeys], ); const handleCreateForRole = useCallback( @@ -260,6 +260,56 @@ export function ScopeSelector({ if (showCreate) { const noRoles = availableRoles.length === 0; const noGroups = availableGroups.length === 0; + const groupsEmptyKey = groupSearch.search + ? 'com_scope_no_matching_groups' + : 'com_scope_no_available_groups'; + + const renderGroupsContent = () => { + if (groupSearch.isLoading) { + return ( +
+ {localize(groupsEmptyKey)} +
+ ); + } + return ( +- {localize('com_scope_no_available_groups')} -
- ) : ( - availableGroups.map((group) => ( - - )) +