| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- import { useState, useMemo, useEffect } from 'react'
- import { useQuery } from '@tanstack/react-query'
- import { getRouteApi } from '@tanstack/react-router'
- import {
- flexRender,
- getCoreRowModel,
- useReactTable,
- type SortingState,
- type VisibilityState,
- } from '@tanstack/react-table'
- import { useMediaQuery } from '@/hooks'
- import { useTranslation } from 'react-i18next'
- import { cn } from '@/lib/utils'
- import { useTableUrlState } from '@/hooks/use-table-url-state'
- import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- } from '@/components/ui/table'
- import {
- DataTableToolbar,
- TableSkeleton,
- TableEmpty,
- MobileCardList,
- } from '@/components/data-table'
- import { DataTablePagination } from '@/components/data-table/pagination'
- import { PageFooterPortal } from '@/components/layout'
- import { getModels, searchModels, getVendors } from '../api'
- import {
- DEFAULT_PAGE_SIZE,
- getModelStatusOptions,
- getSyncStatusOptions,
- } from '../constants'
- import { modelsQueryKeys, vendorsQueryKeys } from '../lib'
- import { DataTableBulkActions } from './data-table-bulk-actions'
- import { useModelsColumns } from './models-columns'
- import { useModels } from './models-provider'
- const route = getRouteApi('/_authenticated/models/$section')
- export function ModelsTable() {
- const { t } = useTranslation()
- const { selectedVendor } = useModels()
- const isMobile = useMediaQuery('(max-width: 640px)')
- // Table state
- const [sorting, setSorting] = useState<SortingState>([])
- const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
- description: false,
- bound_channels: false,
- quota_types: false,
- })
- const [rowSelection, setRowSelection] = useState({})
- // URL state management
- const {
- globalFilter,
- onGlobalFilterChange,
- columnFilters,
- onColumnFiltersChange,
- pagination,
- onPaginationChange,
- ensurePageInRange,
- } = useTableUrlState({
- search: route.useSearch(),
- navigate: route.useNavigate(),
- pagination: {
- defaultPage: 1,
- defaultPageSize: isMobile ? 10 : DEFAULT_PAGE_SIZE,
- },
- globalFilter: { enabled: true, key: 'filter' },
- columnFilters: [
- { columnId: 'status', searchKey: 'status', type: 'array' },
- { columnId: 'vendor_id', searchKey: 'vendor', type: 'array' },
- { columnId: 'sync_official', searchKey: 'sync', type: 'array' },
- ],
- })
- // Extract filters from column filters
- const statusFilter =
- (columnFilters.find((f) => f.id === 'status')?.value as string[]) || []
- const vendorFilter =
- (columnFilters.find((f) => f.id === 'vendor_id')?.value as string[]) || []
- const syncFilter =
- (columnFilters.find((f) => f.id === 'sync_official')?.value as string[]) ||
- []
- // Fetch vendors for filter
- const { data: vendorsData } = useQuery({
- queryKey: vendorsQueryKeys.list(),
- queryFn: () => getVendors({ page_size: 1000 }),
- })
- const vendors = useMemo(
- () => vendorsData?.data?.items || [],
- [vendorsData?.data?.items]
- )
- const vendorOptions = useMemo(() => {
- return vendors.map((v) => ({
- label: v.name,
- value: String(v.id),
- }))
- }, [vendors])
- // Determine whether to use search or regular list API
- const shouldSearch = Boolean(globalFilter?.trim())
- // Apply selected vendor from context or filter
- const activeVendorFilter =
- selectedVendor ||
- (vendorFilter.length > 0 && !vendorFilter.includes('all')
- ? vendorFilter[0]
- : undefined)
- // Fetch models data
- // eslint-disable-next-line @tanstack/query/exhaustive-deps
- const { data, isLoading, isFetching } = useQuery({
- queryKey: modelsQueryKeys.list({
- keyword: globalFilter,
- vendor: activeVendorFilter,
- status:
- statusFilter.length > 0 && !statusFilter.includes('all')
- ? statusFilter[0]
- : undefined,
- sync_official:
- syncFilter.length > 0 && !syncFilter.includes('all')
- ? syncFilter[0]
- : undefined,
- p: pagination.pageIndex + 1,
- page_size: pagination.pageSize,
- }),
- queryFn: async () => {
- if (shouldSearch || activeVendorFilter) {
- return searchModels({
- keyword: globalFilter,
- vendor: activeVendorFilter,
- status:
- statusFilter.length > 0 && !statusFilter.includes('all')
- ? statusFilter[0]
- : undefined,
- sync_official:
- syncFilter.length > 0 && !syncFilter.includes('all')
- ? syncFilter[0]
- : undefined,
- p: pagination.pageIndex + 1,
- page_size: pagination.pageSize,
- })
- } else {
- return getModels({
- status:
- statusFilter.length > 0 && !statusFilter.includes('all')
- ? statusFilter[0]
- : undefined,
- sync_official:
- syncFilter.length > 0 && !syncFilter.includes('all')
- ? syncFilter[0]
- : undefined,
- p: pagination.pageIndex + 1,
- page_size: pagination.pageSize,
- })
- }
- },
- placeholderData: (previousData) => previousData,
- })
- const models = data?.data?.items || []
- const totalCount = data?.data?.total || 0
- const vendorCounts = data?.data?.vendor_counts
- // Columns configuration
- const columns = useModelsColumns(vendors)
- // React Table instance
- const table = useReactTable({
- data: models,
- columns,
- pageCount: Math.ceil(totalCount / pagination.pageSize),
- state: {
- sorting,
- columnFilters,
- columnVisibility,
- rowSelection,
- pagination,
- globalFilter,
- },
- enableRowSelection: true,
- onRowSelectionChange: setRowSelection,
- onSortingChange: setSorting,
- onColumnFiltersChange,
- onColumnVisibilityChange: setColumnVisibility,
- onPaginationChange,
- onGlobalFilterChange,
- getCoreRowModel: getCoreRowModel(),
- manualPagination: true,
- manualSorting: true,
- manualFiltering: true,
- })
- // Ensure page is in range when total count changes
- const pageCount = table.getPageCount()
- useEffect(() => {
- ensurePageInRange(pageCount)
- }, [pageCount, ensurePageInRange])
- // Prepare filter options
- const vendorFilterOptions = [
- {
- label: `${t('All Vendors')}${vendorCounts?.all ? ` (${vendorCounts.all})` : ''}`,
- value: 'all',
- },
- ...vendorOptions.map((option) => ({
- label: `${option.label}${vendorCounts?.[option.value] ? ` (${vendorCounts[option.value]})` : ''}`,
- value: option.value,
- })),
- ]
- return (
- <>
- <div className='space-y-3 sm:space-y-4'>
- <DataTableToolbar
- table={table}
- searchPlaceholder={t('Filter by model name...')}
- filters={[
- {
- columnId: 'status',
- title: t('Status'),
- options: [...getModelStatusOptions(t)],
- singleSelect: true,
- },
- {
- columnId: 'vendor_id',
- title: t('Vendor'),
- options: vendorFilterOptions,
- singleSelect: true,
- },
- {
- columnId: 'sync_official',
- title: t('Official Sync'),
- options: [...getSyncStatusOptions(t)],
- singleSelect: true,
- },
- ]}
- />
- {isMobile ? (
- <MobileCardList
- table={table}
- isLoading={isLoading}
- emptyTitle={t('No Models Found')}
- emptyDescription={t(
- 'No models available. Create your first model to get started.'
- )}
- />
- ) : (
- <>
- <div
- className={cn(
- 'overflow-hidden rounded-md border transition-opacity duration-150',
- isFetching && !isLoading && 'pointer-events-none opacity-50'
- )}
- >
- <Table>
- <TableHeader>
- {table.getHeaderGroups().map((headerGroup) => (
- <TableRow key={headerGroup.id}>
- {headerGroup.headers.map((header) => (
- <TableHead
- key={header.id}
- style={{ width: header.getSize() }}
- >
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
- </TableHead>
- ))}
- </TableRow>
- ))}
- </TableHeader>
- <TableBody>
- {isLoading ? (
- <TableSkeleton table={table} keyPrefix='model-skeleton' />
- ) : table.getRowModel().rows.length === 0 ? (
- <TableEmpty
- colSpan={columns.length}
- title={t('No Models Found')}
- description={t(
- 'No models available. Create your first model to get started.'
- )}
- />
- ) : (
- table.getRowModel().rows.map((row) => (
- <TableRow
- key={row.id}
- data-state={row.getIsSelected() && 'selected'}
- >
- {row.getVisibleCells().map((cell) => (
- <TableCell key={cell.id}>
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext()
- )}
- </TableCell>
- ))}
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </div>
- <DataTableBulkActions table={table} />
- </>
- )}
- </div>
- <PageFooterPortal>
- <DataTablePagination
- table={table as ReturnType<typeof useReactTable>}
- />
- </PageFooterPortal>
- </>
- )
- }
|