axonhub / frontend /src /features /threads /components /threads-table.tsx
llzai's picture
Upload 1793 files
9853396 verified
import { useState } from 'react';
import {
ColumnFiltersState,
RowData,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { motion, AnimatePresence } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { useAnimatedList } from '@/hooks/useAnimatedList';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { TableSkeleton } from '@/components/ui/table-skeleton';
import { ServerSidePagination } from '@/components/server-side-pagination';
import type { DateTimeRangeValue } from '@/utils/date-range';
import { Thread, ThreadConnection } from '../data/schema';
import { ThreadsTableToolbar } from './data-table-toolbar';
import { useThreadsColumns } from './threads-columns';
const MotionTableRow = motion(TableRow);
declare module '@tanstack/react-table' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ColumnMeta<TData extends RowData, TValue> {
className: string;
}
}
interface ThreadsTableProps {
data: Thread[];
loading?: boolean;
pageInfo?: ThreadConnection['pageInfo'];
pageSize: number;
totalCount?: number;
dateRange?: DateTimeRangeValue;
threadIdFilter: string;
onNextPage: () => void;
onPreviousPage: () => void;
onPageSizeChange: (pageSize: number) => void;
onDateRangeChange: (range: DateTimeRangeValue | undefined) => void;
onThreadIdFilterChange: (threadId: string) => void;
onRefresh: () => void;
showRefresh: boolean;
autoRefresh?: boolean;
onAutoRefreshChange?: (enabled: boolean) => void;
}
export function ThreadsTable({
data,
loading,
pageInfo,
totalCount,
pageSize,
dateRange,
threadIdFilter,
onNextPage,
onPreviousPage,
onPageSizeChange,
onDateRangeChange,
onThreadIdFilterChange,
onRefresh,
showRefresh,
autoRefresh = false,
onAutoRefreshChange,
}: ThreadsTableProps) {
const { t } = useTranslation();
const threadsColumns = useThreadsColumns();
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [rowSelection, setRowSelection] = useState({});
const displayedData = useAnimatedList(data, autoRefresh);
const table = useReactTable({
data: displayedData,
getRowId: (row) => row.id,
columns: threadsColumns,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onColumnVisibilityChange: setColumnVisibility,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
manualPagination: true,
manualFiltering: true,
});
return (
<div className='flex flex-1 flex-col overflow-hidden'>
<ThreadsTableToolbar
table={table}
dateRange={dateRange}
onDateRangeChange={onDateRangeChange}
threadIdFilter={threadIdFilter}
onThreadIdFilterChange={onThreadIdFilterChange}
onRefresh={onRefresh}
showRefresh={showRefresh}
autoRefresh={autoRefresh}
onAutoRefreshChange={onAutoRefreshChange}
/>
<div className='shadow-soft relative mt-4 flex-1 overflow-auto overflow-x-hidden rounded-2xl border border-[var(--table-border)]'>
<Table data-testid='threads-table' className='border-separate border-spacing-0 rounded-2xl bg-[var(--table-background)]'>
<TableHeader className='sticky top-0 z-20 bg-[var(--table-header)] shadow-sm'>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className='group/row border-0'>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
colSpan={header.colSpan}
className={`${header.column.columnDef.meta?.className ?? ''} text-muted-foreground border-0 text-xs font-semibold tracking-wider uppercase`}
>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody className='space-y-1 !bg-[var(--table-background)] p-2'>
{loading ? (
<TableSkeleton rows={pageSize} columns={threadsColumns.length} />
) : table.getRowModel().rows?.length ? (
<AnimatePresence initial={false} mode='popLayout'>
{table.getRowModel().rows.map((row) => (
<MotionTableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
initial={{ opacity: 0, y: -20, height: 0 }}
animate={{ opacity: 1, y: 0, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{
type: 'spring',
stiffness: 500,
damping: 30,
mass: 1,
opacity: { duration: 0.2 },
}}
layout
className='group/row hover:bg-muted/50 data-[state=selected]:bg-muted'
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`${cell.column.columnDef.meta?.className ?? ''} border-b border-[var(--table-border)] py-3 group-last/row:border-0`}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</MotionTableRow>
))}
</AnimatePresence>
) : (
<TableRow className='!bg-[var(--table-background)]'>
<TableCell colSpan={threadsColumns.length} className='h-24 !bg-[var(--table-background)] text-center'>
{t('common.noData')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className='mt-4 flex-shrink-0'>
<ServerSidePagination
pageInfo={pageInfo}
pageSize={pageSize}
dataLength={data.length}
totalCount={totalCount}
selectedRows={table.getFilteredSelectedRowModel().rows.length}
onNextPage={onNextPage}
onPreviousPage={onPreviousPage}
onPageSizeChange={onPageSizeChange}
/>
</div>
</div>
);
}