perf(data-table): improve data table layout and badge display (#5460)
* perf(table): use percentage-based column widths
- compute each column's width as a percentage of total column size instead of a fixed pixel value, letting the colgroup scale fluidly with the table container.
* perf(data-table): reduce unnecessary re-renders across table components
- stabilize commitSearchValue in toolbar by reading table/searchKey via refs, eliminating recreation on every parent render
- store onColumnFiltersChange in a ref so debounce effect is not reset when the caller passes a new function reference each render
- wrap DataTableRow in React.memo with a custom comparator that ignores getColumnClassName reference churn
- memoize selectedValues Set in DataTableFacetedFilter and wrap with React.memo to prevent rerenders on unrelated state changes
- cache cell meta reads in CompactRow and FallbackRow to a single pass per row; memoize hasCompactMeta in MobileCardList
- memoize hideable columns list in DataTableViewOptions and colSpan in DataTableView
- remove tableClassName and colgroup from scroll-sync effect deps; cache toolbar button NodeList via useLayoutEffect to avoid per-keydown DOM queries
* perf(data-table): replace scroll-sync split header with CSS sticky
- remove JS scroll-sync effect and event listener between split header and body containers.
- merge separate header/body tables into a single scrollable table element, reducing DOM complexity.
- apply CSS sticky positioning to the header for a simpler, hardware-accelerated freeze effect.
* fix(data-table): replace opacity muted colors with color-mix
- switch from bg-muted/50 and bg-muted/30 to color-mix(in oklch) to produce opaque blended backgrounds that prevent scroll content from showing through pinned cells.
- expose --table-header-bg CSS variable so pinned header cells inherit the exact same computed color as the thead background.
- add group class to TableRow to enable group-hover selectors on pinned cell styles.
* feat(data-table): support column pinning via meta.pinned
- add pinned?: 'left' | 'right' to ColumnMeta so pinning is declared once in the column definition and applies to both header and body automatically
- DataTableView derives pinnedColumns from meta.pinned at runtime, merged with any explicit pinnedColumns prop; explicit entries take precedence
- add header and meta.pinned: 'right' to all actions columns across channels, users, api-keys, redemption-codes, models, deployments, and subscriptions tables
* style(row-actions): align action buttons to leading edge of column
* refactor(data-table): extract BadgeListCell and centralize badge alignment
- add BadgeListCell component to data-table for badge lists with overflow tooltip, replacing duplicated renderLimitedItems helpers in channels, models, and pricing columns
- move StatusBadge -ml-1.5 alignment into the component itself via a table-cell context selector, so callers no longer need manual offset wrappers
- remove the table-cell-level -ml-1.5 selector from TableCell now that alignment is handled by StatusBadge directly
* style(row-actions): offset action buttons to align with column header text
* refactor(data-table): consolidate mobile meta into ColumnMeta declaration
- move mobileTitle, mobileBadge, mobileHidden into the global ColumnMeta augmentation so the type is shared across the project
- remove the local MobileColumnMeta interface and getCellMeta helper from mobile-card-list.tsx
- direct col.columnDef.meta access is now type-safe without explicit casting
* refactor(data-table): simplify column header and meta config
- auto-render string `header` values via DataTableColumnHeader so sortable/non-sortable columns work without boilerplate function wrappers
- promote mobile layout hints (mobileTitle, mobileBadge, mobileHidden) into the global ColumnMeta type, removing the local MobileColumnMeta cast in mobile-card-list
- migrate all column files from `meta: { label }` to top-level `header: t('...')`, cutting ~180 lines of repetitive template code
- ViewOptions and MobileCardList label resolution now reads string header first, then meta.label as fallback
* feat(status-badge): add text and underline display types
- introduce StatusBadgeType ('badge' | 'text' | 'underline') and StatusBadgeTypeContext so ancestors can override rendering without touching call sites
- mobile card field rows now use the text type via context, showing badges as plain colored text instead of pills
- ProviderBadge gains data-slot='provider-badge' to enable targeted CSS resets in compact layouts
- replace the implicit [[data-slot=table-cell]>&]:-ml-1.5 rule with explicit -ml-1.5 at each column call site
* refactor(data-table): simplify table filtering internals
- derive toolbar search state from the active table filter to avoid render-time ref writes.
- extract faceted filter selection updates into a pure helper for clearer single and multi-select behavior.
- split pinned column resolution into focused helpers so explicit and meta pins merge predictably.
Co-authored-by: t0ng7u <dev@aiass.cc>
This commit is contained in:
+31
-96
@@ -19,15 +19,9 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import { type ColumnDef } from '@tanstack/react-table'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getLobeIcon } from '@/lib/lobe-icon'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { DataTableColumnHeader } from '@/components/data-table'
|
||||
import { DataTableColumnHeader, BadgeListCell } from '@/components/data-table'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { StatusBadge, StatusBadgeList } from '@/components/status-badge'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
|
||||
import {
|
||||
getDynamicDisplayGroupRatio,
|
||||
@@ -53,36 +47,6 @@ export interface PricingColumnsOptions {
|
||||
showRechargePrice?: boolean
|
||||
}
|
||||
|
||||
function renderLimitedTags(
|
||||
items: string[],
|
||||
maxDisplay: number = 3
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<StatusBadgeList
|
||||
items={items}
|
||||
max={maxDisplay}
|
||||
getKey={(item) => item}
|
||||
renderItem={(item) => (
|
||||
<StatusBadge label={item} autoColor={item} size='sm' copyable={false} />
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderLimitedGroupBadges(
|
||||
groups: string[],
|
||||
maxDisplay: number = 2
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<StatusBadgeList
|
||||
items={groups}
|
||||
max={maxDisplay}
|
||||
getKey={(group) => group}
|
||||
renderItem={(group) => <GroupBadge group={group} size='sm' />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function usePricingColumns(
|
||||
options: PricingColumnsOptions = {}
|
||||
): ColumnDef<PricingModel>[] {
|
||||
@@ -124,7 +88,6 @@ export function usePricingColumns(
|
||||
// Type column
|
||||
{
|
||||
accessorKey: 'quota_type',
|
||||
meta: { label: t('Type') },
|
||||
header: t('Type'),
|
||||
cell: ({ row }) => {
|
||||
const isTokenBased = row.original.quota_type === QUOTA_TYPE_VALUES.TOKEN
|
||||
@@ -133,6 +96,7 @@ export function usePricingColumns(
|
||||
label={isTokenBased ? t('Token') : t('Request')}
|
||||
variant={isTokenBased ? 'info' : 'neutral'}
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -269,7 +233,6 @@ export function usePricingColumns(
|
||||
// Cached price column (Vercel AI Gateway style)
|
||||
{
|
||||
id: 'cached_price',
|
||||
meta: { label: t('Cached') },
|
||||
header: t('Cached'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
@@ -344,7 +307,6 @@ export function usePricingColumns(
|
||||
// Vendor column
|
||||
{
|
||||
accessorKey: 'vendor_name',
|
||||
meta: { label: t('Vendor') },
|
||||
header: t('Vendor'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
@@ -373,27 +335,21 @@ export function usePricingColumns(
|
||||
// Tags column
|
||||
{
|
||||
accessorKey: 'tags',
|
||||
meta: { label: t('Tags') },
|
||||
header: t('Tags'),
|
||||
cell: ({ row }) => {
|
||||
const tags = parseTags(row.original.tags)
|
||||
if (tags.length === 0) {
|
||||
return <span className='text-muted-foreground/50 text-xs'>—</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedTags(tags, 2)}
|
||||
</TooltipTrigger>
|
||||
{tags.length > 2 && (
|
||||
<TooltipContent side='top' className='max-w-[280px] p-2'>
|
||||
<span className='text-xs'>{tags.join(', ')}</span>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={tags.map((tag) => (
|
||||
<StatusBadge
|
||||
key={tag}
|
||||
label={tag}
|
||||
autoColor={tag}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 140,
|
||||
@@ -403,27 +359,21 @@ export function usePricingColumns(
|
||||
// Endpoints column
|
||||
{
|
||||
accessorKey: 'supported_endpoint_types',
|
||||
meta: { label: t('Endpoints') },
|
||||
header: t('Endpoints'),
|
||||
cell: ({ row }) => {
|
||||
const endpoints = row.original.supported_endpoint_types || []
|
||||
if (endpoints.length === 0) {
|
||||
return <span className='text-muted-foreground/50 text-xs'>—</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedTags(endpoints, 2)}
|
||||
</TooltipTrigger>
|
||||
{endpoints.length > 2 && (
|
||||
<TooltipContent side='top' className='max-w-[280px] p-2'>
|
||||
<span className='text-xs'>{endpoints.join(', ')}</span>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={endpoints.map((ep) => (
|
||||
<StatusBadge
|
||||
key={ep}
|
||||
label={ep}
|
||||
autoColor={ep}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 130,
|
||||
@@ -433,31 +383,16 @@ export function usePricingColumns(
|
||||
// Enable Groups column
|
||||
{
|
||||
accessorKey: 'enable_groups',
|
||||
meta: { label: t('Groups') },
|
||||
header: t('Groups'),
|
||||
cell: ({ row }) => {
|
||||
const groups = row.original.enable_groups || []
|
||||
if (groups.length === 0) {
|
||||
return <span className='text-muted-foreground/50 text-xs'>—</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedGroupBadges(groups, 2)}
|
||||
</TooltipTrigger>
|
||||
{groups.length > 2 && (
|
||||
<TooltipContent side='top' className='max-w-[280px] p-2'>
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{groups.map((group) => (
|
||||
<GroupBadge key={group} group={group} size='sm' />
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={groups.map((group) => (
|
||||
<GroupBadge key={group} group={group} size='sm' />
|
||||
))}
|
||||
tooltipClassName='max-w-[280px] p-2'
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 130,
|
||||
|
||||
Reference in New Issue
Block a user