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:
+9
-36
@@ -35,7 +35,6 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { DataTableColumnHeader } from '@/components/data-table'
|
||||
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
|
||||
import { LOG_TYPE_ALL_VALUE } from '../../constants'
|
||||
import type { UsageLog } from '../../data/schema'
|
||||
@@ -265,9 +264,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
const columns: ColumnDef<UsageLog>[] = [
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Time')} />
|
||||
),
|
||||
header: t('Time'),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
const timestamp = row.getValue('created_at') as number
|
||||
@@ -295,7 +292,6 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
},
|
||||
enableHiding: false,
|
||||
size: 180,
|
||||
meta: { label: t('Time') },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -303,10 +299,8 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
columns.push(
|
||||
{
|
||||
id: 'channel',
|
||||
header: t('Channel'),
|
||||
accessorFn: (row) => row.channel,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Channel')} />
|
||||
),
|
||||
cell: function ChannelCell({ row }) {
|
||||
const { sensitiveVisible, setAffinityTarget, setAffinityDialogOpen } =
|
||||
useUsageLogsContext()
|
||||
@@ -404,14 +398,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</TooltipProvider>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Channel') },
|
||||
},
|
||||
{
|
||||
id: 'user',
|
||||
header: t('User'),
|
||||
accessorFn: (row) => row.username,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('User')} />
|
||||
),
|
||||
cell: function UserCell({ row }) {
|
||||
const { sensitiveVisible, setSelectedUserId, setUserInfoDialogOpen } =
|
||||
useUsageLogsContext()
|
||||
@@ -461,16 +452,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</button>
|
||||
)
|
||||
},
|
||||
meta: { label: t('User') },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
columns.push({
|
||||
accessorKey: 'token_name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Token')} />
|
||||
),
|
||||
header: t('Token'),
|
||||
cell: function TokenNameCell({ row }) {
|
||||
const { sensitiveVisible } = useUsageLogsContext()
|
||||
const log = row.original
|
||||
@@ -520,16 +508,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Token') },
|
||||
size: 160,
|
||||
})
|
||||
|
||||
columns.push(
|
||||
{
|
||||
accessorKey: 'model_name',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Model')} />
|
||||
),
|
||||
header: t('Model'),
|
||||
cell: function ModelCell({ row }) {
|
||||
const log = row.original
|
||||
if (!isDisplayableLogType(log.type)) return null
|
||||
@@ -545,14 +529,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Model'), mobileTitle: true },
|
||||
meta: { mobileTitle: true },
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'use_time',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Timing')} />
|
||||
),
|
||||
header: t('Timing'),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
if (!isTimingLogType(log.type)) return null
|
||||
@@ -656,14 +637,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Timing') },
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'prompt_tokens',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title='Tokens' />
|
||||
),
|
||||
header: 'Tokens',
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
if (!isDisplayableLogType(log.type)) return null
|
||||
@@ -707,14 +685,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: 'Tokens' },
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: 'quota',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Cost')} />
|
||||
),
|
||||
header: t('Cost'),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
if (!isDisplayableLogType(log.type)) return null
|
||||
@@ -762,7 +737,6 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Cost') },
|
||||
},
|
||||
|
||||
{
|
||||
@@ -820,7 +794,6 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Details') },
|
||||
size: 180,
|
||||
maxSize: 200,
|
||||
}
|
||||
|
||||
+9
-25
@@ -38,7 +38,6 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatTimestampToDate } from '@/lib/format'
|
||||
import { DataTableColumnHeader } from '@/components/data-table'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { MJ_TASK_TYPES } from '../../constants'
|
||||
import {
|
||||
@@ -87,9 +86,7 @@ export function useDrawingLogsColumns(
|
||||
const columns: ColumnDef<MidjourneyLog>[] = [
|
||||
{
|
||||
accessorKey: 'submit_time',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Submit Time')} />
|
||||
),
|
||||
header: t('Submit Time'),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
const submitTime = row.getValue('submit_time') as number
|
||||
@@ -109,7 +106,6 @@ export function useDrawingLogsColumns(
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
meta: { label: t('Submit Time') },
|
||||
},
|
||||
]
|
||||
|
||||
@@ -121,9 +117,7 @@ export function useDrawingLogsColumns(
|
||||
|
||||
columns.push({
|
||||
accessorKey: 'action',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Type')} />
|
||||
),
|
||||
header: t('Type'),
|
||||
cell: ({ row }) => {
|
||||
const action = row.getValue('action') as string
|
||||
return (
|
||||
@@ -133,17 +127,15 @@ export function useDrawingLogsColumns(
|
||||
icon={getDrawingTypeIcon(action)}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Type') },
|
||||
})
|
||||
|
||||
columns.push({
|
||||
accessorKey: 'mj_id',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Task ID')} />
|
||||
),
|
||||
header: t('Task ID'),
|
||||
cell: ({ row }) => {
|
||||
const mjId = row.getValue('mj_id') as string
|
||||
|
||||
@@ -162,7 +154,7 @@ export function useDrawingLogsColumns(
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Task ID'), mobileTitle: true },
|
||||
meta: { mobileTitle: true },
|
||||
})
|
||||
|
||||
columns.push(
|
||||
@@ -176,9 +168,7 @@ export function useDrawingLogsColumns(
|
||||
if (isAdmin) {
|
||||
columns.push({
|
||||
accessorKey: 'code',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Submit Result')} />
|
||||
),
|
||||
header: t('Submit Result'),
|
||||
cell: ({ row }) => {
|
||||
const code = row.getValue('code') as number
|
||||
|
||||
@@ -188,10 +178,10 @@ export function useDrawingLogsColumns(
|
||||
variant={mjSubmitResultMapper.getVariant(String(code))}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Submit Result') },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -199,9 +189,7 @@ export function useDrawingLogsColumns(
|
||||
createProgressColumn<MidjourneyLog>({ headerLabel: t('Progress') }),
|
||||
{
|
||||
accessorKey: 'image_url',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Image')} />
|
||||
),
|
||||
header: t('Image'),
|
||||
cell: function ImageCell({ row }) {
|
||||
const log = row.original
|
||||
const imageUrl = row.getValue('image_url') as string
|
||||
@@ -232,13 +220,10 @@ export function useDrawingLogsColumns(
|
||||
</>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Image') },
|
||||
},
|
||||
{
|
||||
accessorKey: 'prompt',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Prompt')} />
|
||||
),
|
||||
header: t('Prompt'),
|
||||
cell: function PromptCell({ row }) {
|
||||
const log = row.original
|
||||
const prompt = row.getValue('prompt') as string
|
||||
@@ -269,7 +254,6 @@ export function useDrawingLogsColumns(
|
||||
</>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Prompt') },
|
||||
size: 200,
|
||||
maxSize: 220,
|
||||
},
|
||||
|
||||
+7
-21
@@ -25,7 +25,6 @@ import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
||||
import { formatTimestampToDate } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { DataTableColumnHeader } from '@/components/data-table'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { TASK_ACTIONS, TASK_STATUS } from '../../constants'
|
||||
import { taskActionMapper, taskStatusMapper } from '../../lib/mappers'
|
||||
@@ -94,9 +93,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
const columns: ColumnDef<TaskLog>[] = [
|
||||
{
|
||||
accessorKey: 'submit_time',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Submit Time')} />
|
||||
),
|
||||
header: t('Submit Time'),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
const submitTime = row.getValue('submit_time') as number
|
||||
@@ -117,17 +114,14 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
meta: { label: t('Submit Time') },
|
||||
},
|
||||
]
|
||||
|
||||
if (isAdmin) {
|
||||
columns.push(createChannelColumn<TaskLog>({ headerLabel: t('Channel') }), {
|
||||
id: 'user',
|
||||
header: t('User'),
|
||||
accessorFn: (row) => row.username || row.user_id,
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('User')} />
|
||||
),
|
||||
cell: function UserCell({ row }) {
|
||||
const { sensitiveVisible, setSelectedUserId, setUserInfoDialogOpen } =
|
||||
useUsageLogsContext()
|
||||
@@ -163,16 +157,13 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
</button>
|
||||
)
|
||||
},
|
||||
meta: { label: t('User') },
|
||||
})
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
accessorKey: 'task_id',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Task ID')} />
|
||||
),
|
||||
header: t('Task ID'),
|
||||
cell: ({ row }) => {
|
||||
const log = row.original
|
||||
const taskId = row.getValue('task_id') as string
|
||||
@@ -193,7 +184,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Task ID'), mobileTitle: true },
|
||||
meta: { mobileTitle: true },
|
||||
},
|
||||
createDurationColumn<TaskLog>({
|
||||
submitTimeKey: 'submit_time',
|
||||
@@ -204,9 +195,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
}),
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Status')} />
|
||||
),
|
||||
header: t('Status'),
|
||||
cell: ({ row }) => {
|
||||
const status = row.getValue('status') as string
|
||||
return (
|
||||
@@ -215,17 +204,15 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
variant={taskStatusMapper.getVariant(status)}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
className='-ml-1.5'
|
||||
/>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Status') },
|
||||
},
|
||||
createProgressColumn<TaskLog>({ headerLabel: t('Progress') }),
|
||||
{
|
||||
accessorKey: 'fail_reason',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Details')} />
|
||||
),
|
||||
header: t('Details'),
|
||||
cell: function DetailsCell({ row }) {
|
||||
const log = row.original
|
||||
const failReason = row.getValue('fail_reason') as string
|
||||
@@ -295,7 +282,6 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
</>
|
||||
)
|
||||
},
|
||||
meta: { label: t('Details') },
|
||||
size: 200,
|
||||
maxSize: 220,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user