fix: handle paginated API key search response (#5014)
* fix: handle paginated API key search response * fix: add accessible label to API key filter
This commit is contained in:
parent
465c5edab9
commit
349d5429ca
2
web/default/src/features/keys/api.ts
vendored
2
web/default/src/features/keys/api.ts
vendored
@ -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<GetApiKeysResponse> {
|
||||
const { keyword = '', token = '', p, size } = params
|
||||
const queryParams = new URLSearchParams()
|
||||
if (keyword) queryParams.set('keyword', keyword)
|
||||
|
||||
@ -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,
|
||||
@ -207,9 +209,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({
|
||||
@ -218,32 +246,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 }
|
||||
}
|
||||
|
||||
@ -272,13 +299,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(),
|
||||
@ -288,10 +309,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()
|
||||
@ -311,7 +330,16 @@ export function ApiKeysTable() {
|
||||
)}
|
||||
skeletonKeyPrefix='api-keys-skeleton'
|
||||
toolbarProps={{
|
||||
searchPlaceholder: t('Filter by name or key...'),
|
||||
searchPlaceholder: t('Filter by name...'),
|
||||
additionalSearch: (
|
||||
<Input
|
||||
placeholder={t('Filter by API key...')}
|
||||
aria-label={t('Filter by API key...')}
|
||||
value={tokenFilterInput}
|
||||
onChange={(e) => setTokenFilterInput(e.target.value)}
|
||||
className='w-full sm:w-50 lg:w-60'
|
||||
/>
|
||||
),
|
||||
filters: [
|
||||
{
|
||||
columnId: 'status',
|
||||
|
||||
3
web/default/src/i18n/locales/en.json
vendored
3
web/default/src/i18n/locales/en.json
vendored
@ -1715,8 +1715,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",
|
||||
|
||||
3
web/default/src/i18n/locales/fr.json
vendored
3
web/default/src/i18n/locales/fr.json
vendored
@ -1715,8 +1715,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",
|
||||
|
||||
3
web/default/src/i18n/locales/ja.json
vendored
3
web/default/src/i18n/locales/ja.json
vendored
@ -1715,8 +1715,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": "倍率タイプで絞り込み",
|
||||
|
||||
3
web/default/src/i18n/locales/ru.json
vendored
3
web/default/src/i18n/locales/ru.json
vendored
@ -1715,8 +1715,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": "Фильтровать по типу коэффициента",
|
||||
|
||||
3
web/default/src/i18n/locales/vi.json
vendored
3
web/default/src/i18n/locales/vi.json
vendored
@ -1715,8 +1715,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ệ",
|
||||
|
||||
3
web/default/src/i18n/locales/zh.json
vendored
3
web/default/src/i18n/locales/zh.json
vendored
@ -1715,8 +1715,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": "按倍率类型筛选",
|
||||
|
||||
@ -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/')({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user