/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useEffect, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { getRouteApi } from '@tanstack/react-router' import { type SortingState, type VisibilityState, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table' import { Database } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { formatQuota } from '@/lib/format' import { cn } from '@/lib/utils' import { useTableUrlState } from '@/hooks/use-table-url-state' import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/components/ui/empty' import { Skeleton } from '@/components/ui/skeleton' import { DISABLED_ROW_DESKTOP, DISABLED_ROW_MOBILE, DataTablePage, } from '@/components/data-table' import { StatusBadge } from '@/components/status-badge' import { getApiKeys, searchApiKeys } from '../api' import { API_KEY_STATUS, API_KEY_STATUS_OPTIONS, API_KEY_STATUSES, ERROR_MESSAGES, } from '../constants' import { type ApiKey } from '../types' import { ApiKeyCell } from './api-keys-cells' import { useApiKeysColumns } from './api-keys-columns' import { useApiKeys } from './api-keys-provider' import { DataTableBulkActions } from './data-table-bulk-actions' import { DataTableRowActions } from './data-table-row-actions' const route = getRouteApi('/_authenticated/keys/') function isDisabledApiKeyRow(apiKey: ApiKey) { return apiKey.status !== API_KEY_STATUS.ENABLED } function ApiKeysMobileSkeleton() { return (
{Array.from({ length: 5 }).map((_, index) => (
))}
) } function ApiKeysMobileList({ table, isLoading, }: { table: ReturnType> isLoading: boolean }) { const { t } = useTranslation() const rows = table.getRowModel().rows if (isLoading) return if (!rows.length) { return (
{t('No API Keys Found')} {t( 'No API keys available. Create your first API key to get started.' )}
) } return (
{rows.map((row) => { const apiKey = row.original const statusConfig = API_KEY_STATUSES[apiKey.status] const total = apiKey.used_quota + apiKey.remain_quota return (
{apiKey.name}
{t('API Key')}
{statusConfig && ( )}
{t('Quota')} {apiKey.unlimited_quota ? ( {t('Unlimited')} ) : ( {formatQuota(apiKey.remain_quota)} {' / '} {formatQuota(total)} )}
) })}
) } export function ApiKeysTable() { const { t } = useTranslation() const { refreshTrigger } = useApiKeys() const columns = useApiKeysColumns() const [rowSelection, setRowSelection] = useState({}) const [sorting, setSorting] = useState([]) const [columnVisibility, setColumnVisibility] = useState({}) const { globalFilter, onGlobalFilterChange, columnFilters, onColumnFiltersChange, pagination, onPaginationChange, ensurePageInRange, } = useTableUrlState({ search: route.useSearch(), navigate: route.useNavigate(), pagination: { defaultPage: 1, defaultPageSize: 20 }, globalFilter: { enabled: true, key: 'filter' }, columnFilters: [{ columnId: 'status', searchKey: 'status', type: 'array' }], }) // Fetch data with React Query // eslint-disable-next-line @tanstack/query/exhaustive-deps const { data, isLoading, isFetching } = useQuery({ queryKey: [ 'keys', pagination.pageIndex + 1, pagination.pageSize, globalFilter, 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, }) if (!result.success) { toast.error(result.message || t(ERROR_MESSAGES.LOAD_FAILED)) return { items: [], total: 0 } } return { items: result.data?.items || [], total: result.data?.total || 0, } }, placeholderData: (previousData) => previousData, }) const apiKeys = data?.items || [] const table = useReactTable({ data: apiKeys, columns, state: { sorting, columnVisibility, rowSelection, columnFilters, globalFilter, pagination, }, enableRowSelection: true, 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) }, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), onPaginationChange, onGlobalFilterChange, onColumnFiltersChange, manualPagination: !globalFilter, pageCount: globalFilter ? Math.ceil((data?.total || 0) / pagination.pageSize) : Math.ceil((data?.total || 0) / pagination.pageSize), }) const pageCount = table.getPageCount() useEffect(() => { ensurePageInRange(pageCount) }, [pageCount, ensurePageInRange]) return ( } getRowClassName={(row) => isDisabledApiKeyRow(row.original) ? DISABLED_ROW_DESKTOP : undefined } bulkActions={} /> ) }