♻️ refactor(web): consolidate design-system primitives and responsive data views

This commit is contained in:
t0ng7u
2026-07-11 05:13:16 +08:00
parent 262ab93123
commit 0918bdb49a
451 changed files with 7295 additions and 7693 deletions
@@ -22,7 +22,7 @@ import { Zap } from 'lucide-react'
import { useState } from 'react'
import { DataTableColumnHeader } from '@/components/data-table'
import { StatusBadge, tintedBadgeClassMap } from '@/components/status-badge'
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
import {
Tooltip,
TooltipContent,
@@ -30,7 +30,6 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip'
import { formatTimestampToDate, formatTokens } from '@/lib/format'
import { cn } from '@/lib/utils'
import { formatDuration } from '../../lib/format'
import { FailReasonDialog } from '../dialogs/fail-reason-dialog'
@@ -54,7 +53,7 @@ export function CacheTooltip({
<Tooltip>
<TooltipTrigger
render={<Zap className={`size-3 flex-shrink-0 ${color}`} />}
></TooltipTrigger>
/>
<TooltipContent side='top'>
<p className='text-xs'>
{label}: {formatTokens(tokens)}
@@ -95,7 +94,12 @@ export function createTimestampColumn<T>(config: {
</span>
)
},
meta: { label: title },
meta: {
label: title,
cardRole: 'primary',
cardOrder: 10,
contentMode: 'full',
},
}
}
@@ -135,22 +139,20 @@ export function createDurationColumn<T>(config: {
}
const variant =
duration.durationSec > warningThresholdSec ? 'danger' : 'success'
duration.durationSec > warningThresholdSec ? 'destructive' : 'success'
return (
<StatusBadge
label={`${duration.durationSec.toFixed(1)}s`}
variant={variant}
size='sm'
copyable={false}
className={cn(
'rounded-md tabular-nums',
tintedBadgeClassMap[variant]
)}
/>
<StatusBadge variant={variant} size='sm' className='tabular-nums'>
{duration.durationSec.toFixed(1)}s
</StatusBadge>
)
},
meta: { label: headerLabel },
meta: {
label: headerLabel,
cardRole: 'primary',
cardOrder: 40,
contentMode: 'full',
},
}
}
@@ -174,17 +176,22 @@ export function createChannelColumn<T>(config: {
return <span className='text-muted-foreground/60 text-xs'>-</span>
}
return (
<StatusBadge
label={`#${channelId}`}
autoColor={String(channelId)}
copyText={String(channelId)}
<CopyableStatusBadge
value={String(channelId)}
variant='neutral'
size='sm'
showDot={false}
className='font-mono'
/>
>
#{channelId}
</CopyableStatusBadge>
)
},
meta: { label: headerLabel },
meta: {
label: headerLabel,
cardRole: 'primary',
cardOrder: 20,
contentMode: 'full',
},
}
}
@@ -231,7 +238,13 @@ export function createFailReasonColumn<T>(config: {
</>
)
},
meta: { label: headerLabel },
meta: {
label: headerLabel,
cardRole: 'secondary',
cardOrder: 30,
cardSpan: 2,
contentMode: 'summary',
},
}
}
@@ -255,11 +268,16 @@ export function createProgressColumn<T>(config: {
return <span className='text-muted-foreground/60 text-xs'>-</span>
}
return (
<span className='border-border/60 bg-muted/30 inline-flex items-center rounded-md border px-1.5 py-0.5 font-mono text-xs'>
<StatusBadge variant='neutral' className='font-mono'>
{progress}
</span>
</StatusBadge>
)
},
meta: { label: headerLabel },
meta: {
label: headerLabel,
cardRole: 'secondary',
cardOrder: 10,
contentMode: 'full',
},
}
}
@@ -16,16 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type ColumnDef } from '@tanstack/react-table'
import { CircleAlert, GitBranch, Sparkles, KeyRound } from 'lucide-react'
import type { ColumnDef } from '@tanstack/react-table'
import { CircleAlert, GitBranch, Sparkles } from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
StatusBadge,
tintedBadgeClassMap,
type StatusBadgeProps,
} from '@/components/status-badge'
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
Popover,
@@ -38,6 +34,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useGroupRatios } from '@/hooks/use-group-ratios'
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
import {
@@ -83,7 +80,10 @@ function formatRatioCompact(ratio: number | undefined): string {
: ratio.toFixed(4).replace(/\.?0+$/, '')
}
function getGroupRatioText(other: LogOtherData | null): string | null {
function getGroupRatioText(
other: LogOtherData | null,
configuredGroupRatio?: number
): string | null {
const userGroupRatio = other?.user_group_ratio
if (
userGroupRatio != null &&
@@ -93,20 +93,14 @@ function getGroupRatioText(other: LogOtherData | null): string | null {
return `${formatRatioCompact(userGroupRatio)}x`
}
const groupRatio = other?.group_ratio
if (groupRatio != null && groupRatio !== 1 && Number.isFinite(groupRatio)) {
const groupRatio = other?.group_ratio ?? configuredGroupRatio
if (groupRatio != null && Number.isFinite(groupRatio)) {
return `${formatRatioCompact(groupRatio)}x`
}
return null
}
function splitQuotaDisplay(value: string): { prefix: string; amount: string } {
const match = value.match(/^([^0-9+\-.,\s]+)(.+)$/)
if (!match) return { prefix: '', amount: value }
return { prefix: match[1], amount: match[2] }
}
function buildDetailSegments(
log: UsageLog,
other: LogOtherData | null,
@@ -227,10 +221,10 @@ function buildTypeDetailSegments(
})
}
} else {
const isPerCall = isPerCallBilling(other.model_price)
if (isPerCall) {
const modelPrice = other.model_price
if (modelPrice != null && isPerCallBilling(modelPrice)) {
segments.push({
text: `${t('Per-call')} · ${formatBillingCurrencyFromUSD(other.model_price!, priceOpts)}`,
text: `${t('Per-call')} · ${formatBillingCurrencyFromUSD(modelPrice, priceOpts)}`,
})
} else if (other.model_ratio != null) {
const inputPriceUSD = other.model_ratio * 2.0
@@ -297,6 +291,7 @@ function buildTypeDetailSegments(
export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
const { t } = useTranslation()
const groupRatios = useGroupRatios()
const columns: ColumnDef<UsageLog>[] = [
{
accessorKey: 'created_at',
@@ -308,16 +303,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate text-xs tabular-nums'>
<span className='text-xs tabular-nums'>
{formatTimestampToDate(timestamp)}
</span>
<StatusBadge
label={t(config.label)}
variant={config.color as StatusBadgeProps['variant']}
size='sm'
copyable={false}
className='text-xs [&_span]:text-xs'
/>
<StatusBadge variant={config.variant} size='sm'>
{t(config.label)}
</StatusBadge>
</div>
)
},
@@ -328,6 +319,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
},
enableHiding: false,
size: 180,
meta: {
cardRole: 'primary',
cardOrder: 10,
contentMode: 'full',
},
},
]
@@ -374,24 +370,23 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
}
>
<div className='relative inline-flex w-fit items-center gap-1'>
<StatusBadge
label={channelIdDisplay}
autoColor={String(log.channel)}
copyText={String(log.channel)}
<CopyableStatusBadge
value={String(log.channel)}
variant='neutral'
size='sm'
showDot={false}
className='font-mono'
/>
>
{channelIdDisplay}
</CopyableStatusBadge>
{showMultiKeyIndex && (
<StatusBadge
label={String(multiKeyIndex)}
size='sm'
showDot={false}
copyable={false}
variant='neutral'
className='h-5 min-w-5 justify-center rounded-full px-1 font-mono text-xs'
className='min-w-5 justify-center font-mono'
aria-label={`${t('Key')} ${multiKeyIndex}`}
/>
>
{multiKeyIndex}
</StatusBadge>
)}
{hasRetryChain && (
<Popover>
@@ -489,6 +484,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</TooltipProvider>
)
},
meta: {
cardRole: 'primary',
cardOrder: 20,
contentMode: 'wrap',
},
},
{
id: 'user',
@@ -543,6 +543,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</button>
)
},
meta: {
cardRole: 'primary',
cardOrder: 30,
contentMode: 'wrap',
},
}
)
}
@@ -564,7 +569,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
if (!group) group = other?.group || ''
const metaParts: string[] = []
const groupRatioText = getGroupRatioText(other)
const groupRatioText = getGroupRatioText(
other,
group ? groupRatios[group] : undefined
)
if (group) {
metaParts.push(sensitiveVisible ? group : '••••')
}
@@ -575,14 +583,24 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<TooltipProvider delay={300}>
<Tooltip>
<TooltipTrigger render={<div className='max-w-full' />}>
<StatusBadge
label={displayName}
icon={KeyRound}
copyText={sensitiveVisible ? tokenName : undefined}
size='sm'
showDot={false}
className='border-border/60 bg-muted/30 text-foreground h-6 max-w-full gap-1.5 overflow-hidden rounded-md border px-2 py-0.5'
/>
{sensitiveVisible ? (
<CopyableStatusBadge
value={tokenName}
variant='neutral'
size='sm'
className='max-w-full'
>
{displayName}
</CopyableStatusBadge>
) : (
<StatusBadge
variant='neutral'
size='sm'
className='max-w-full'
>
{displayName}
</StatusBadge>
)}
</TooltipTrigger>
{sensitiveVisible && tokenName.length > 16 && (
<TooltipContent side='top' className='max-w-xs break-all'>
@@ -592,7 +610,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</Tooltip>
</TooltipProvider>
{metaParts.length > 0 && (
<span className='text-muted-foreground/60 truncate text-xs'>
<span className='text-muted-foreground/60 text-xs [overflow-wrap:anywhere] break-words'>
{metaParts.join(' · ')}
</span>
)}
@@ -600,6 +618,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
)
},
size: 160,
meta: {
cardRole: 'primary',
cardOrder: 40,
cardSpan: 2,
contentMode: 'full',
},
})
columns.push(
{
@@ -620,7 +644,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</div>
)
},
meta: { mobileTitle: true },
size: 180,
meta: {
cardRole: 'title',
cardSpan: 2,
contentMode: 'full',
},
},
{
accessorKey: 'use_time',
@@ -641,51 +670,33 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
? getFirstResponseTimeColor(frt / 1000)
: 'neutral'
const timingClass = (variant: string) =>
tintedBadgeClassMap[
(variant in tintedBadgeClassMap
? variant
: 'neutral') as keyof typeof tintedBadgeClassMap
]
return (
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-1.5'>
<StatusBadge
label={formatUseTime(useTime)}
variant={timeVariant as StatusBadgeProps['variant']}
variant={timeVariant}
size='sm'
copyable={false}
className={cn(
'rounded-md tabular-nums',
timingClass(timeVariant)
)}
/>
className='tabular-nums'
>
{formatUseTime(useTime)}
</StatusBadge>
{log.is_stream &&
(frt != null && frt > 0 ? (
<StatusBadge
label={formatUseTime(frt / 1000)}
variant={frtVariant as StatusBadgeProps['variant']}
variant={frtVariant}
size='sm'
showDot={false}
copyable={false}
className={cn(
'rounded-md tabular-nums',
timingClass(frtVariant)
)}
/>
className='tabular-nums'
>
{formatUseTime(frt / 1000)}
</StatusBadge>
) : (
<StatusBadge
label='N/A'
variant='neutral'
size='sm'
showDot={false}
copyable={false}
className={cn(
'rounded-md tabular-nums',
tintedBadgeClassMap.neutral
)}
/>
className='tabular-nums'
>
N/A
</StatusBadge>
))}
</div>
<div className='flex items-center gap-1 text-xs leading-none'>
@@ -710,7 +721,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
render={
<CircleAlert className='text-destructive size-3' />
}
></TooltipTrigger>
/>
<TooltipContent>
<div className='space-y-0.5 text-xs'>
<p>
@@ -732,6 +743,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</div>
)
},
meta: {
cardRole: 'primary',
cardOrder: 50,
contentMode: 'full',
},
},
{
@@ -780,6 +796,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
</div>
)
},
meta: {
cardRole: 'primary',
cardOrder: 60,
cardSpan: 2,
contentMode: 'full',
},
},
{
@@ -799,15 +821,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
<Tooltip>
<TooltipTrigger
render={
<StatusBadge
label={t('Subscription')}
variant='success'
size='sm'
copyable={false}
className='cursor-help'
/>
<span className='text-success cursor-help text-sm font-medium' />
}
/>
>
{t('Subscription')}
</TooltipTrigger>
<TooltipContent>
<span>
{t('Deducted by subscription')}: {formatLogQuota(quota)}
@@ -818,20 +836,16 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
)
}
const quotaStr = formatLogQuota(quota)
const quotaDisplay = splitQuotaDisplay(quotaStr)
return (
<div className='flex flex-col gap-0.5'>
<span className='border-border/80 bg-muted/60 inline-flex h-6 w-fit items-center rounded-md border px-2 text-sm leading-none font-semibold tabular-nums'>
{quotaDisplay.prefix && (
<span className='mr-1'>{quotaDisplay.prefix}</span>
)}
<span>{quotaDisplay.amount}</span>
</span>
</div>
<span className='text-sm font-medium tabular-nums'>
{formatLogQuota(quota)}
</span>
)
},
meta: {
cardRole: 'badge',
contentMode: 'full',
},
},
{
@@ -841,43 +855,59 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
const [dialogOpen, setDialogOpen] = useState(false)
const log = row.original
const other = parseLogOther(log.other)
const ip = log.ip.trim()
const segments = buildDetailSegments(log, other, t, isAdmin)
const primary = segments[0]
const hasMore = segments.length > 1
let detailsContent = <span className='text-muted-foreground/40'></span>
if (log.content) {
detailsContent = (
<span className='text-muted-foreground truncate group-hover:underline'>
{log.content}
</span>
)
}
if (primary) {
let primaryClassName = 'text-foreground'
if (primary.muted) {
primaryClassName = 'text-muted-foreground/60'
} else if (primary.danger) {
primaryClassName = 'text-destructive'
}
detailsContent = (
<span
className={cn(
'truncate leading-snug group-hover:underline',
primaryClassName
)}
>
{primary.text}
{hasMore && (
<span className='text-muted-foreground/40 ml-0.5'>
+{segments.length - 1}
</span>
)}
</span>
)
}
return (
<>
<button
type='button'
className='group flex max-w-[200px] items-center gap-1 text-left text-xs'
className='group flex max-w-[200px] flex-col gap-0.5 text-left text-xs'
onClick={() => setDialogOpen(true)}
title={t('Click to view full details')}
>
{primary ? (
<span
className={cn(
'truncate leading-snug group-hover:underline',
primary.muted
? 'text-muted-foreground/60'
: primary.danger
? 'text-destructive'
: 'text-foreground'
)}
>
{primary.text}
{hasMore && (
<span className='text-muted-foreground/40 ml-0.5'>
+{segments.length - 1}
</span>
)}
{detailsContent}
{ip && (
<span className='text-muted-foreground/60 max-w-full truncate tabular-nums'>
{ip}
</span>
) : log.content ? (
<span className='text-muted-foreground truncate group-hover:underline'>
{log.content}
</span>
) : (
<span className='text-muted-foreground/40'></span>
)}
</button>
<DetailsDialog
@@ -891,6 +921,14 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
},
size: 180,
maxSize: 200,
meta: {
pinned: 'right',
contentSized: true,
cardRole: 'secondary',
cardOrder: 10,
cardSpan: 2,
contentMode: 'summary',
},
}
)
@@ -38,7 +38,7 @@ import {
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { StatusBadge } from '@/components/status-badge'
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
import { formatTimestampToDate } from '@/lib/format'
import { MJ_TASK_TYPES } from '../../constants'
@@ -95,19 +95,24 @@ export function useDrawingLogsColumns(
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate text-xs tabular-nums'>
<span className='text-xs tabular-nums'>
{formatTimestampToDate(submitTime, 'milliseconds')}
</span>
<StatusBadge
label={t(mjStatusMapper.getLabel(log.status))}
variant={mjStatusMapper.getVariant(log.status)}
size='sm'
copyable={false}
/>
>
{t(mjStatusMapper.getLabel(log.status))}
</StatusBadge>
</div>
)
},
size: 180,
meta: {
cardRole: 'primary',
cardOrder: 10,
contentMode: 'full',
},
},
]
@@ -122,16 +127,19 @@ export function useDrawingLogsColumns(
header: t('Type'),
cell: ({ row }) => {
const action = row.getValue('action') as string
const TypeIcon = getDrawingTypeIcon(action)
return (
<StatusBadge
label={t(mjTaskTypeMapper.getLabel(action))}
variant={mjTaskTypeMapper.getVariant(action)}
icon={getDrawingTypeIcon(action)}
size='sm'
copyable={false}
/>
<StatusBadge variant={mjTaskTypeMapper.getVariant(action)} size='sm'>
<TypeIcon data-icon='inline-start' aria-hidden='true' />
{t(mjTaskTypeMapper.getLabel(action))}
</StatusBadge>
)
},
meta: {
cardRole: 'title',
cardSpan: 2,
contentMode: 'wrap',
},
})
columns.push({
@@ -146,17 +154,23 @@ export function useDrawingLogsColumns(
return (
<div className='flex max-w-[160px] flex-col gap-0.5'>
<StatusBadge
label={mjId}
copyText={mjId}
<CopyableStatusBadge
value={mjId}
variant='neutral'
size='sm'
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
/>
className='h-auto max-w-full overflow-visible font-mono [overflow-wrap:anywhere] whitespace-normal [&_[data-slot=status-badge-label]]:overflow-visible [&_[data-slot=status-badge-label]]:text-clip [&_[data-slot=status-badge-label]]:whitespace-normal'
>
{mjId}
</CopyableStatusBadge>
</div>
)
},
meta: { mobileTitle: true },
meta: {
cardRole: 'primary',
cardOrder: 30,
cardSpan: 2,
contentMode: 'full',
},
})
columns.push(
@@ -176,13 +190,17 @@ export function useDrawingLogsColumns(
return (
<StatusBadge
label={t(mjSubmitResultMapper.getLabel(String(code)))}
variant={mjSubmitResultMapper.getVariant(String(code))}
size='sm'
copyable={false}
/>
>
{t(mjSubmitResultMapper.getLabel(String(code)))}
</StatusBadge>
)
},
meta: {
cardRole: 'badge',
contentMode: 'full',
},
})
}
@@ -208,7 +226,7 @@ export function useDrawingLogsColumns(
onClick={() => setDialogOpen(true)}
title={t('Click to view image')}
>
<span className='text-foreground truncate leading-snug group-hover:underline'>
<span className='text-foreground leading-snug group-hover:underline'>
{t('View')}
</span>
</button>
@@ -221,6 +239,11 @@ export function useDrawingLogsColumns(
</>
)
},
meta: {
cardRole: 'primary',
cardOrder: 50,
contentMode: 'full',
},
},
{
accessorKey: 'prompt',
@@ -257,6 +280,12 @@ export function useDrawingLogsColumns(
},
size: 200,
maxSize: 220,
meta: {
cardRole: 'secondary',
cardOrder: 20,
cardSpan: 2,
contentMode: 'summary',
},
},
createFailReasonColumn<MidjourneyLog>({
headerLabel: t('Fail Reason'),
@@ -22,7 +22,7 @@ import { Music } from 'lucide-react'
import { useState, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StatusBadge } from '@/components/status-badge'
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
import { formatTimestampToDate } from '@/lib/format'
@@ -102,11 +102,11 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
return (
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='truncate text-xs tabular-nums'>
<span className='text-xs tabular-nums'>
{formatTimestampToDate(submitTime, 'seconds')}
</span>
{log.finish_time ? (
<span className='text-muted-foreground/60 truncate text-xs tabular-nums'>
<span className='text-muted-foreground/60 text-xs tabular-nums'>
{formatTimestampToDate(log.finish_time, 'seconds')}
</span>
) : (
@@ -116,6 +116,11 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
)
},
size: 180,
meta: {
cardRole: 'primary',
cardOrder: 10,
contentMode: 'full',
},
},
]
@@ -153,12 +158,17 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
{sensitiveVisible ? getUserAvatarFallback(displayName) : '•'}
</AvatarFallback>
</Avatar>
<span className='text-muted-foreground truncate text-sm hover:underline'>
<span className='text-muted-foreground text-sm [overflow-wrap:anywhere] break-words hover:underline'>
{sensitiveVisible ? displayName : '••••'}
</span>
</button>
)
},
meta: {
cardRole: 'primary',
cardOrder: 30,
contentMode: 'wrap',
},
})
}
@@ -174,20 +184,25 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
}
return (
<div className='flex max-w-[170px] flex-col gap-0.5'>
<StatusBadge
label={taskId}
copyText={taskId}
<CopyableStatusBadge
value={taskId}
variant='neutral'
size='sm'
className='border-border/60 bg-muted/30 text-foreground max-w-full truncate rounded-md border px-1.5 py-0.5 font-mono'
/>
<span className='text-muted-foreground/60 truncate text-xs'>
className='h-auto max-w-full overflow-visible font-mono [overflow-wrap:anywhere] whitespace-normal [&_[data-slot=status-badge-label]]:overflow-visible [&_[data-slot=status-badge-label]]:text-clip [&_[data-slot=status-badge-label]]:whitespace-normal'
>
{taskId}
</CopyableStatusBadge>
<span className='text-muted-foreground/60 text-xs [overflow-wrap:anywhere] break-words'>
{t(log.platform)} · {t(taskActionMapper.getLabel(log.action))}
</span>
</div>
)
},
meta: { mobileTitle: true },
meta: {
cardRole: 'title',
cardSpan: 2,
contentMode: 'full',
},
},
createDurationColumn<TaskLog>({
submitTimeKey: 'submit_time',
@@ -202,14 +217,15 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
cell: ({ row }) => {
const status = row.getValue('status') as string
return (
<StatusBadge
label={t(taskStatusMapper.getLabel(status, status || 'Submitting'))}
variant={taskStatusMapper.getVariant(status)}
size='sm'
copyable={false}
/>
<StatusBadge variant={taskStatusMapper.getVariant(status)} size='sm'>
{t(taskStatusMapper.getLabel(status, status || 'Submitting'))}
</StatusBadge>
)
},
meta: {
cardRole: 'badge',
contentMode: 'full',
},
},
createProgressColumn<TaskLog>({ headerLabel: t('Progress') }),
{
@@ -272,7 +288,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
onClick={() => setDialogOpen(true)}
title={t('Click to view full error message')}
>
<span className='truncate leading-snug text-red-600 group-hover:underline dark:text-red-400'>
<span className='text-destructive truncate leading-snug group-hover:underline'>
{failReason}
</span>
</button>
@@ -286,6 +302,12 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
},
size: 200,
maxSize: 220,
meta: {
cardRole: 'secondary',
cardOrder: 20,
cardSpan: 2,
contentMode: 'summary',
},
}
)
@@ -23,7 +23,7 @@ import { Eye, EyeOff } from 'lucide-react'
import { useState, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Button } from '@/components/design-system/button'
import {
Select,
SelectContent,
@@ -31,7 +31,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
} from '@/components/design-system/select'
import {
Tooltip,
TooltipContent,
@@ -275,10 +275,10 @@ export function CommonLogsFilterBar<TData>(
render={
<Button
variant='ghost'
size='icon'
size='icon-sm'
onClick={() => setSensitiveVisible(!sensitiveVisible)}
aria-label={sensitiveVisible ? t('Hide') : t('Show')}
className='text-muted-foreground hover:text-foreground size-7'
className='text-muted-foreground hover:text-foreground'
/>
}
>
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { Eye, EyeOff } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Button } from '@/components/design-system/button'
import {
Tooltip,
TooltipContent,
@@ -48,10 +48,10 @@ export function CommonLogsHeaderActions() {
render={
<Button
variant='ghost'
size='icon'
size='icon-sm'
onClick={() => setSensitiveVisible(!sensitiveVisible)}
aria-label={sensitiveVisible ? t('Hide') : t('Show')}
className='text-muted-foreground hover:text-foreground size-7'
className='text-muted-foreground hover:text-foreground'
/>
}
>
@@ -20,10 +20,10 @@ import { useQuery } from '@tanstack/react-query'
import { getRouteApi } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { useIsAdmin } from '@/hooks/use-admin'
import { formatLogQuota } from '@/lib/format'
import { cn } from '@/lib/utils'
import { getLogStats, getUserLogStats } from '../api'
import { DEFAULT_LOG_STATS } from '../constants'
@@ -34,17 +34,23 @@ const route = getRouteApi('/_authenticated/usage-logs/$section')
function StatBadge(props: {
label: string
tone: 'usage' | 'rpm' | 'tpm'
value: string | number
accent: string
}) {
let labelClassName = 'text-foreground'
if (props.tone === 'usage') {
labelClassName = 'text-status-info'
} else if (props.tone === 'rpm') {
labelClassName = 'text-metric-rpm'
}
return (
<span className='border-border/60 bg-muted/25 inline-flex h-7 items-center gap-2 rounded-md border px-2.5 text-xs shadow-xs'>
<span className={cn('h-3.5 w-0.5 rounded-full', props.accent)} />
<span className='text-muted-foreground'>{props.label}</span>
<Badge variant='outline' className='h-6 gap-2'>
<span className={labelClassName}>{props.label}</span>
<span className='text-foreground/85 font-semibold tabular-nums'>
{props.value}
</span>
</span>
</Badge>
)
}
@@ -90,19 +96,11 @@ export function CommonLogsStats() {
<div className='flex flex-wrap items-center gap-2'>
<StatBadge
label={t('Usage')}
tone='usage'
value={sensitiveVisible ? formatLogQuota(stats?.quota || 0) : '••••'}
accent='bg-chart-1/70'
/>
<StatBadge
label={t('RPM')}
value={stats?.rpm || 0}
accent='bg-chart-5/70'
/>
<StatBadge
label={t('TPM')}
value={stats?.tpm || 0}
accent='bg-muted-foreground/50'
/>
<StatBadge label={t('RPM')} tone='rpm' value={stats?.rpm || 0} />
<StatBadge label={t('TPM')} tone='tpm' value={stats?.tpm || 0} />
</div>
)
}
@@ -20,8 +20,8 @@ import { CalendarDays } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/design-system/button'
import { Input } from '@/components/design-system/input'
import {
Popover,
PopoverContent,
@@ -148,7 +148,7 @@ export function CompactDateTimeRangePicker({
type='datetime-local'
value={draftStart}
onChange={(e) => setDraftStart(e.target.value)}
className='h-8 text-sm leading-5 tabular-nums'
className='text-sm leading-5 tabular-nums'
/>
</div>
<span className='text-muted-foreground hidden pb-2 text-xs sm:block'>
@@ -162,7 +162,7 @@ export function CompactDateTimeRangePicker({
type='datetime-local'
value={draftEnd}
onChange={(e) => setDraftEnd(e.target.value)}
className='h-8 text-sm leading-5 tabular-nums'
className='text-sm leading-5 tabular-nums'
/>
</div>
</div>
@@ -171,8 +171,7 @@ export function CompactDateTimeRangePicker({
<Button
type='button'
variant='secondary'
size='sm'
className='h-7 flex-1 px-2 text-xs'
className='flex-1'
onClick={() => applyPreset('today')}
>
{t('Today')}
@@ -180,8 +179,7 @@ export function CompactDateTimeRangePicker({
<Button
type='button'
variant='secondary'
size='sm'
className='h-7 flex-1 px-2 text-xs'
className='flex-1'
onClick={() => applyPreset('7d')}
>
{t('7 Days')}
@@ -189,8 +187,7 @@ export function CompactDateTimeRangePicker({
<Button
type='button'
variant='secondary'
size='sm'
className='h-7 flex-1 px-2 text-xs'
className='flex-1'
onClick={() => applyPreset('week')}
>
{t('This week')}
@@ -198,8 +195,7 @@ export function CompactDateTimeRangePicker({
<Button
type='button'
variant='secondary'
size='sm'
className='h-7 flex-1 px-2 text-xs'
className='flex-1'
onClick={() => applyPreset('30d')}
>
{t('30 Days')}
@@ -207,8 +203,7 @@ export function CompactDateTimeRangePicker({
<Button
type='button'
variant='secondary'
size='sm'
className='h-7 flex-1 px-2 text-xs'
className='flex-1'
onClick={() => applyPreset('month')}
>
{t('This month')}
@@ -216,9 +211,7 @@ export function CompactDateTimeRangePicker({
</div>
<div className='flex justify-end'>
<Button size='sm' className='h-8' onClick={applyDraft}>
{t('Confirm')}
</Button>
<Button onClick={applyDraft}>{t('Confirm')}</Button>
</div>
</div>
</PopoverContent>
@@ -21,9 +21,9 @@ import { useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/design-system/button'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
export interface AudioClip {
@@ -88,12 +88,9 @@ function AudioClipCard({ clip }: { clip: AudioClip }) {
<div className='mb-1 flex items-center gap-2'>
<span className='truncate text-sm font-medium'>{title}</span>
{duration != null && duration > 0 && (
<StatusBadge
label={formatDuration(duration)}
variant='neutral'
className='shrink-0'
copyable={false}
/>
<StatusBadge variant='neutral' className='shrink-0'>
{formatDuration(duration)}
</StatusBadge>
)}
</div>
@@ -108,8 +105,6 @@ function AudioClipCard({ clip }: { clip: AudioClip }) {
</span>
<Button
variant='outline'
size='sm'
className='h-7 gap-1 text-xs'
onClick={() => window.open(audioUrl, '_blank')}
>
<ExternalLink className='h-3 w-3' />
@@ -117,8 +112,6 @@ function AudioClipCard({ clip }: { clip: AudioClip }) {
</Button>
<Button
variant='outline'
size='sm'
className='h-7 gap-1 text-xs'
onClick={() => {
navigator.clipboard.writeText(audioUrl)
toast.success(t('Copied'))
@@ -33,9 +33,9 @@ import {
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Dialog } from '@/components/dialog'
import { StatusBadge, type StatusBadgeProps } from '@/components/status-badge'
import { Button } from '@/components/ui/button'
import { StatusBadge, type StatusVariant } from '@/components/status-badge'
import { Label } from '@/components/ui/label'
import { DynamicPricingBreakdown } from '@/features/pricing/components/dynamic-pricing-breakdown'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
@@ -74,14 +74,6 @@ const CHANNEL_FIELD_LABELS: Record<string, string> = {
key: 'Key',
}
function timingTextColorClass(
variant: 'success' | 'warning' | 'danger'
): string {
if (variant === 'success') return 'text-success'
if (variant === 'warning') return 'text-warning'
return 'text-destructive'
}
function DetailRow(props: {
label: React.ReactNode
value: React.ReactNode
@@ -109,16 +101,16 @@ function DetailRow(props: {
function DetailSection(props: {
icon?: React.ReactNode
label: string
variant?: 'default' | 'danger'
variant?: 'default' | 'destructive'
children: React.ReactNode
}) {
const isDanger = props.variant === 'danger'
const isDestructive = props.variant === 'destructive'
return (
<div className='min-w-0 space-y-1.5'>
<Label
className={cn(
'flex items-center gap-1.5 text-xs font-semibold',
isDanger && 'text-red-500'
isDestructive && 'text-destructive'
)}
>
{props.icon}
@@ -127,8 +119,8 @@ function DetailSection(props: {
<div
className={cn(
'min-w-0 space-y-1 overflow-hidden rounded-md border p-2.5 max-sm:p-2',
isDanger
? 'border-red-200 bg-red-50 dark:border-red-900 dark:bg-red-950/20'
isDestructive
? 'border-destructive/25 bg-destructive/10'
: 'bg-muted/30'
)}
>
@@ -335,8 +327,8 @@ function BillingBreakdown(props: {
return (
<DetailSection label={t('Billing Details')}>
{rows.map((row, idx) => (
<DetailRow key={idx} label={row.label} value={row.value} mono />
{rows.map((row) => (
<DetailRow key={row.label} label={row.label} value={row.value} mono />
))}
</DetailSection>
)
@@ -401,8 +393,8 @@ function TokenBreakdown(props: { log: UsageLog; other: LogOtherData }) {
return (
<DetailSection label={t('Token Breakdown')}>
{rows.map((row, idx) => (
<DetailRow key={idx} label={row.label} value={row.value} mono />
{rows.map((row) => (
<DetailRow key={row.label} label={row.label} value={row.value} mono />
))}
</DetailSection>
)
@@ -421,6 +413,12 @@ export function DetailsDialog(props: DetailsDialogProps) {
const details = props.log.content ?? ''
const other = parseLogOther(props.log.other)
const typeConfig = getLogTypeConfig(props.log.type)
let reasoningEffortVariant: StatusVariant = 'success'
if (other?.reasoning_effort === 'high') {
reasoningEffortVariant = 'warning'
} else if (other?.reasoning_effort === 'medium') {
reasoningEffortVariant = 'info'
}
const isViolation = isViolationFeeLog(other)
const isRefund = props.log.type === 6
@@ -551,12 +549,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
title={
<>
{t('Log Details')}
<StatusBadge
label={t(typeConfig.label)}
variant={typeConfig.color as StatusBadgeProps['variant']}
size='sm'
copyable={false}
/>
<StatusBadge variant={typeConfig.variant} size='sm'>
{t(typeConfig.label)}
</StatusBadge>
</>
}
description={t('View the complete details for this log entry')}
@@ -643,32 +638,25 @@ export function DetailsDialog(props: DetailsDialogProps) {
<DetailRow
label={t('Response Time')}
value={
<span
className={cn(
'font-medium',
timingTextColorClass(
getResponseTimeColor(
props.log.use_time,
props.log.completion_tokens
)
)
)}
>
{formatUseTime(props.log.use_time)}
<span className='flex flex-wrap items-center gap-1'>
<StatusBadge
appearance='plain'
variant={getResponseTimeColor(
props.log.use_time,
props.log.completion_tokens
)}
>
{formatUseTime(props.log.use_time)}
</StatusBadge>
{props.log.is_stream &&
other?.frt != null &&
other.frt > 0 && (
<span
className={cn(
'font-normal',
timingTextColorClass(
getFirstResponseTimeColor(other.frt / 1000)
)
)}
<StatusBadge
appearance='plain'
variant={getFirstResponseTimeColor(other.frt / 1000)}
>
{' '}
(FRT: {formatUseTime(other.frt / 1000)})
</span>
{`(FRT: ${formatUseTime(other.frt / 1000)})`}
</StatusBadge>
)}
</span>
}
@@ -682,14 +670,14 @@ export function DetailsDialog(props: DetailsDialogProps) {
<div className='relative min-w-0'>
<Button
variant='ghost'
size='sm'
className='absolute top-0 right-0 h-5 w-5 p-0'
size='icon-xs'
className='absolute top-0 right-0'
onClick={() => copyToClipboard(conversionLabel)}
title={t('Copy to clipboard')}
aria-label={t('Copy to clipboard')}
>
{copiedText === conversionLabel ? (
<Check className='size-3 text-green-600' />
<Check className='text-success size-3' />
) : (
<Copy className='size-3' />
)}
@@ -721,7 +709,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
<DetailSection
icon={<AlertTriangle className='size-3.5' aria-hidden='true' />}
label={t('Quota clamped')}
variant='danger'
variant='destructive'
>
<p className='mb-1 text-xs wrap-break-word'>
{t('Quota saturation protection triggered')}
@@ -756,7 +744,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
<DetailSection
icon={<AlertTriangle className='size-3.5' aria-hidden='true' />}
label={t('Reject Reason')}
variant='danger'
variant='destructive'
>
<p className='text-xs wrap-break-word'>{other.reject_reason}</p>
</DetailSection>
@@ -767,7 +755,7 @@ export function DetailsDialog(props: DetailsDialogProps) {
<DetailSection
icon={<AlertTriangle className='size-3.5' aria-hidden='true' />}
label={t('Violation Fee')}
variant='danger'
variant='destructive'
>
{other.violation_fee_code && (
<DetailRow
@@ -808,9 +796,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
icon={<ShieldCheck className='size-3.5' aria-hidden='true' />}
label={t('Top-up Audit Info')}
>
{topupAuditFields.map((field, idx) => (
{topupAuditFields.map((field) => (
<DetailRow
key={idx}
key={field.label}
label={field.label}
value={field.value}
mono
@@ -897,9 +885,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
{operationText != null && (
<DetailRow label={t('Operation')} value={operationText} />
)}
{loginAuditFields.map((field, idx) => (
{loginAuditFields.map((field) => (
<DetailRow
key={idx}
key={field.label}
label={field.label}
value={field.value}
mono
@@ -950,18 +938,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
<DetailRow
label={t('Reasoning Effort')}
value={
<StatusBadge
label={other.reasoning_effort}
variant={
other.reasoning_effort === 'high'
? 'orange'
: other.reasoning_effort === 'medium'
? 'yellow'
: 'green'
}
size='sm'
copyable={false}
/>
<StatusBadge variant={reasoningEffortVariant} size='sm'>
{other.reasoning_effort}
</StatusBadge>
}
/>
)}
@@ -971,12 +950,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
<DetailRow
label={t('System Prompt')}
value={
<StatusBadge
label={t('Overwritten')}
variant='orange'
size='sm'
copyable={false}
/>
<StatusBadge variant='warning' size='sm'>
{t('Overwritten')}
</StatusBadge>
}
/>
)}
@@ -1055,12 +1031,9 @@ export function DetailsDialog(props: DetailsDialogProps) {
<DetailRow
label={t('Status')}
value={
<StatusBadge
label={other.stream_status.status || t('Error')}
variant='red'
size='sm'
copyable={false}
/>
<StatusBadge variant='destructive' size='sm'>
{other.stream_status.status || t('Error')}
</StatusBadge>
}
/>
{other.stream_status.end_reason && (
@@ -1144,20 +1117,17 @@ export function DetailsDialog(props: DetailsDialogProps) {
icon={<Settings2 className='size-3.5' aria-hidden='true' />}
label={`${t('Param Override')} (${other.po.length})`}
>
{other.po.filter(Boolean).map((line, idx) => {
{other.po.filter(Boolean).map((line) => {
const parsed = parseAuditLine(line)
if (!parsed) return null
return (
<div
key={idx}
key={line}
className='bg-background/60 flex min-w-0 flex-col gap-1.5 rounded border p-2 sm:flex-row sm:items-start sm:gap-2'
>
<StatusBadge
variant='neutral'
label={getParamOverrideActionLabel(parsed.action, t)}
className='shrink-0 font-medium'
copyable={false}
/>
<StatusBadge variant='neutral' className='shrink-0'>
{getParamOverrideActionLabel(parsed.action, t)}
</StatusBadge>
<span className='min-w-0 font-mono text-xs leading-relaxed break-all sm:wrap-break-word'>
{parsed.content}
</span>
@@ -1174,14 +1144,14 @@ export function DetailsDialog(props: DetailsDialogProps) {
<div className='bg-muted/30 relative min-w-0 overflow-hidden rounded-md border p-2.5'>
<Button
variant='ghost'
size='sm'
className='absolute top-1.5 right-1.5 h-5 w-5 p-0'
size='icon-xs'
className='absolute top-1.5 right-1.5'
onClick={() => copyToClipboard(details)}
title={t('Copy to clipboard')}
aria-label={t('Copy to clipboard')}
>
{copiedText === details ? (
<Check className='size-3 text-green-600' />
<Check className='text-success size-3' />
) : (
<Copy className='size-3' />
)}
@@ -19,8 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
import { Copy, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Dialog } from '@/components/dialog'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
@@ -58,8 +58,8 @@ export function FailReasonDialog({
<div className='bg-muted/50 relative rounded-md border border-red-200 p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
size='icon'
className='absolute top-2 right-2'
onClick={() => copyToClipboard(failReason)}
title={t('Copy to clipboard')}
>
@@ -19,8 +19,8 @@ For commercial licensing, please contact support@quantumnous.com
import { Copy, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
import { Dialog } from '@/components/dialog'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
@@ -59,8 +59,8 @@ export function PromptDialog({
<div className='bg-muted/50 relative rounded-md border p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
size='icon'
className='absolute top-2 right-2'
onClick={() => copyToClipboard(prompt)}
title={t('Copy to clipboard')}
>
@@ -85,8 +85,8 @@ export function PromptDialog({
<div className='bg-muted/50 relative rounded-md border p-3'>
<Button
variant='ghost'
size='sm'
className='absolute top-2 right-2 h-8 w-8 p-0'
size='icon'
className='absolute top-2 right-2'
onClick={() => copyToClipboard(promptEn)}
title={t('Copy to clipboard')}
>
@@ -17,25 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Table } from '@tanstack/react-table'
import { ChevronDown, Loader2 } from 'lucide-react'
import { useState, type ComponentProps, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import type { ReactNode } from 'react'
import { DataTableViewOptions } from '@/components/data-table'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer'
import { Input } from '@/components/ui/input'
import { useMediaQuery } from '@/hooks'
import { cn } from '@/lib/utils'
DataTableFilterField,
DataTableFilterInput,
DataTableFilterPanel,
} from '@/components/data-table'
interface LogsFilterToolbarProps<TData> {
table: Table<TData>
@@ -55,214 +43,9 @@ interface LogsFilterToolbarProps<TData> {
className?: string
}
interface LogsFilterFieldProps {
children: ReactNode
wide?: boolean
className?: string
}
export function LogsFilterField(props: LogsFilterFieldProps) {
return (
<div
className={cn(
'min-w-0 [&_[data-slot=select-trigger]]:w-full [&_[data-slot=select-trigger]]:text-sm [&_[data-slot=select-value]]:leading-5',
props.wide && 'sm:col-span-2',
props.className
)}
>
{props.children}
</div>
)
}
export function LogsFilterInput(props: ComponentProps<typeof Input>) {
return (
<Input
{...props}
className={cn('h-8 min-w-0 text-sm leading-5', props.className)}
/>
)
}
export const LogsFilterField = DataTableFilterField
export const LogsFilterInput = DataTableFilterInput
export function LogsFilterToolbar<TData>(props: LogsFilterToolbarProps<TData>) {
const { t } = useTranslation()
const [advancedOpen, setAdvancedOpen] = useState(false)
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false)
const isMobile = useMediaQuery('(max-width: 640px)')
const hasAdvancedFilters = props.advancedFilters != null
const activeAdvancedCount =
props.advancedFilterCount ?? (props.hasAdvancedActiveFilters ? 1 : 0)
const activeMobileFilterCount = props.mobileFilterCount ?? activeAdvancedCount
const handleMobileReset = () => {
props.onReset()
setMobileFiltersOpen(false)
}
const handleMobileSearch = () => {
props.onSearch()
setMobileFiltersOpen(false)
}
const advancedToggle = hasAdvancedFilters ? (
<Button
type='button'
variant='ghost'
onClick={() => setAdvancedOpen((open) => !open)}
aria-expanded={advancedOpen}
className={cn(
'text-muted-foreground hover:text-foreground gap-1 px-2',
props.hasAdvancedActiveFilters &&
!advancedOpen &&
'text-primary hover:text-primary'
)}
>
{advancedOpen ? t('Collapse') : t('Expand')}
{activeAdvancedCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
{activeAdvancedCount}
</Badge>
)}
<ChevronDown
className={cn(
'size-3.5 transition-transform duration-200',
advancedOpen && 'rotate-180'
)}
/>
</Button>
) : null
if (isMobile && props.mobilePinnedFilters != null) {
return (
<Drawer open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
<div
className={cn('bg-card/50 rounded-lg border p-2.5', props.className)}
>
<div className='grid gap-2'>{props.mobilePinnedFilters}</div>
<div className='mt-2 flex flex-col gap-2'>
{props.stats}
<div className='flex items-center justify-end gap-1.5'>
{props.actionStart}
<DrawerTrigger asChild>
<Button
type='button'
variant='ghost'
className={cn(
'text-muted-foreground hover:text-foreground gap-1 px-2',
activeMobileFilterCount > 0 &&
'text-primary hover:text-primary'
)}
>
{t('Filter')}
{activeMobileFilterCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-xs'>
{activeMobileFilterCount}
</Badge>
)}
</Button>
</DrawerTrigger>
<Button
type='button'
onClick={props.onSearch}
disabled={props.searchLoading}
>
{props.searchLoading && <Loader2 className='animate-spin' />}
{t('Search')}
</Button>
<DataTableViewOptions table={props.table} />
</div>
</div>
</div>
<DrawerContent className='max-h-[85dvh] p-0'>
<div className='mx-auto flex w-full max-w-md flex-1 flex-col overflow-hidden'>
<DrawerHeader className='border-border/70 border-b px-4 py-3 text-left'>
<DrawerTitle>{t('Filter')}</DrawerTitle>
<DrawerDescription>
{t('Adjust filters, then search to refresh the logs.')}
</DrawerDescription>
</DrawerHeader>
<div className='flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-4 py-3'>
{props.mobileFilters ?? (
<>
{props.primaryFilters}
{props.advancedFilters}
</>
)}
</div>
<DrawerFooter className='border-border/70 grid grid-cols-2 gap-2 border-t px-4 py-3'>
<Button
type='button'
variant='outline'
onClick={handleMobileReset}
disabled={!props.hasActiveFilters}
>
{t('Reset')}
</Button>
<Button
type='button'
onClick={handleMobileSearch}
disabled={props.searchLoading}
>
{props.searchLoading && <Loader2 className='animate-spin' />}
{t('Search')}
</Button>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
)
}
return (
<div
className={cn(
'bg-card/50 rounded-lg border p-2.5 sm:p-3',
props.className
)}
>
<div className='flex flex-wrap items-start gap-2'>
<div className='grid min-w-0 flex-1 grid-cols-1 gap-2 sm:grid-cols-[repeat(auto-fit,minmax(10rem,1fr))]'>
{props.primaryFilters}
</div>
{advancedToggle && (
<div className='flex shrink-0 items-center justify-end'>
{advancedToggle}
</div>
)}
</div>
{advancedOpen && props.advancedFilters && (
<div className='mt-2 grid grid-cols-1 gap-2 sm:grid-cols-[repeat(auto-fit,minmax(10rem,1fr))]'>
{props.advancedFilters}
</div>
)}
<div className='mt-2 flex flex-wrap items-center gap-2'>
{props.stats}
<div className='ms-auto flex flex-wrap items-center justify-end gap-1.5 sm:gap-2'>
{props.actionStart}
<Button
type='button'
variant='outline'
onClick={props.onReset}
disabled={!props.hasActiveFilters}
>
{t('Reset')}
</Button>
<Button
type='button'
onClick={props.onSearch}
disabled={props.searchLoading}
>
{props.searchLoading && <Loader2 className='animate-spin' />}
{t('Search')}
</Button>
<DataTableViewOptions table={props.table} />
</div>
</div>
</div>
)
return <DataTableFilterPanel {...props} />
}
@@ -19,12 +19,13 @@ For commercial licensing, please contact support@quantumnous.com
import { Route } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { StatusBadge } from '@/components/status-badge'
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { getIdentityColorClass } from '@/lib/colors'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
@@ -122,34 +123,58 @@ function resolveModelProvider(modelName: string): ModelProvider | null {
return null
}
function ModelBadgeContent(props: ModelBadgeProps) {
function ModelBadgeContent(
props: ModelBadgeProps & {
staticOnly: boolean
}
) {
const provider = resolveModelProvider(props.modelName)
const colorClassName = getIdentityColorClass(props.modelName)
const content = [
provider ? (
<span
key='provider'
data-icon='inline-start'
className='flex items-center justify-center'
aria-label={provider.label}
>
{getLobeIcon(provider.icon, 12)}
</span>
) : null,
<span key='model' className='whitespace-nowrap'>
{props.modelName}
</span>,
]
if (props.staticOnly) {
return (
<StatusBadge
variant='neutral'
size='sm'
className={cn(
'h-5! max-w-none shrink-0 whitespace-nowrap! border-current/20 [&_[data-slot=status-badge-label]]:whitespace-nowrap!',
colorClassName,
props.className
)}
>
{content}
</StatusBadge>
)
}
return (
<StatusBadge
copyText={props.modelName}
<CopyableStatusBadge
value={props.modelName}
variant='neutral'
size='sm'
showDot={!provider}
autoColor={provider ? undefined : props.modelName}
className={cn(
'border-border/60 bg-muted/30 h-6 max-w-none gap-1.5 rounded-md border px-2',
provider && 'text-foreground',
'h-5! max-w-none shrink-0 whitespace-nowrap! border-current/20 [&_[data-slot=status-badge-label]]:whitespace-nowrap!',
colorClassName,
props.className
)}
>
<span className='flex max-w-none items-center gap-1.5'>
{provider && (
<span
className='flex h-[18px] w-[18px] shrink-0 items-center justify-center'
title={provider.label}
aria-label={provider.label}
>
{getLobeIcon(provider.icon, 18)}
</span>
)}
<span className='whitespace-nowrap'>{props.modelName}</span>
</span>
</StatusBadge>
{content}
</CopyableStatusBadge>
)
}
@@ -157,7 +182,7 @@ export function ModelBadge(props: ModelBadgeProps) {
const { t } = useTranslation()
if (!props.actualModel) {
return <ModelBadgeContent {...props} />
return <ModelBadgeContent {...props} staticOnly={false} />
}
return (
@@ -167,7 +192,7 @@ export function ModelBadge(props: ModelBadgeProps) {
<button type='button' className='inline-flex items-center gap-1' />
}
>
<ModelBadgeContent {...props} />
<ModelBadgeContent {...props} staticOnly />
<Route className='text-muted-foreground size-3 shrink-0' />
</PopoverTrigger>
<PopoverContent className='w-72'>
@@ -176,17 +201,21 @@ export function ModelBadge(props: ModelBadgeProps) {
<span className='text-muted-foreground text-xs'>
{t('Request Model:')}
</span>
<span className='truncate font-mono text-xs font-medium'>
{props.modelName}
</span>
<ModelBadgeContent
modelName={props.modelName}
staticOnly
className='max-w-[11rem]'
/>
</div>
<div className='flex items-start justify-between gap-3'>
<span className='text-muted-foreground text-xs'>
{t('Actual Model:')}
</span>
<span className='truncate font-mono text-xs font-medium'>
{props.actualModel}
</span>
<ModelBadgeContent
modelName={props.actualModel}
staticOnly
className='max-w-[11rem]'
/>
</div>
</div>
</PopoverContent>
@@ -16,29 +16,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { flexRender, type Cell, type Table } from '@tanstack/react-table'
import { Database } from 'lucide-react'
import {
flexRender,
type Cell,
type Row,
type Table,
} from '@tanstack/react-table'
import { useTranslation } from 'react-i18next'
import {
dotColorMap,
textColorMap,
type StatusVariant,
} from '@/components/status-badge'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { Skeleton } from '@/components/ui/skeleton'
import { formatTimestampToDate } from '@/lib/format'
DataTableCardDetails,
DataTableCardField,
MobileCardList,
} from '@/components/data-table'
import { cn } from '@/lib/utils'
import { LOG_TYPE_ENUM } from '../constants'
import { getLogTypeConfig } from '../lib/utils'
import type { LogCategory } from '../types'
const logTypeRowTint: Record<number, string> = {
[LOG_TYPE_ENUM.ERROR]: 'bg-destructive/5 border-destructive/20',
@@ -50,268 +43,106 @@ interface UsageLogsMobileListProps<TData> {
isLoading?: boolean
emptyTitle?: string
emptyDescription?: string
logCategory: LogCategory
getRowClassName?: (row: Row<TData>) => string | undefined
}
function UsageLogsMobileSkeleton() {
return (
<div className='border-border/50 bg-card overflow-hidden rounded-lg border'>
{[1, 2, 3].map((i) => (
<div
key={i}
className='border-border/40 space-y-2.5 border-b p-3 last:border-b-0'
>
<div className='flex items-center justify-between gap-3'>
<Skeleton className='h-5 w-40 rounded-md' />
<Skeleton className='h-5 w-16 rounded-md' />
</div>
<div className='grid grid-cols-2 gap-x-4 gap-y-2'>
{[1, 2, 3, 4, 5, 6].map((j) => (
<div key={j} className='min-w-0 space-y-1'>
<Skeleton className='h-3 w-10 rounded' />
<Skeleton className='h-4 w-full rounded' />
</div>
))}
</div>
</div>
))}
</div>
)
type CardRole = 'title' | 'badge' | 'primary' | 'secondary' | 'hidden'
function getCardRole<TData>(cell: Cell<TData, unknown>): CardRole {
return cell.column.columnDef.meta?.cardRole ?? 'primary'
}
function CompactCell<TData>({
cell,
fallback = '-',
className,
primaryOnly = false,
}: {
cell?: Cell<TData, unknown>
fallback?: string
className?: string
primaryOnly?: boolean
}) {
return (
<div
className={cn(
'min-w-0 overflow-hidden leading-tight [&_button]:max-w-full [&_span]:max-w-full',
primaryOnly &&
'[&_.flex-col]:min-w-0 [&_.flex-col>*:not(:first-child)]:hidden',
className
)}
>
{cell ? (
flexRender(cell.column.columnDef.cell, cell.getContext())
) : (
<span className='text-muted-foreground/50'>{fallback}</span>
)}
</div>
)
function getCardLabel<TData>(cell: Cell<TData, unknown>): string {
const metaLabel = cell.column.columnDef.meta?.label
if (metaLabel) return metaLabel
const header = cell.column.columnDef.header
return typeof header === 'string' ? header : cell.column.id
}
function SummaryField<TData>({
label,
cell,
className,
valueClassName,
primaryOnly = false,
}: {
label: string
cell?: Cell<TData, unknown>
function orderCardCells<TData>(
cells: Cell<TData, unknown>[]
): Cell<TData, unknown>[] {
return [...cells].sort((a, b) => {
const aOrder = a.column.columnDef.meta?.cardOrder
const bOrder = b.column.columnDef.meta?.cardOrder
if (aOrder == null && bOrder == null) return 0
if (aOrder == null) return 1
if (bOrder == null) return -1
return aOrder - bOrder
})
}
function CardCellField<TData>(props: {
cell: Cell<TData, unknown>
hideLabel?: boolean
className?: string
valueClassName?: string
primaryOnly?: boolean
}) {
if (!cell) return null
const meta = props.cell.column.columnDef.meta
return (
<div
className={cn('bg-muted/20 min-w-0 rounded-md px-2 py-1.5', className)}
<DataTableCardField
label={props.hideLabel ? undefined : getCardLabel(props.cell)}
contentMode={meta?.contentMode}
span={meta?.cardSpan}
className={props.className}
valueClassName={props.valueClassName}
>
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
{label}
</div>
<CompactCell
cell={cell}
primaryOnly={primaryOnly}
className={valueClassName}
/>
</div>
{flexRender(props.cell.column.columnDef.cell, props.cell.getContext())}
</DataTableCardField>
)
}
function MobileLogTimeStatus({
createdAt,
type,
}: {
createdAt: unknown
type: unknown
}) {
const { t } = useTranslation()
const timestamp = typeof createdAt === 'number' ? createdAt : undefined
const logType = typeof type === 'number' ? type : undefined
const config = getLogTypeConfig(logType ?? LOG_TYPE_ENUM.UNKNOWN)
const variant = config.color as StatusVariant
return (
<div className='space-y-1'>
<div className='text-xs leading-tight tabular-nums'>
{formatTimestampToDate(timestamp)}
</div>
<div
className={cn(
'inline-flex items-center gap-1 text-xs leading-none font-medium',
textColorMap[variant]
)}
>
<span
className={cn('size-1.5 shrink-0 rounded-full', dotColorMap[variant])}
aria-hidden='true'
/>
<span>{t(config.label)}</span>
</div>
</div>
function UsageLogCard<TData>(props: { cells: Cell<TData, unknown>[] }) {
const titleCell = props.cells.find((cell) => getCardRole(cell) === 'title')
const badgeCell = props.cells.find((cell) => getCardRole(cell) === 'badge')
const primaryCells = orderCardCells(
props.cells.filter((cell) => getCardRole(cell) === 'primary')
)
const secondaryCells = orderCardCells(
props.cells.filter((cell) => getCardRole(cell) === 'secondary')
)
}
function CommonLogsCard<TData>({
cells,
}: {
cells: Map<string, Cell<TData, unknown>>
}) {
const { t } = useTranslation()
const modelCell = cells.get('model_name')
const quotaCell = cells.get('quota')
const rowData = cells.get('created_at')?.row.original as
| Record<string, unknown>
| undefined
return (
<div className='space-y-2.5'>
<div className='flex min-w-0 items-center justify-between gap-3'>
<CompactCell cell={modelCell} className='flex-1' />
<CompactCell
cell={quotaCell}
className='shrink-0 text-right [&_.flex-col]:items-end'
/>
</div>
<div className='grid grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)] gap-1.5'>
<div className='bg-muted/20 min-w-0 rounded-md px-2 py-1.5'>
<div className='text-muted-foreground mb-1 text-xs leading-none font-medium select-none'>
{t('Time')}
</div>
<MobileLogTimeStatus
createdAt={rowData?.created_at}
type={rowData?.type}
/>
<>
{(titleCell || badgeCell) && (
<div className='flex min-w-0 items-start justify-between gap-3'>
{titleCell && (
<CardCellField
cell={titleCell}
hideLabel
className='min-w-0 flex-1'
valueClassName='font-medium'
/>
)}
{badgeCell && (
<CardCellField
cell={badgeCell}
hideLabel
className='max-w-1/2 shrink text-right'
valueClassName='flex justify-end text-right tabular-nums'
/>
)}
</div>
<SummaryField
label={t('Channel')}
cell={cells.get('channel')}
valueClassName='[&_.flex-col]:max-w-none'
/>
<SummaryField label={t('User')} cell={cells.get('user')} primaryOnly />
<SummaryField
label={t('Token')}
cell={cells.get('token_name')}
valueClassName='[&_.flex-col]:max-w-none [&_.flex-col>*:not(:first-child)]:text-xs [&_.flex-col>*:not(:first-child)]:leading-none'
/>
<SummaryField
label={t('Timing')}
cell={cells.get('use_time')}
primaryOnly
/>
<SummaryField
label={t('Tokens')}
cell={cells.get('prompt_tokens')}
primaryOnly
/>
<SummaryField
label={t('Details')}
cell={cells.get('content')}
className='col-span-2 bg-transparent px-0 py-0'
/>
</div>
</div>
)
}
)}
function TaskLogsCard<TData>({
cells,
}: {
cells: Map<string, Cell<TData, unknown>>
}) {
const { t } = useTranslation()
{primaryCells.length > 0 && (
<div className='mt-2 grid grid-cols-2 gap-x-3 gap-y-2'>
{primaryCells.map((cell) => (
<CardCellField key={cell.id} cell={cell} />
))}
</div>
)}
const taskIdCell = cells.get('task_id')
const statusCell = cells.get('status')
const submitTimeCell = cells.get('submit_time')
return (
<div className='space-y-2.5'>
<div className='flex min-w-0 items-start justify-between gap-3'>
<CompactCell cell={taskIdCell} className='flex-1' />
<CompactCell cell={statusCell} className='shrink-0 text-right' />
</div>
<div className='grid grid-cols-2 gap-1.5'>
<SummaryField label={t('Submit Time')} cell={submitTimeCell} />
<SummaryField label={t('User')} cell={cells.get('user')} primaryOnly />
<SummaryField
label={t('Result')}
cell={cells.get('fail_reason')}
className='col-span-2 bg-transparent px-0 py-0'
/>
</div>
</div>
)
}
function DrawingLogsCard<TData>({
cells,
}: {
cells: Map<string, Cell<TData, unknown>>
}) {
const { t } = useTranslation()
const actionCell = cells.get('action')
const codeCell = cells.get('code')
const submitTimeCell = cells.get('submit_time')
return (
<div className='space-y-2.5'>
<div className='flex min-w-0 items-start justify-between gap-3'>
<CompactCell cell={actionCell} className='flex-1' />
<CompactCell cell={codeCell} className='shrink-0 text-right' />
</div>
<div className='grid grid-cols-2 gap-1.5'>
<SummaryField label={t('Submit Time')} cell={submitTimeCell} />
<SummaryField
label={t('Channel')}
cell={cells.get('channel')}
primaryOnly
/>
<SummaryField label={t('Task ID')} cell={cells.get('mj_id')} />
<SummaryField
label={t('Duration')}
cell={cells.get('duration')}
primaryOnly
/>
<SummaryField label={t('Image')} cell={cells.get('image_url')} />
<SummaryField
label={t('Prompt')}
cell={cells.get('prompt')}
primaryOnly
/>
<SummaryField
label={t('Fail Reason')}
cell={cells.get('fail_reason')}
className='col-span-2 bg-transparent px-0 py-0'
/>
</div>
</div>
{secondaryCells.length > 0 && (
<DataTableCardDetails count={secondaryCells.length}>
{secondaryCells.map((cell) => (
<CardCellField key={cell.id} cell={cell} />
))}
</DataTableCardDetails>
)}
</>
)
}
@@ -320,7 +151,7 @@ export function UsageLogsMobileList<TData>({
isLoading = false,
emptyTitle,
emptyDescription,
logCategory,
getRowClassName,
}: UsageLogsMobileListProps<TData>) {
const { t } = useTranslation()
@@ -329,54 +160,24 @@ export function UsageLogsMobileList<TData>({
emptyDescription ??
t('No usage logs available. Logs will appear here once API calls are made.')
if (isLoading) {
return <UsageLogsMobileSkeleton />
}
const rows = table.getRowModel().rows
if (!rows || rows.length === 0) {
return (
<div className='rounded-lg border p-6'>
<Empty className='border-none p-0'>
<EmptyHeader>
<EmptyMedia variant='icon'>
<Database className='size-6' />
</EmptyMedia>
<EmptyTitle>{resolvedEmptyTitle}</EmptyTitle>
<EmptyDescription>{resolvedEmptyDescription}</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
)
}
return (
<div className='border-border/50 bg-card overflow-hidden rounded-lg border'>
{rows.map((row) => {
const cells = new Map(
row.getVisibleCells().map((cell) => [cell.column.id, cell])
)
<MobileCardList
table={table}
isLoading={isLoading}
emptyTitle={resolvedEmptyTitle}
emptyDescription={resolvedEmptyDescription}
renderCard={(row) => <UsageLogCard cells={row.getVisibleCells()} />}
getRowClassName={(row) => {
const logType = (row.original as Record<string, unknown>).type as
| number
| undefined
const tintClass = logType != null ? (logTypeRowTint[logType] ?? '') : ''
return (
<div
key={row.id}
className={cn(
'border-border/40 border-b border-l-2 border-l-transparent p-3 transition-colors last:border-b-0',
tintClass
)}
>
{logCategory === 'common' && <CommonLogsCard cells={cells} />}
{logCategory === 'task' && <TaskLogsCard cells={cells} />}
{logCategory === 'drawing' && <DrawingLogsCard cells={cells} />}
</div>
return cn(
'border-l-2 border-l-transparent transition-colors',
tintClass,
getRowClassName?.(row)
)
})}
</div>
}}
/>
)
}
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useQuery } from '@tanstack/react-query'
import { getRouteApi } from '@tanstack/react-router'
import { type ColumnDef } from '@tanstack/react-table'
import type { ColumnDef } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
@@ -27,7 +27,6 @@ import {
DataTableRow,
useDataTable,
} from '@/components/data-table'
import { useMediaQuery } from '@/hooks'
import { useIsAdmin } from '@/hooks/use-admin'
import { useTableUrlState } from '@/hooks/use-table-url-state'
import { cn } from '@/lib/utils'
@@ -64,7 +63,12 @@ function getColumnVisibilityStorageKey(
}
function deserializeLogTypeFilter(value: unknown): unknown[] {
const values = Array.isArray(value) ? value : value ? [value] : []
let values: unknown[] = []
if (Array.isArray(value)) {
values = value
} else if (value) {
values = [value]
}
return values.filter((item) => String(item) !== LOG_TYPE_ALL_VALUE)
}
@@ -75,7 +79,6 @@ interface UsageLogsTableProps {
export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
const { t } = useTranslation()
const isAdmin = useIsAdmin()
const isMobile = useMediaQuery('(max-width: 640px)')
const searchParams = route.useSearch()
const {
@@ -87,7 +90,11 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
} = useTableUrlState({
search: route.useSearch(),
navigate: route.useNavigate(),
pagination: { defaultPage: 1, defaultPageSize: isMobile ? 20 : 100 },
pagination: {
defaultPage: 1,
defaultPageSize: 20,
pageSizeStorageKey: `usage-logs:${logCategory}:${isAdmin ? 'admin' : 'user'}:page-size:v1`,
},
globalFilter: { enabled: false },
columnFilters: [
{
@@ -180,6 +187,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
<DataTablePage
table={table}
columns={columns as ColumnDef<Record<string, unknown>>[]}
tableLabel={t('Usage Logs')}
isLoading={isLoadingData}
isFetching={isFetching}
emptyTitle={t('No Logs Found')}
@@ -195,7 +203,15 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
<UsageLogsMobileList
table={table}
isLoading={isLoadingData}
logCategory={logCategory}
getRowClassName={(row) => {
if (!isCommon || !isAdmin) return undefined
const other = parseLogOther(
((row.original as Record<string, unknown>).other as string) ?? ''
)
return other?.admin_info?.quota_saturation
? quotaSaturationRowTint
: undefined
}}
/>
}
toolbar={
@@ -205,7 +221,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
<TaskLogsFilterBar table={table} logCategory={logCategory} />
)
}
renderRow={(row) => {
renderRow={(row, helpers) => {
const logType = (row.original as Record<string, unknown>).type as
| number
| undefined
@@ -225,7 +241,9 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
key={row.id}
row={row}
className={cn('transition-colors', tintClass)}
getColumnClassName={() => (isCommon ? 'py-2' : 'py-3.5')}
getColumnClassName={(columnId) =>
helpers.getCellClassName(columnId, isCommon ? 'py-2' : 'py-3.5')
}
/>
)
}}