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:
Vendored
+1
-1
@@ -42,7 +42,7 @@ export async function getApiKeys(
|
|||||||
// Search API keys by keyword or token (with pagination)
|
// Search API keys by keyword or token (with pagination)
|
||||||
export async function searchApiKeys(
|
export async function searchApiKeys(
|
||||||
params: SearchApiKeysParams
|
params: SearchApiKeysParams
|
||||||
): Promise<{ success: boolean; message?: string; data?: ApiKey[] }> {
|
): Promise<GetApiKeysResponse> {
|
||||||
const { keyword = '', token = '', p, size } = params
|
const { keyword = '', token = '', p, size } = params
|
||||||
const queryParams = new URLSearchParams()
|
const queryParams = new URLSearchParams()
|
||||||
if (keyword) queryParams.set('keyword', keyword)
|
if (keyword) queryParams.set('keyword', keyword)
|
||||||
|
|||||||
+62
-34
@@ -30,6 +30,7 @@ import {
|
|||||||
getSortedRowModel,
|
getSortedRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from '@tanstack/react-table'
|
} from '@tanstack/react-table'
|
||||||
|
import { useDebounce } from '@/hooks'
|
||||||
import { Database } from 'lucide-react'
|
import { Database } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@@ -43,6 +44,7 @@ import {
|
|||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '@/components/ui/empty'
|
} from '@/components/ui/empty'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import {
|
import {
|
||||||
DISABLED_ROW_DESKTOP,
|
DISABLED_ROW_DESKTOP,
|
||||||
@@ -207,9 +209,35 @@ export function ApiKeysTable() {
|
|||||||
navigate: route.useNavigate(),
|
navigate: route.useNavigate(),
|
||||||
pagination: { defaultPage: 1, defaultPageSize: 20 },
|
pagination: { defaultPage: 1, defaultPageSize: 20 },
|
||||||
globalFilter: { enabled: true, key: 'filter' },
|
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
|
// Fetch data with React Query
|
||||||
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
||||||
const { data, isLoading, isFetching } = useQuery({
|
const { data, isLoading, isFetching } = useQuery({
|
||||||
@@ -218,32 +246,31 @@ export function ApiKeysTable() {
|
|||||||
pagination.pageIndex + 1,
|
pagination.pageIndex + 1,
|
||||||
pagination.pageSize,
|
pagination.pageSize,
|
||||||
globalFilter,
|
globalFilter,
|
||||||
|
tokenFilter,
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
],
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// If there's a global filter, use search
|
const result = shouldSearch
|
||||||
const hasFilter = globalFilter?.trim()
|
? await searchApiKeys({
|
||||||
|
keyword: globalFilter,
|
||||||
if (hasFilter) {
|
token: tokenFilter,
|
||||||
const result = await searchApiKeys({ keyword: globalFilter })
|
p: pagination.pageIndex + 1,
|
||||||
if (!result.success) {
|
size: pagination.pageSize,
|
||||||
toast.error(result.message || t(ERROR_MESSAGES.SEARCH_FAILED))
|
})
|
||||||
return { items: [], total: 0 }
|
: await getApiKeys({
|
||||||
}
|
p: pagination.pageIndex + 1,
|
||||||
return {
|
size: pagination.pageSize,
|
||||||
items: result.data || [],
|
})
|
||||||
total: result.data?.length || 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise use pagination
|
|
||||||
const result = await getApiKeys({
|
|
||||||
p: pagination.pageIndex + 1,
|
|
||||||
size: pagination.pageSize,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!result.success) {
|
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 }
|
return { items: [], total: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,13 +299,7 @@ export function ApiKeysTable() {
|
|||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
globalFilterFn: (row, _columnId, filterValue) => {
|
globalFilterFn: () => true,
|
||||||
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)
|
|
||||||
},
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
@@ -288,10 +309,8 @@ export function ApiKeysTable() {
|
|||||||
onPaginationChange,
|
onPaginationChange,
|
||||||
onGlobalFilterChange,
|
onGlobalFilterChange,
|
||||||
onColumnFiltersChange,
|
onColumnFiltersChange,
|
||||||
manualPagination: !globalFilter,
|
manualPagination: true,
|
||||||
pageCount: globalFilter
|
pageCount: Math.ceil((data?.total || 0) / pagination.pageSize),
|
||||||
? Math.ceil((data?.total || 0) / pagination.pageSize)
|
|
||||||
: Math.ceil((data?.total || 0) / pagination.pageSize),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const pageCount = table.getPageCount()
|
const pageCount = table.getPageCount()
|
||||||
@@ -311,7 +330,16 @@ export function ApiKeysTable() {
|
|||||||
)}
|
)}
|
||||||
skeletonKeyPrefix='api-keys-skeleton'
|
skeletonKeyPrefix='api-keys-skeleton'
|
||||||
toolbarProps={{
|
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: [
|
filters: [
|
||||||
{
|
{
|
||||||
columnId: 'status',
|
columnId: 'status',
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1715,8 +1715,9 @@
|
|||||||
"Filter by Midjourney task ID": "Filter by Midjourney task ID",
|
"Filter by Midjourney task ID": "Filter by Midjourney task ID",
|
||||||
"Filter by model name...": "Filter by model name...",
|
"Filter by model name...": "Filter by model name...",
|
||||||
"Filter by model...": "Filter by model...",
|
"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 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 name, ID, or key...": "Filter by name, ID, or key...",
|
||||||
"Filter by price field": "Filter by price field",
|
"Filter by price field": "Filter by price field",
|
||||||
"Filter by ratio type": "Filter by ratio type",
|
"Filter by ratio type": "Filter by ratio type",
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1715,8 +1715,9 @@
|
|||||||
"Filter by Midjourney task ID": "Filtrer par ID de tâche Midjourney",
|
"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 name...": "Filtrer par nom du modèle...",
|
||||||
"Filter by model...": "Filtrer par 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 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 name, ID, or key...": "Filtrer par nom, ID ou clé...",
|
||||||
"Filter by price field": "Filtrer par champ de prix",
|
"Filter by price field": "Filtrer par champ de prix",
|
||||||
"Filter by ratio type": "Filtrer par type de ratio",
|
"Filter by ratio type": "Filtrer par type de ratio",
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1715,8 +1715,9 @@
|
|||||||
"Filter by Midjourney task ID": "MidjourneyタスクIDでフィルター",
|
"Filter by Midjourney task ID": "MidjourneyタスクIDでフィルター",
|
||||||
"Filter by model name...": "モデル名でフィルター...",
|
"Filter by model name...": "モデル名でフィルター...",
|
||||||
"Filter by model...": "モデルでフィルタリング...",
|
"Filter by model...": "モデルでフィルタリング...",
|
||||||
|
"Filter by API key...": "APIキーでフィルター...",
|
||||||
"Filter by name or ID...": "名前またはIDでフィルター...",
|
"Filter by name or ID...": "名前またはIDでフィルター...",
|
||||||
"Filter by name or key...": "名前またはキーでフィルター...",
|
"Filter by name...": "名前でフィルター...",
|
||||||
"Filter by name, ID, or key...": "名前、ID、またはキーでフィルター...",
|
"Filter by name, ID, or key...": "名前、ID、またはキーでフィルター...",
|
||||||
"Filter by price field": "価格フィールドでフィルター",
|
"Filter by price field": "価格フィールドでフィルター",
|
||||||
"Filter by ratio type": "倍率タイプで絞り込み",
|
"Filter by ratio type": "倍率タイプで絞り込み",
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1715,8 +1715,9 @@
|
|||||||
"Filter by Midjourney task ID": "Фильтр по ID задачи Midjourney",
|
"Filter by Midjourney task ID": "Фильтр по ID задачи Midjourney",
|
||||||
"Filter by model name...": "Фильтр по имени модели...",
|
"Filter by model name...": "Фильтр по имени модели...",
|
||||||
"Filter by model...": "Фильтровать по модели...",
|
"Filter by model...": "Фильтровать по модели...",
|
||||||
|
"Filter by API key...": "Фильтр по API-ключу...",
|
||||||
"Filter by name or ID...": "Фильтр по имени или ID...",
|
"Filter by name or ID...": "Фильтр по имени или ID...",
|
||||||
"Filter by name or key...": "Фильтровать по имени или ключу...",
|
"Filter by name...": "Фильтр по имени...",
|
||||||
"Filter by name, ID, or key...": "Фильтровать по имени, ID или ключу...",
|
"Filter by name, ID, or key...": "Фильтровать по имени, ID или ключу...",
|
||||||
"Filter by price field": "Фильтр по полю цены",
|
"Filter by price field": "Фильтр по полю цены",
|
||||||
"Filter by ratio type": "Фильтровать по типу коэффициента",
|
"Filter by ratio type": "Фильтровать по типу коэффициента",
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1715,8 +1715,9 @@
|
|||||||
"Filter by Midjourney task ID": "Lọc theo ID nhiệm vụ Midjourney",
|
"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 name...": "Lọc theo tên mô hình...",
|
||||||
"Filter by model...": "Lọc theo mẫu...",
|
"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 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 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 price field": "Lọc theo trường giá",
|
||||||
"Filter by ratio type": "Lọc theo loại tỷ lệ",
|
"Filter by ratio type": "Lọc theo loại tỷ lệ",
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1715,8 +1715,9 @@
|
|||||||
"Filter by Midjourney task ID": "按 Midjourney 任务 ID 筛选",
|
"Filter by Midjourney task ID": "按 Midjourney 任务 ID 筛选",
|
||||||
"Filter by model name...": "按模型名称筛选...",
|
"Filter by model name...": "按模型名称筛选...",
|
||||||
"Filter by model...": "按模型筛选...",
|
"Filter by model...": "按模型筛选...",
|
||||||
|
"Filter by API key...": "按 API 密钥筛选...",
|
||||||
"Filter by name or ID...": "按名称或 ID 筛选...",
|
"Filter by name or ID...": "按名称或 ID 筛选...",
|
||||||
"Filter by name or key...": "按名称或密钥筛选...",
|
"Filter by name...": "按名称筛选...",
|
||||||
"Filter by name, ID, or key...": "按名称、ID 或密钥筛选...",
|
"Filter by name, ID, or key...": "按名称、ID 或密钥筛选...",
|
||||||
"Filter by price field": "按价格字段筛选",
|
"Filter by price field": "按价格字段筛选",
|
||||||
"Filter by ratio type": "按倍率类型筛选",
|
"Filter by ratio type": "按倍率类型筛选",
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const apiKeySearchSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.catch([]),
|
.catch([]),
|
||||||
filter: z.string().optional().catch(''),
|
filter: z.string().optional().catch(''),
|
||||||
|
token: z.string().optional().catch(''),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authenticated/keys/')({
|
export const Route = createFileRoute('/_authenticated/keys/')({
|
||||||
|
|||||||
Reference in New Issue
Block a user