From 143d4a92a62a50d33e60c971703ba7b6483895b1 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 26 Oct 2026 20:17:54 -0400 Subject: [PATCH] refactor(mosaic): compose the API keys table model in its wrapper --- .changeset/mosaic-api-keys-wiring.md | 2 + .../api-keys/api-keys-table.controller.ts | 40 ++++++------------- .../features/api-keys/api-keys-table.model.ts | 6 +++ .../src/features/api-keys/api-keys-table.tsx | 12 +++--- 4 files changed, 28 insertions(+), 32 deletions(-) create mode 100644 .changeset/mosaic-api-keys-wiring.md diff --git a/.changeset/mosaic-api-keys-wiring.md b/.changeset/mosaic-api-keys-wiring.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-api-keys-wiring.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/features/api-keys/api-keys-table.controller.ts b/packages/mosaic/src/features/api-keys/api-keys-table.controller.ts index 5ece9722f34..b529a71d2fd 100644 --- a/packages/mosaic/src/features/api-keys/api-keys-table.controller.ts +++ b/packages/mosaic/src/features/api-keys/api-keys-table.controller.ts @@ -1,36 +1,27 @@ -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { useConfirmController } from '../../blocks/confirm'; +import { useDebouncedAsync } from '../../hooks/use-debounced-async'; import { useListRemovalFocus } from '../../hooks/use-list-removal-focus'; import { useNow } from '../../hooks/use-now'; import { useLocale } from '../../localization'; import { formatDate } from '../../utils/format-date'; import { formatRelativeTime } from './api-keys-table.format'; -import { useAPIKeysTableModel } from './api-keys-table.model'; +import type { APIKeysTableModel } from './api-keys-table.model'; import type { APIKey, APIKeyRecord, APIKeysTableMessages } from './api-keys-table.types'; import { useCreateAPIKeyController } from './create-api-key.controller'; -const SEARCH_DEBOUNCE_MS = 500; - export interface APIKeysTableControllerOptions { - subject: string; - pageSize: number; showDescription: boolean; messages: APIKeysTableMessages; } -export function useAPIKeysTableController({ - subject, - pageSize, - showDescription, - messages, -}: APIKeysTableControllerOptions) { - const [query, setQuery] = useState(''); - const model = useAPIKeysTableModel(subject, pageSize, query); +export function useAPIKeysTableController( + model: APIKeysTableModel, + { showDescription, messages }: APIKeysTableControllerOptions, +) { const locale = useLocale(); const now = useNow({ updateInterval: 60_000 }); - const [searchValue, setSearchValue] = useState(''); - const timer = useRef | undefined>(undefined); const create = useCreateAPIKeyController({ messages, showDescription, onCreate: model.create }); const apiKeys = model.data.map(key => toAPIKey(key, locale, now)); const searchInputRef = useRef(null); @@ -53,8 +44,6 @@ export function useAPIKeysTableController({ errorFallback: messages.revokeError, }); - useEffect(() => () => clearTimeout(timer.current), []); - return { isLoaded: model.isLoaded, isAvailable: model.isAvailable, @@ -65,24 +54,21 @@ export function useAPIKeysTableController({ finalFocus: removalFocus.finalFocus, totalCount: model.count, page: model.page, - searchValue, isLoading: model.isLoading, isFetching: model.isFetching, isError: model.isError, onRetry: () => void model.revalidate(), onPageChange: model.fetchPage, - onSearchChange: (value: string) => { - setSearchValue(value); - clearTimeout(timer.current); - timer.current = setTimeout(() => { - setQuery(value.trim()); - model.fetchPage(1); - }, SEARCH_DEBOUNCE_MS); - }, manage: model.canManage ? { onCreate: create.onOpen, createDialog: create.dialog, revoke } : undefined, }; } +export function useAPIKeysTableSearchController() { + const [searchValue, setSearchValue] = useState(''); + const query = useDebouncedAsync(searchValue, value => Promise.resolve(value.trim()), { delayMs: 500 }); + return { searchValue, onSearchChange: setSearchValue, query: query.data ?? '' }; +} + function toAPIKey(key: APIKeyRecord, locale: string, now: Date): APIKey { return { id: key.id, diff --git a/packages/mosaic/src/features/api-keys/api-keys-table.model.ts b/packages/mosaic/src/features/api-keys/api-keys-table.model.ts index 58a8661b0af..881a4666cd9 100644 --- a/packages/mosaic/src/features/api-keys/api-keys-table.model.ts +++ b/packages/mosaic/src/features/api-keys/api-keys-table.model.ts @@ -2,6 +2,7 @@ import { ClerkRuntimeError } from '@clerk/shared/error'; import { isOrganizationId } from '@clerk/shared/internal/clerk-js/organization'; import { useAPIKeys, useClerk, useSession } from '@clerk/shared/react'; import type { APIKeyResource } from '@clerk/shared/types'; +import { useEffect, useRef } from 'react'; import { useMosaicEnvironment } from '../../hooks/use-mosaic-environment'; import { save } from '../../utils/errors'; @@ -40,6 +41,11 @@ export function useAPIKeysTableModel(subject: string, pageSize: number, query: s keepPreviousData: true, enabled: isAvailable && canRead, }); + const fetchPage = useRef(apiKeys.fetchPage); + fetchPage.current = apiKeys.fetchPage; + useEffect(() => { + fetchPage.current(1); + }, [query]); return { isLoaded, diff --git a/packages/mosaic/src/features/api-keys/api-keys-table.tsx b/packages/mosaic/src/features/api-keys/api-keys-table.tsx index d9c8cd5447a..d89ff58592f 100644 --- a/packages/mosaic/src/features/api-keys/api-keys-table.tsx +++ b/packages/mosaic/src/features/api-keys/api-keys-table.tsx @@ -2,9 +2,9 @@ import { isOrganizationId } from '@clerk/shared/internal/clerk-js/organization'; import type { ReactNode } from 'react'; import { useMessages } from '../../localization'; -import { useAPIKeysTableController } from './api-keys-table.controller'; +import { useAPIKeysTableController, useAPIKeysTableSearchController } from './api-keys-table.controller'; import { resolveAPIKeysTableMessages } from './api-keys-table.messages'; -import { PAGE_SIZE } from './api-keys-table.model'; +import { PAGE_SIZE, useAPIKeysTableModel } from './api-keys-table.model'; import { APIKeysTableSkeleton } from './api-keys-table.skeleton'; import type { APIKeysTableMessages } from './api-keys-table.types'; import { APIKeysTableView } from './api-keys-table.view'; @@ -43,9 +43,9 @@ function SubjectAPIKeysTable({ overrides, ); const pageSize = perPage !== undefined && Number.isInteger(perPage) && perPage > 0 ? perPage : PAGE_SIZE; - const { isLoaded, isAvailable, isLoading, manage, ...controller } = useAPIKeysTableController({ - subject, - pageSize, + const search = useAPIKeysTableSearchController(); + const model = useAPIKeysTableModel(subject, pageSize, search.query); + const { isLoaded, isAvailable, isLoading, manage, ...controller } = useAPIKeysTableController(model, { showDescription, messages, }); @@ -67,6 +67,8 @@ function SubjectAPIKeysTable({ messages={messages} pageSize={pageSize} {...controller} + searchValue={search.searchValue} + onSearchChange={search.onSearchChange} {...manage} /> );