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:
@@ -62,7 +62,8 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<div className='-ml-2'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
@@ -134,5 +135,6 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { Eye, Info, Pencil, Settings2, Timer, Trash2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatTimestampToDate } from '@/lib/format'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DataTableColumnHeader } from '@/components/data-table'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import { getDeploymentStatusConfig } from '../constants'
|
||||
@@ -45,10 +44,8 @@ export function useDeploymentsColumns(opts: {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
meta: { label: t('ID'), mobileHidden: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('ID')} />
|
||||
),
|
||||
header: t('ID'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const id = row.original.id
|
||||
return <TableId value={id} />
|
||||
@@ -59,10 +56,8 @@ export function useDeploymentsColumns(opts: {
|
||||
id: 'name',
|
||||
accessorFn: (row) =>
|
||||
row.container_name || row.deployment_name || row.name || '',
|
||||
meta: { label: t('Name'), mobileTitle: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Name')} />
|
||||
),
|
||||
header: t('Name'),
|
||||
meta: { mobileTitle: true },
|
||||
cell: ({ getValue }) => {
|
||||
const name = String(getValue() || '-') || '-'
|
||||
return (
|
||||
@@ -71,7 +66,7 @@ export function useDeploymentsColumns(opts: {
|
||||
variant='neutral'
|
||||
copyText={name}
|
||||
size='sm'
|
||||
className='font-mono'
|
||||
className='-ml-1.5 font-mono'
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -79,8 +74,8 @@ export function useDeploymentsColumns(opts: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
meta: { label: t('Status'), mobileBadge: true },
|
||||
header: t('Status'),
|
||||
meta: { mobileBadge: true },
|
||||
cell: ({ row }) => {
|
||||
const raw = row.original.status
|
||||
const key = normalizeDeploymentStatus(raw)
|
||||
@@ -95,6 +90,7 @@ export function useDeploymentsColumns(opts: {
|
||||
variant={config.variant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -114,7 +110,6 @@ export function useDeploymentsColumns(opts: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'provider',
|
||||
meta: { label: t('Provider') },
|
||||
header: t('Provider'),
|
||||
cell: ({ row }) => {
|
||||
const provider = row.original.provider
|
||||
@@ -126,6 +121,7 @@ export function useDeploymentsColumns(opts: {
|
||||
autoColor={String(provider)}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -134,7 +130,6 @@ export function useDeploymentsColumns(opts: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'time_remaining',
|
||||
meta: { label: t('Time remaining') },
|
||||
header: t('Time remaining'),
|
||||
cell: ({ row }) => {
|
||||
const status = normalizeDeploymentStatus(row.original.status)
|
||||
@@ -185,8 +180,8 @@ export function useDeploymentsColumns(opts: {
|
||||
},
|
||||
{
|
||||
id: 'hardware',
|
||||
meta: { label: t('Hardware'), mobileHidden: true },
|
||||
header: t('Hardware'),
|
||||
meta: { mobileHidden: true },
|
||||
accessorFn: (row) =>
|
||||
row.hardware_info || row.hardware_name || row.brand_name || '',
|
||||
cell: ({ row }) => {
|
||||
@@ -220,10 +215,8 @@ export function useDeploymentsColumns(opts: {
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
meta: { label: t('Created'), mobileHidden: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Created')} />
|
||||
),
|
||||
header: t('Created'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const ts =
|
||||
typeof row.original.created_at === 'number'
|
||||
@@ -241,6 +234,7 @@ export function useDeploymentsColumns(opts: {
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => t('Actions'),
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
@@ -252,7 +246,7 @@ export function useDeploymentsColumns(opts: {
|
||||
''
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='-ml-2.5 flex items-center gap-1'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
@@ -305,6 +299,7 @@ export function useDeploymentsColumns(opts: {
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
meta: { pinned: 'right' as const },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
+73
-193
@@ -27,10 +27,10 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { DataTableColumnHeader } from '@/components/data-table'
|
||||
import { BadgeListCell } from '@/components/data-table'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { ProviderBadge } from '@/components/provider-badge'
|
||||
import { StatusBadge, StatusBadgeList } from '@/components/status-badge'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { TableId } from '@/components/table-id'
|
||||
import {
|
||||
getModelStatusConfig,
|
||||
@@ -48,22 +48,6 @@ function getCompactModelIcon(iconKey: string) {
|
||||
return getLobeIcon(`${baseIconKey}.Avatar.type={'platform'}`, 20)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render limited items with "and X more" indicator
|
||||
*/
|
||||
function renderLimitedItems(
|
||||
items: React.ReactNode[],
|
||||
maxDisplay: number = 2
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<StatusBadgeList
|
||||
items={items}
|
||||
max={maxDisplay}
|
||||
renderItem={(item) => item}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate models columns configuration
|
||||
*/
|
||||
@@ -107,10 +91,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// ID column
|
||||
{
|
||||
accessorKey: 'id',
|
||||
meta: { label: t('ID'), mobileHidden: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title='ID' />
|
||||
),
|
||||
header: t('ID'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const id = row.getValue('id') as number
|
||||
return <TableId value={id} />
|
||||
@@ -121,8 +103,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Icon column
|
||||
{
|
||||
accessorKey: 'icon',
|
||||
meta: { label: t('Icon'), mobileHidden: true },
|
||||
header: t('Icon'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const iconKey =
|
||||
@@ -145,10 +127,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Model Name column
|
||||
{
|
||||
accessorKey: 'model_name',
|
||||
meta: { label: t('Model Name'), mobileTitle: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Model Name')} />
|
||||
),
|
||||
header: t('Model Name'),
|
||||
meta: { mobileTitle: true },
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue('model_name') as string
|
||||
return (
|
||||
@@ -157,7 +137,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
variant='neutral'
|
||||
copyText={name}
|
||||
size='sm'
|
||||
className='font-mono'
|
||||
className='-ml-1.5 font-mono'
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -167,10 +147,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Name Rule column
|
||||
{
|
||||
accessorKey: 'name_rule',
|
||||
meta: { label: t('Match Type') },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Match Type')} />
|
||||
),
|
||||
header: t('Match Type'),
|
||||
cell: ({ row }) => {
|
||||
const rule = row.getValue('name_rule') as 0 | 1 | 2 | 3
|
||||
const model = row.original
|
||||
@@ -193,6 +170,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
| 'info'
|
||||
}
|
||||
size='sm'
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -209,7 +187,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>{badge}</TooltipTrigger>
|
||||
<TooltipTrigger render={<div className='-ml-1.5' />}>{badge}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side='top'
|
||||
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
|
||||
@@ -230,8 +208,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Status column
|
||||
{
|
||||
accessorKey: 'status',
|
||||
meta: { label: t('Status'), mobileBadge: true },
|
||||
header: t('Status'),
|
||||
meta: { mobileBadge: true },
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as number
|
||||
const config =
|
||||
@@ -243,6 +221,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
variant={config.variant}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -260,7 +239,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Vendor column
|
||||
{
|
||||
accessorKey: 'vendor_id',
|
||||
meta: { label: t('Vendor') },
|
||||
header: t('Vendor'),
|
||||
cell: ({ row }) => {
|
||||
const vendorId = row.getValue('vendor_id') as number
|
||||
@@ -283,8 +261,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Description column
|
||||
{
|
||||
accessorKey: 'description',
|
||||
meta: { label: t('Description'), mobileHidden: true },
|
||||
header: t('Description'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue('description') as string
|
||||
const modelName = row.getValue('model_name') as string
|
||||
@@ -300,36 +278,17 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Tags column
|
||||
{
|
||||
accessorKey: 'tags',
|
||||
meta: { label: t('Tags'), mobileHidden: true },
|
||||
header: t('Tags'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const tags = row.getValue('tags') as string
|
||||
const tagArray = parseModelTags(tags)
|
||||
|
||||
if (tagArray.length === 0) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
const tagBadges = tagArray.map((tag, idx) => (
|
||||
<StatusBadge key={idx} label={tag} autoColor={tag} size='sm' />
|
||||
))
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedItems(tagBadges, 2)}
|
||||
</TooltipTrigger>
|
||||
{tagArray.length > 2 && (
|
||||
<TooltipContent
|
||||
side='top'
|
||||
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
|
||||
>
|
||||
<div className='flex flex-wrap gap-1'>{tagBadges}</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={tagArray.map((tag, idx) => (
|
||||
<StatusBadge key={idx} label={tag} autoColor={tag} size='sm' />
|
||||
))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 150,
|
||||
@@ -339,36 +298,17 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Endpoints column
|
||||
{
|
||||
accessorKey: 'endpoints',
|
||||
meta: { label: t('Endpoints'), mobileHidden: true },
|
||||
header: t('Endpoints'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const endpoints = row.getValue('endpoints') as string
|
||||
const endpointArray = formatEndpointsDisplay(endpoints)
|
||||
|
||||
if (endpointArray.length === 0) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
const endpointBadges = endpointArray.map((ep, idx) => (
|
||||
<StatusBadge key={idx} label={ep} autoColor={ep} size='sm' />
|
||||
))
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedItems(endpointBadges, 2)}
|
||||
</TooltipTrigger>
|
||||
{endpointArray.length > 2 && (
|
||||
<TooltipContent
|
||||
side='top'
|
||||
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
|
||||
>
|
||||
<div className='flex flex-wrap gap-1'>{endpointBadges}</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={endpointArray.map((ep, idx) => (
|
||||
<StatusBadge key={idx} label={ep} autoColor={ep} size='sm' />
|
||||
))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 150,
|
||||
@@ -378,8 +318,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Bound Channels column
|
||||
{
|
||||
accessorKey: 'bound_channels',
|
||||
meta: { label: t('Bound Channels'), mobileHidden: true },
|
||||
header: t('Bound Channels'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const channels = row.getValue('bound_channels') as Array<{
|
||||
id: number
|
||||
@@ -387,36 +327,17 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
type?: number
|
||||
status?: number
|
||||
}>
|
||||
|
||||
if (!channels || channels.length === 0) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
const channelBadges = channels.map((c, idx) => (
|
||||
<StatusBadge
|
||||
key={idx}
|
||||
label={`${c.name} (${c.type})`}
|
||||
autoColor={c.name}
|
||||
size='sm'
|
||||
/>
|
||||
))
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedItems(channelBadges, 2)}
|
||||
</TooltipTrigger>
|
||||
{channels.length > 2 && (
|
||||
<TooltipContent
|
||||
side='top'
|
||||
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
|
||||
>
|
||||
<div className='flex flex-wrap gap-1'>{channelBadges}</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={(channels ?? []).map((c, idx) => (
|
||||
<StatusBadge
|
||||
key={idx}
|
||||
label={`${c.name} (${c.type})`}
|
||||
autoColor={c.name}
|
||||
size='sm'
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 150,
|
||||
@@ -426,37 +347,16 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Enable Groups column
|
||||
{
|
||||
accessorKey: 'enable_groups',
|
||||
meta: { label: t('Enable Groups'), mobileHidden: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Enable Groups')} />
|
||||
),
|
||||
header: t('Enable Groups'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const groups = row.getValue('enable_groups') as string[]
|
||||
|
||||
if (!groups || groups.length === 0) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
const groupBadges = groups.map((g) => (
|
||||
<GroupBadge key={g} group={g} size='sm' />
|
||||
))
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedItems(groupBadges, 2)}
|
||||
</TooltipTrigger>
|
||||
{groups.length > 2 && (
|
||||
<TooltipContent
|
||||
side='top'
|
||||
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
|
||||
>
|
||||
<div className='flex flex-wrap gap-1'>{groupBadges}</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={(groups ?? []).map((g) => (
|
||||
<GroupBadge key={g} group={g} size='sm' />
|
||||
))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 150,
|
||||
@@ -466,50 +366,31 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Quota Types column
|
||||
{
|
||||
accessorKey: 'quota_types',
|
||||
meta: { label: t('Quota Types'), mobileHidden: true },
|
||||
header: t('Quota Types'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const quotaTypes = row.getValue('quota_types') as number[]
|
||||
|
||||
if (!quotaTypes || quotaTypes.length === 0) {
|
||||
return <span className='text-muted-foreground text-xs'>-</span>
|
||||
}
|
||||
|
||||
const quotaBadges = quotaTypes.map((qt, idx) => {
|
||||
const config = QUOTA_TYPE_CONFIG[qt]
|
||||
return (
|
||||
<StatusBadge
|
||||
key={idx}
|
||||
label={config?.label || String(qt)}
|
||||
variant={
|
||||
(config?.color === 'error' ? 'danger' : config?.color) as
|
||||
| 'neutral'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'info'
|
||||
}
|
||||
size='sm'
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedItems(quotaBadges, 2)}
|
||||
</TooltipTrigger>
|
||||
{quotaTypes.length > 2 && (
|
||||
<TooltipContent
|
||||
side='top'
|
||||
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
|
||||
>
|
||||
<div className='flex flex-wrap gap-1'>{quotaBadges}</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<BadgeListCell
|
||||
items={(quotaTypes ?? []).map((qt, idx) => {
|
||||
const config = QUOTA_TYPE_CONFIG[qt]
|
||||
return (
|
||||
<StatusBadge
|
||||
key={idx}
|
||||
label={config?.label || String(qt)}
|
||||
variant={
|
||||
(config?.color === 'error' ? 'danger' : config?.color) as
|
||||
| 'neutral'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'danger'
|
||||
| 'info'
|
||||
}
|
||||
size='sm'
|
||||
/>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 150,
|
||||
@@ -519,8 +400,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Sync Official column
|
||||
{
|
||||
accessorKey: 'sync_official',
|
||||
meta: { label: t('Official Sync'), mobileHidden: true },
|
||||
header: t('Official Sync'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const syncOfficial = row.getValue('sync_official') as number
|
||||
return (
|
||||
@@ -529,6 +410,7 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
variant={syncOfficial === 1 ? 'success' : 'warning'}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -546,10 +428,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Created Time column
|
||||
{
|
||||
accessorKey: 'created_time',
|
||||
meta: { label: t('Created'), mobileHidden: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Created')} />
|
||||
),
|
||||
header: t('Created'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const timestamp = row.getValue('created_time') as number
|
||||
return (
|
||||
@@ -564,10 +444,8 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Updated Time column
|
||||
{
|
||||
accessorKey: 'updated_time',
|
||||
meta: { label: t('Updated'), mobileHidden: true },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Updated')} />
|
||||
),
|
||||
header: t('Updated'),
|
||||
meta: { mobileHidden: true },
|
||||
cell: ({ row }) => {
|
||||
const timestamp = row.getValue('updated_time') as number
|
||||
return (
|
||||
@@ -582,12 +460,14 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
|
||||
// Actions column
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => t('Actions'),
|
||||
cell: ({ row }) => {
|
||||
return <DataTableRowActions row={row} />
|
||||
},
|
||||
size: 100,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
meta: { pinned: 'right' as const },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user