diff --git a/web/default/src/features/keys/api.ts b/web/default/src/features/keys/api.ts index cd0954202b1..80727e56466 100644 --- a/web/default/src/features/keys/api.ts +++ b/web/default/src/features/keys/api.ts @@ -42,7 +42,7 @@ export async function getApiKeys( // Search API keys by keyword or token (with pagination) export async function searchApiKeys( params: SearchApiKeysParams -): Promise<{ success: boolean; message?: string; data?: ApiKey[] }> { +): Promise { const { keyword = '', token = '', p, size } = params const queryParams = new URLSearchParams() if (keyword) queryParams.set('keyword', keyword) diff --git a/web/default/src/features/keys/components/api-keys-table.tsx b/web/default/src/features/keys/components/api-keys-table.tsx index 46a605e298b..83d442228cb 100644 --- a/web/default/src/features/keys/components/api-keys-table.tsx +++ b/web/default/src/features/keys/components/api-keys-table.tsx @@ -30,6 +30,7 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table' +import { useDebounce } from '@/hooks' import { Database } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -43,6 +44,7 @@ import { EmptyMedia, EmptyTitle, } from '@/components/ui/empty' +import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import { DISABLED_ROW_DESKTOP, @@ -208,9 +210,35 @@ export function ApiKeysTable() { navigate: route.useNavigate(), pagination: { defaultPage: 1, defaultPageSize: 20 }, globalFilter: { enabled: true, key: 'filter' }, - columnFilters: [{ columnId: 'status', searchKey: 'status', type: 'array' }], + columnFilters: [ + { columnId: 'status', searchKey: 'status', type: 'array' }, + { columnId: '_tokenSearch', searchKey: 'token', type: 'string' }, + ], }) + const tokenFilterFromUrl = + (columnFilters.find((f) => f.id === '_tokenSearch')?.value as string) || '' + const [tokenFilterInput, setTokenFilterInput] = useState(tokenFilterFromUrl) + const debouncedTokenFilter = useDebounce(tokenFilterInput, 500) + + useEffect(() => { + setTokenFilterInput(tokenFilterFromUrl) + }, [tokenFilterFromUrl]) + + useEffect(() => { + if (debouncedTokenFilter !== tokenFilterFromUrl) { + onColumnFiltersChange((prev) => { + const filtered = prev.filter((f) => f.id !== '_tokenSearch') + return debouncedTokenFilter + ? [...filtered, { id: '_tokenSearch', value: debouncedTokenFilter }] + : filtered + }) + } + }, [debouncedTokenFilter, tokenFilterFromUrl, onColumnFiltersChange]) + + const tokenFilter = tokenFilterFromUrl + const shouldSearch = Boolean(globalFilter?.trim() || tokenFilter.trim()) + // Fetch data with React Query // eslint-disable-next-line @tanstack/query/exhaustive-deps const { data, isLoading, isFetching } = useQuery({ @@ -219,32 +247,31 @@ export function ApiKeysTable() { pagination.pageIndex + 1, pagination.pageSize, globalFilter, + tokenFilter, refreshTrigger, ], queryFn: async () => { - // If there's a global filter, use search - const hasFilter = globalFilter?.trim() - - if (hasFilter) { - const result = await searchApiKeys({ keyword: globalFilter }) - if (!result.success) { - toast.error(result.message || t(ERROR_MESSAGES.SEARCH_FAILED)) - return { items: [], total: 0 } - } - return { - items: result.data || [], - total: result.data?.length || 0, - } - } - - // Otherwise use pagination - const result = await getApiKeys({ - p: pagination.pageIndex + 1, - size: pagination.pageSize, - }) + const result = shouldSearch + ? await searchApiKeys({ + keyword: globalFilter, + token: tokenFilter, + p: pagination.pageIndex + 1, + size: pagination.pageSize, + }) + : await getApiKeys({ + p: pagination.pageIndex + 1, + size: pagination.pageSize, + }) if (!result.success) { - toast.error(result.message || t(ERROR_MESSAGES.LOAD_FAILED)) + toast.error( + result.message || + t( + shouldSearch + ? ERROR_MESSAGES.SEARCH_FAILED + : ERROR_MESSAGES.LOAD_FAILED + ) + ) return { items: [], total: 0 } } @@ -273,13 +300,7 @@ export function ApiKeysTable() { onRowSelectionChange: setRowSelection, onSortingChange: setSorting, onColumnVisibilityChange: setColumnVisibility, - globalFilterFn: (row, _columnId, filterValue) => { - const name = String(row.getValue('name')).toLowerCase() - const key = String(row.original.key).toLowerCase() - const searchValue = String(filterValue).toLowerCase() - - return name.includes(searchValue) || key.includes(searchValue) - }, + globalFilterFn: () => true, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), @@ -289,10 +310,8 @@ export function ApiKeysTable() { onPaginationChange, onGlobalFilterChange, onColumnFiltersChange, - manualPagination: !globalFilter, - pageCount: globalFilter - ? Math.ceil((data?.total || 0) / pagination.pageSize) - : Math.ceil((data?.total || 0) / pagination.pageSize), + manualPagination: true, + pageCount: Math.ceil((data?.total || 0) / pagination.pageSize), }) const pageCount = table.getPageCount() @@ -312,7 +331,16 @@ export function ApiKeysTable() { )} skeletonKeyPrefix='api-keys-skeleton' toolbarProps={{ - searchPlaceholder: t('Filter by name or key...'), + searchPlaceholder: t('Filter by name...'), + additionalSearch: ( + setTokenFilterInput(e.target.value)} + className='w-full sm:w-50 lg:w-60' + /> + ), filters: [ { columnId: 'status', diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index ab736093e2f..6f737fea8a8 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -1729,8 +1729,9 @@ "Filter by Midjourney task ID": "Filter by Midjourney task ID", "Filter by model name...": "Filter by model name...", "Filter by model...": "Filter by model...", + "Filter by API key...": "Filter by API key...", "Filter by name or ID...": "Filter by name or ID...", - "Filter by name or key...": "Filter by name or key...", + "Filter by name...": "Filter by name...", "Filter by name, ID, or key...": "Filter by name, ID, or key...", "Filter by price field": "Filter by price field", "Filter by ratio type": "Filter by ratio type", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 7f1aae94734..43d23f97c69 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -1729,8 +1729,9 @@ "Filter by Midjourney task ID": "Filtrer par ID de tâche Midjourney", "Filter by model name...": "Filtrer par nom du modèle...", "Filter by model...": "Filtrer par modèle...", + "Filter by API key...": "Filtrer par clé API...", "Filter by name or ID...": "Filtrer par nom ou ID...", - "Filter by name or key...": "Filtrer par nom ou clé...", + "Filter by name...": "Filtrer par nom...", "Filter by name, ID, or key...": "Filtrer par nom, ID ou clé...", "Filter by price field": "Filtrer par champ de prix", "Filter by ratio type": "Filtrer par type de ratio", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index c317c61264f..0b88ab71b2c 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -1729,8 +1729,9 @@ "Filter by Midjourney task ID": "MidjourneyタスクIDでフィルター", "Filter by model name...": "モデル名でフィルター...", "Filter by model...": "モデルでフィルタリング...", + "Filter by API key...": "APIキーでフィルター...", "Filter by name or ID...": "名前またはIDでフィルター...", - "Filter by name or key...": "名前またはキーでフィルター...", + "Filter by name...": "名前でフィルター...", "Filter by name, ID, or key...": "名前、ID、またはキーでフィルター...", "Filter by price field": "価格フィールドでフィルター", "Filter by ratio type": "倍率タイプで絞り込み", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index baa9f2909fb..da922903673 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -1729,8 +1729,9 @@ "Filter by Midjourney task ID": "Фильтр по ID задачи Midjourney", "Filter by model name...": "Фильтр по имени модели...", "Filter by model...": "Фильтровать по модели...", + "Filter by API key...": "Фильтр по API-ключу...", "Filter by name or ID...": "Фильтр по имени или ID...", - "Filter by name or key...": "Фильтровать по имени или ключу...", + "Filter by name...": "Фильтр по имени...", "Filter by name, ID, or key...": "Фильтровать по имени, ID или ключу...", "Filter by price field": "Фильтр по полю цены", "Filter by ratio type": "Фильтровать по типу коэффициента", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 2bc63aae329..2b0de37332b 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -1729,8 +1729,9 @@ "Filter by Midjourney task ID": "Lọc theo ID nhiệm vụ Midjourney", "Filter by model name...": "Lọc theo tên mô hình...", "Filter by model...": "Lọc theo mẫu...", + "Filter by API key...": "Lọc theo khóa API...", "Filter by name or ID...": "Lọc theo tên hoặc ID...", - "Filter by name or key...": "Lọc theo tên hoặc khóa...", + "Filter by name...": "Lọc theo tên...", "Filter by name, ID, or key...": "Lọc theo tên, ID hoặc khóa...", "Filter by price field": "Lọc theo trường giá", "Filter by ratio type": "Lọc theo loại tỷ lệ", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index d1c5a906d47..8346ba10224 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -1729,8 +1729,9 @@ "Filter by Midjourney task ID": "按 Midjourney 任务 ID 筛选", "Filter by model name...": "按模型名称筛选...", "Filter by model...": "按模型筛选...", + "Filter by API key...": "按 API 密钥筛选...", "Filter by name or ID...": "按名称或 ID 筛选...", - "Filter by name or key...": "按名称或密钥筛选...", + "Filter by name...": "按名称筛选...", "Filter by name, ID, or key...": "按名称、ID 或密钥筛选...", "Filter by price field": "按价格字段筛选", "Filter by ratio type": "按倍率类型筛选", diff --git a/web/default/src/routes/_authenticated/keys/index.tsx b/web/default/src/routes/_authenticated/keys/index.tsx index 6fb3ac29820..b956042e3ee 100644 --- a/web/default/src/routes/_authenticated/keys/index.tsx +++ b/web/default/src/routes/_authenticated/keys/index.tsx @@ -29,6 +29,7 @@ const apiKeySearchSchema = z.object({ .optional() .catch([]), filter: z.string().optional().catch(''), + token: z.string().optional().catch(''), }) export const Route = createFileRoute('/_authenticated/keys/')({