feat(logs): enhance usage logs table with log type indicators and improve UI elements
This commit is contained in:
parent
22ef5b2f80
commit
db48108d21
@ -19,7 +19,7 @@ import {
|
|||||||
type DataTableColumnHeaderProps<TData, TValue> =
|
type DataTableColumnHeaderProps<TData, TValue> =
|
||||||
React.HTMLAttributes<HTMLDivElement> & {
|
React.HTMLAttributes<HTMLDivElement> & {
|
||||||
column: Column<TData, TValue>
|
column: Column<TData, TValue>
|
||||||
title: string
|
title: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableColumnHeader<TData, TValue>({
|
export function DataTableColumnHeader<TData, TValue>({
|
||||||
|
|||||||
24
web/default/src/components/status-badge.tsx
vendored
24
web/default/src/components/status-badge.tsx
vendored
@ -69,7 +69,7 @@ export interface StatusBadgeProps extends Omit<
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
icon?: LucideIcon
|
icon?: LucideIcon
|
||||||
pulse?: boolean
|
pulse?: boolean
|
||||||
/** @deprecated Dot is always shown in flat design */
|
/** When false, hides the leading dot */
|
||||||
showDot?: boolean
|
showDot?: boolean
|
||||||
variant?: StatusVariant | null
|
variant?: StatusVariant | null
|
||||||
size?: 'sm' | 'md' | 'lg' | null
|
size?: 'sm' | 'md' | 'lg' | null
|
||||||
@ -87,7 +87,7 @@ export function StatusBadge({
|
|||||||
variant,
|
variant,
|
||||||
size = 'sm',
|
size = 'sm',
|
||||||
pulse = false,
|
pulse = false,
|
||||||
showDot: _showDot,
|
showDot = true,
|
||||||
rounded: _rounded,
|
rounded: _rounded,
|
||||||
copyable = true,
|
copyable = true,
|
||||||
copyText,
|
copyText,
|
||||||
@ -110,7 +110,7 @@ export function StatusBadge({
|
|||||||
onClick?.(e)
|
onClick?.(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = children ?? (label ? <span>{label}</span> : null)
|
const content = children ?? (label ? <span className='truncate'>{label}</span> : null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@ -127,14 +127,16 @@ export function StatusBadge({
|
|||||||
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined}
|
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span
|
{showDot && (
|
||||||
className={cn(
|
<span
|
||||||
'inline-block size-1.5 shrink-0 rounded-full',
|
className={cn(
|
||||||
dotColorMap[computedVariant]
|
'inline-block size-1.5 shrink-0 rounded-full',
|
||||||
)}
|
dotColorMap[computedVariant]
|
||||||
aria-hidden='true'
|
)}
|
||||||
/>
|
aria-hidden='true'
|
||||||
{Icon && <Icon className='h-3 w-3' />}
|
/>
|
||||||
|
)}
|
||||||
|
{Icon && <Icon className='size-3 shrink-0' />}
|
||||||
{content}
|
{content}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { type ColumnDef } from '@tanstack/react-table'
|
import { type ColumnDef } from '@tanstack/react-table'
|
||||||
import { Route, CircleAlert, Sparkles } from 'lucide-react'
|
import { Route, CircleAlert, Sparkles, KeyRound } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
|
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
|
||||||
import {
|
import {
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
} from '@/lib/format'
|
} from '@/lib/format'
|
||||||
import { getAvatarColorClass } from '@/lib/colors'
|
import { getAvatarColorClass } from '@/lib/colors'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@ -371,7 +370,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
} = useUsageLogsContext()
|
} = useUsageLogsContext()
|
||||||
const log = row.original
|
const log = row.original
|
||||||
|
|
||||||
if (!isDisplayableLogType(log.type) || !log.username) return null
|
if (!log.username) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -385,7 +384,7 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex size-5 items-center justify-center rounded-full text-[11px] font-bold',
|
'flex size-6 items-center justify-center rounded-full text-xs font-bold ring-1 ring-border/60 saturate-[1.2] brightness-95 dark:brightness-110',
|
||||||
sensitiveVisible
|
sensitiveVisible
|
||||||
? getAvatarColorClass(log.username)
|
? getAvatarColorClass(log.username)
|
||||||
: 'bg-muted text-muted-foreground'
|
: 'bg-muted text-muted-foreground'
|
||||||
@ -404,6 +403,39 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
columns.push({
|
||||||
|
accessorKey: 'token_name',
|
||||||
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title={t('Token')} />
|
||||||
|
),
|
||||||
|
cell: function TokenNameCell({ row }) {
|
||||||
|
const { sensitiveVisible } = useUsageLogsContext()
|
||||||
|
const log = row.original
|
||||||
|
if (!isDisplayableLogType(log.type)) return null
|
||||||
|
|
||||||
|
const tokenName = log.token_name
|
||||||
|
if (!tokenName) return null
|
||||||
|
|
||||||
|
const displayName = sensitiveVisible ? tokenName : '••••'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='max-w-[120px]'>
|
||||||
|
<StatusBadge
|
||||||
|
label={displayName}
|
||||||
|
icon={KeyRound}
|
||||||
|
autoColor={tokenName}
|
||||||
|
copyText={sensitiveVisible ? tokenName : undefined}
|
||||||
|
size='sm'
|
||||||
|
showDot={false}
|
||||||
|
className='max-w-full overflow-hidden rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
meta: { label: t('Token') },
|
||||||
|
size: 130,
|
||||||
|
})
|
||||||
|
|
||||||
columns.push(
|
columns.push(
|
||||||
{
|
{
|
||||||
accessorKey: 'model_name',
|
accessorKey: 'model_name',
|
||||||
@ -416,30 +448,29 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
if (!isDisplayableLogType(log.type)) return null
|
if (!isDisplayableLogType(log.type)) return null
|
||||||
|
|
||||||
const modelInfo = formatModelName(log)
|
const modelInfo = formatModelName(log)
|
||||||
const tokenName = log.token_name
|
|
||||||
const other = parseLogOther(log.other)
|
const other = parseLogOther(log.other)
|
||||||
let group = log.group
|
let group = log.group
|
||||||
if (!group) group = other?.group || ''
|
if (!group) group = other?.group || ''
|
||||||
|
|
||||||
|
const badgeClass =
|
||||||
|
'truncate rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 font-mono'
|
||||||
|
|
||||||
const modelBadge = modelInfo.isMapped ? (
|
const modelBadge = modelInfo.isMapped ? (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<button
|
||||||
variant='ghost'
|
type='button'
|
||||||
size='sm'
|
className='inline-flex items-center gap-1'
|
||||||
className='h-auto p-0 hover:bg-transparent'
|
|
||||||
>
|
>
|
||||||
<span className='flex items-center gap-1'>
|
<StatusBadge
|
||||||
<StatusBadge
|
label={modelInfo.name}
|
||||||
label={modelInfo.name}
|
autoColor={modelInfo.name}
|
||||||
autoColor={modelInfo.name}
|
copyText={modelInfo.name}
|
||||||
copyText={modelInfo.name}
|
size='sm'
|
||||||
size='sm'
|
className={badgeClass}
|
||||||
className='truncate font-mono'
|
/>
|
||||||
/>
|
<Route className='text-muted-foreground size-3 shrink-0' />
|
||||||
<Route className='text-muted-foreground size-3 shrink-0' />
|
</button>
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className='w-72'>
|
<PopoverContent className='w-72'>
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
@ -468,12 +499,11 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
autoColor={modelInfo.name}
|
autoColor={modelInfo.name}
|
||||||
copyText={modelInfo.name}
|
copyText={modelInfo.name}
|
||||||
size='sm'
|
size='sm'
|
||||||
className='truncate font-mono'
|
className={badgeClass}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const metaParts: string[] = []
|
const metaParts: string[] = []
|
||||||
if (tokenName) metaParts.push(sensitiveVisible ? tokenName : '••••')
|
|
||||||
if (group) metaParts.push(sensitiveVisible ? group : '••••')
|
if (group) metaParts.push(sensitiveVisible ? group : '••••')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -505,41 +535,65 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
const timeVariant = getTimeColor(useTime)
|
const timeVariant = getTimeColor(useTime)
|
||||||
const frtVariant = frt ? getTimeColor(frt / 1000) : null
|
const frtVariant = frt ? getTimeColor(frt / 1000) : null
|
||||||
|
|
||||||
|
const pillBg: Record<string, string> = {
|
||||||
|
success:
|
||||||
|
'border border-emerald-200/60 bg-emerald-50/50 dark:border-emerald-800/50 dark:bg-emerald-950/20',
|
||||||
|
info: 'border border-sky-200/60 bg-sky-50/50 dark:border-sky-800/50 dark:bg-sky-950/20',
|
||||||
|
warning:
|
||||||
|
'border border-amber-200/60 bg-amber-50/50 dark:border-amber-800/50 dark:bg-amber-950/20',
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-0.5'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='flex items-center gap-1 text-xs'>
|
<div className='flex items-center gap-1.5'>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'size-1.5 shrink-0 rounded-full',
|
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
|
||||||
dotColorMap[timeVariant]
|
pillBg[timeVariant],
|
||||||
)}
|
|
||||||
aria-hidden='true'
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'font-mono font-medium',
|
|
||||||
textColorMap[timeVariant]
|
textColorMap[timeVariant]
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'size-1.5 shrink-0 rounded-full',
|
||||||
|
dotColorMap[timeVariant]
|
||||||
|
)}
|
||||||
|
aria-hidden='true'
|
||||||
|
/>
|
||||||
{formatUseTime(useTime)}
|
{formatUseTime(useTime)}
|
||||||
</span>
|
</span>
|
||||||
{log.is_stream && frt != null && frt > 0 && (
|
{log.is_stream && (frt != null && frt > 0 ? (
|
||||||
<>
|
<span
|
||||||
<span className='text-muted-foreground/30'>·</span>
|
className={cn(
|
||||||
<span
|
'inline-flex items-center rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
|
||||||
className={cn(
|
pillBg[frtVariant!],
|
||||||
'font-mono font-medium',
|
textColorMap[frtVariant!]
|
||||||
textColorMap[frtVariant!]
|
)}
|
||||||
)}
|
>
|
||||||
>
|
{formatUseTime(frt / 1000)}
|
||||||
{formatUseTime(frt / 1000)}
|
</span>
|
||||||
</span>
|
) : (
|
||||||
</>
|
<span className='inline-flex items-center rounded-md border border-border/60 px-1.5 py-0.5 text-[11px] text-muted-foreground/50'>
|
||||||
)}
|
N/A
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-1 text-[11px]'>
|
<div className='flex items-center gap-1 text-[11px]'>
|
||||||
<span className='text-muted-foreground/60'>
|
<span className='text-muted-foreground/60'>
|
||||||
{log.is_stream ? t('Stream') : t('Non-stream')}
|
{log.is_stream ? t('Stream') : t('Non-stream')}
|
||||||
|
{useTime > 0 && (log.prompt_tokens + log.completion_tokens) > 0 && (
|
||||||
|
<>
|
||||||
|
{' · '}
|
||||||
|
<span className='font-mono tabular-nums'>
|
||||||
|
{Math.round(
|
||||||
|
(log.is_stream
|
||||||
|
? log.completion_tokens
|
||||||
|
: log.prompt_tokens + log.completion_tokens) / useTime
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
{' t/s'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{log.is_stream &&
|
{log.is_stream &&
|
||||||
other?.stream_status &&
|
other?.stream_status &&
|
||||||
@ -583,9 +637,6 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
if (!isDisplayableLogType(log.type)) return null
|
if (!isDisplayableLogType(log.type)) return null
|
||||||
|
|
||||||
const other = parseLogOther(log.other)
|
const other = parseLogOther(log.other)
|
||||||
if (isPerCallBilling(other?.model_price)) {
|
|
||||||
return <span className='text-muted-foreground text-xs'>-</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
const promptTokens = log.prompt_tokens || 0
|
const promptTokens = log.prompt_tokens || 0
|
||||||
const completionTokens = log.completion_tokens || 0
|
const completionTokens = log.completion_tokens || 0
|
||||||
@ -600,24 +651,25 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
const cacheWriteTokens = hasSplitCache
|
const cacheWriteTokens = hasSplitCache
|
||||||
? cacheWrite5m + cacheWrite1h
|
? cacheWrite5m + cacheWrite1h
|
||||||
: other?.cache_creation_tokens || 0
|
: other?.cache_creation_tokens || 0
|
||||||
const cacheSegments = [
|
|
||||||
cacheReadTokens > 0
|
|
||||||
? `${t('Cache')}读 ${cacheReadTokens.toLocaleString()}`
|
|
||||||
: null,
|
|
||||||
cacheWriteTokens > 0
|
|
||||||
? `写 ${cacheWriteTokens.toLocaleString()}`
|
|
||||||
: null,
|
|
||||||
].filter(Boolean)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-0.5'>
|
<div className='flex flex-col gap-0.5'>
|
||||||
<span className='font-mono text-xs font-medium'>
|
<span className='font-mono text-xs font-medium tabular-nums'>
|
||||||
{promptTokens.toLocaleString()} / {completionTokens.toLocaleString()}
|
{promptTokens.toLocaleString()} / {completionTokens.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
{cacheSegments.length > 0 && (
|
{(cacheReadTokens > 0 || cacheWriteTokens > 0) && (
|
||||||
<span className='text-muted-foreground/60 text-[11px]'>
|
<div className='flex items-center gap-1 text-[11px]'>
|
||||||
{cacheSegments.join(' · ')}
|
{cacheReadTokens > 0 && (
|
||||||
</span>
|
<span className='text-muted-foreground/60'>
|
||||||
|
{t('Cache')}↓ {cacheReadTokens.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{cacheWriteTokens > 0 && (
|
||||||
|
<span className='text-muted-foreground/60'>
|
||||||
|
↑ {cacheWriteTokens.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -643,14 +695,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div>
|
<span className='inline-flex items-center gap-1 rounded-md border border-emerald-200 bg-emerald-50 px-1.5 py-0.5 text-xs font-medium text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300'>
|
||||||
<StatusBadge
|
<span className='size-1.5 rounded-full bg-emerald-500' aria-hidden='true' />
|
||||||
label={t('Subscription')}
|
{t('Subscription')}
|
||||||
variant='green'
|
</span>
|
||||||
size='sm'
|
|
||||||
copyable={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<span>
|
<span>
|
||||||
@ -662,10 +710,12 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quotaStr = formatLogQuota(quota)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col gap-0.5'>
|
||||||
<span className='font-mono text-xs font-medium tabular-nums'>
|
<span className='border-border/80 inline-flex w-fit items-center rounded-md border bg-muted/60 px-1.5 py-0.5 font-mono text-xs font-semibold tabular-nums'>
|
||||||
{formatLogQuota(quota)}
|
{quotaStr}
|
||||||
</span>
|
</span>
|
||||||
{(() => {
|
{(() => {
|
||||||
const userGroupRatio = other?.user_group_ratio
|
const userGroupRatio = other?.user_group_ratio
|
||||||
@ -705,39 +755,43 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
const other = parseLogOther(log.other)
|
const other = parseLogOther(log.other)
|
||||||
|
|
||||||
const segments = buildDetailSegments(log, other, t)
|
const segments = buildDetailSegments(log, other, t)
|
||||||
|
const primary = segments[0]
|
||||||
|
const hasMore = segments.length > 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<button
|
||||||
variant='ghost'
|
type='button'
|
||||||
className='h-auto max-w-[220px] justify-start p-0 text-left text-xs font-normal hover:underline'
|
className='group flex max-w-[200px] items-center gap-1 text-left text-xs'
|
||||||
onClick={() => setDialogOpen(true)}
|
onClick={() => setDialogOpen(true)}
|
||||||
title={t('Click to view full details')}
|
title={t('Click to view full details')}
|
||||||
>
|
>
|
||||||
{segments.length > 0 ? (
|
{primary ? (
|
||||||
<div className='flex flex-col gap-px'>
|
<span
|
||||||
{segments.map((seg, i) => (
|
className={cn(
|
||||||
<span
|
'truncate leading-snug group-hover:underline',
|
||||||
key={i}
|
primary.muted
|
||||||
className={cn(
|
? 'text-muted-foreground/60'
|
||||||
'leading-snug',
|
: primary.danger
|
||||||
seg.muted
|
? 'text-red-600 dark:text-red-400'
|
||||||
? 'text-muted-foreground/60'
|
: 'text-foreground'
|
||||||
: seg.danger
|
)}
|
||||||
? 'text-red-600 dark:text-red-400'
|
>
|
||||||
: 'text-foreground'
|
{primary.text}
|
||||||
)}
|
{hasMore && (
|
||||||
>
|
<span className='text-muted-foreground/40 ml-0.5'>
|
||||||
{seg.text}
|
+{segments.length - 1}
|
||||||
</span>
|
</span>
|
||||||
))}
|
)}
|
||||||
</div>
|
</span>
|
||||||
) : log.content ? (
|
) : log.content ? (
|
||||||
<span className='line-clamp-2'>{log.content}</span>
|
<span className='text-muted-foreground truncate group-hover:underline'>
|
||||||
|
{log.content}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className='text-muted-foreground'>-</span>
|
<span className='text-muted-foreground/40'>—</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</button>
|
||||||
<DetailsDialog
|
<DetailsDialog
|
||||||
log={log}
|
log={log}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
@ -748,8 +802,8 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
meta: { label: t('Details'), mobileHidden: true },
|
meta: { label: t('Details'), mobileHidden: true },
|
||||||
size: 200,
|
size: 180,
|
||||||
maxSize: 220,
|
maxSize: 200,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, type ReactNode } from 'react'
|
import { useState, useEffect, useCallback, type ReactNode } from 'react'
|
||||||
import { useNavigate, getRouteApi } from '@tanstack/react-router'
|
import { useNavigate, getRouteApi } from '@tanstack/react-router'
|
||||||
import { ChevronDown, Eye, EyeOff, RotateCcw, Search } from 'lucide-react'
|
import { useQueryClient, useIsFetching } from '@tanstack/react-query'
|
||||||
|
import { ChevronDown, Eye, EyeOff, Loader2, RotateCcw, Search } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useIsAdmin } from '@/hooks/use-admin'
|
import { useIsAdmin } from '@/hooks/use-admin'
|
||||||
@ -33,9 +34,11 @@ export function CommonLogsFilterBar({
|
|||||||
}: CommonLogsFilterBarProps) {
|
}: CommonLogsFilterBarProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const searchParams = route.useSearch()
|
const searchParams = route.useSearch()
|
||||||
const isAdmin = useIsAdmin()
|
const isAdmin = useIsAdmin()
|
||||||
const { sensitiveVisible, setSensitiveVisible } = useUsageLogsContext()
|
const { sensitiveVisible, setSensitiveVisible } = useUsageLogsContext()
|
||||||
|
const fetchingLogs = useIsFetching({ queryKey: ['logs'] })
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [filters, setFilters] = useState<CommonLogFilters>(() => {
|
const [filters, setFilters] = useState<CommonLogFilters>(() => {
|
||||||
@ -88,14 +91,15 @@ export function CommonLogsFilterBar({
|
|||||||
navigate({
|
navigate({
|
||||||
to: '/usage-logs/$section',
|
to: '/usage-logs/$section',
|
||||||
params: { section: 'common' },
|
params: { section: 'common' },
|
||||||
search: (prev: Record<string, unknown>) => ({
|
search: {
|
||||||
...prev,
|
|
||||||
...filterParams,
|
...filterParams,
|
||||||
...(logType ? { type: [logType] } : { type: undefined }),
|
...(logType ? { type: [logType] } : {}),
|
||||||
page: 1,
|
page: 1,
|
||||||
}),
|
},
|
||||||
})
|
})
|
||||||
}, [filters, logType, navigate])
|
queryClient.invalidateQueries({ queryKey: ['logs'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['usage-logs-stats'] })
|
||||||
|
}, [filters, logType, navigate, queryClient])
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
const { start, end } = getDefaultTimeRange()
|
const { start, end } = getDefaultTimeRange()
|
||||||
@ -112,7 +116,9 @@ export function CommonLogsFilterBar({
|
|||||||
endTime: end.getTime(),
|
endTime: end.getTime(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [navigate])
|
queryClient.invalidateQueries({ queryKey: ['logs'] })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['usage-logs-stats'] })
|
||||||
|
}, [navigate, queryClient])
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
@ -267,8 +273,12 @@ export function CommonLogsFilterBar({
|
|||||||
<RotateCcw className='size-3.5' />
|
<RotateCcw className='size-3.5' />
|
||||||
{t('Reset')}
|
{t('Reset')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size='sm' className='h-8' onClick={handleApply}>
|
<Button size='sm' className='h-8' onClick={handleApply} disabled={fetchingLogs > 0}>
|
||||||
<Search className='size-3.5' />
|
{fetchingLogs > 0 ? (
|
||||||
|
<Loader2 className='size-3.5 animate-spin' />
|
||||||
|
) : (
|
||||||
|
<Search className='size-3.5' />
|
||||||
|
)}
|
||||||
{t('Search')}
|
{t('Search')}
|
||||||
</Button>
|
</Button>
|
||||||
{viewOptions}
|
{viewOptions}
|
||||||
|
|||||||
@ -42,19 +42,19 @@ export function CommonLogsStats() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-1.5'>
|
<div className='flex items-center gap-2'>
|
||||||
<Skeleton className='h-6 w-[126px] rounded-md' />
|
<Skeleton className='h-7 w-[150px] rounded-md' />
|
||||||
<Skeleton className='h-6 w-[76px] rounded-md' />
|
<Skeleton className='h-7 w-[100px] rounded-md' />
|
||||||
<Skeleton className='h-6 w-[92px] rounded-md' />
|
<Skeleton className='h-7 w-[120px] rounded-md' />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagClass =
|
const tagClass =
|
||||||
'inline-flex h-6 items-center rounded-md border px-2.5 text-xs font-medium shadow-xs'
|
'inline-flex h-7 items-center rounded-md border px-3 py-1 text-xs font-medium shadow-xs'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-wrap items-center gap-1.5'>
|
<div className='flex flex-wrap items-center gap-2'>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
tagClass,
|
tagClass,
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import {
|
|||||||
MobileCardList,
|
MobileCardList,
|
||||||
} from '@/components/data-table'
|
} from '@/components/data-table'
|
||||||
import { PageFooterPortal } from '@/components/layout'
|
import { PageFooterPortal } from '@/components/layout'
|
||||||
import { DEFAULT_LOGS_DATA } from '../constants'
|
import { DEFAULT_LOGS_DATA, LOG_TYPE_ENUM } from '../constants'
|
||||||
import { useColumnsByCategory } from '../lib/columns'
|
import { useColumnsByCategory } from '../lib/columns'
|
||||||
import { fetchLogsByCategory } from '../lib/utils'
|
import { fetchLogsByCategory } from '../lib/utils'
|
||||||
import type { LogCategory } from '../types'
|
import type { LogCategory } from '../types'
|
||||||
@ -43,6 +43,20 @@ import { CommonLogsStats } from './common-logs-stats'
|
|||||||
|
|
||||||
const route = getRouteApi('/_authenticated/usage-logs/$section')
|
const route = getRouteApi('/_authenticated/usage-logs/$section')
|
||||||
|
|
||||||
|
const logTypeBorderColor: Record<number, string> = {
|
||||||
|
[LOG_TYPE_ENUM.TOPUP]: 'border-l-cyan-400 dark:border-l-cyan-500',
|
||||||
|
[LOG_TYPE_ENUM.CONSUME]: 'border-l-emerald-400 dark:border-l-emerald-500',
|
||||||
|
[LOG_TYPE_ENUM.MANAGE]: 'border-l-orange-400 dark:border-l-orange-500',
|
||||||
|
[LOG_TYPE_ENUM.SYSTEM]: 'border-l-purple-400 dark:border-l-purple-500',
|
||||||
|
[LOG_TYPE_ENUM.ERROR]: 'border-l-rose-400 dark:border-l-rose-500',
|
||||||
|
[LOG_TYPE_ENUM.REFUND]: 'border-l-blue-400 dark:border-l-blue-500',
|
||||||
|
}
|
||||||
|
|
||||||
|
const logTypeRowTint: Record<number, string> = {
|
||||||
|
[LOG_TYPE_ENUM.ERROR]: 'bg-rose-50/40 dark:bg-rose-950/20',
|
||||||
|
[LOG_TYPE_ENUM.REFUND]: 'bg-blue-50/30 dark:bg-blue-950/15',
|
||||||
|
}
|
||||||
|
|
||||||
interface UsageLogsTableProps {
|
interface UsageLogsTableProps {
|
||||||
logCategory: LogCategory
|
logCategory: LogCategory
|
||||||
}
|
}
|
||||||
@ -150,6 +164,42 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
|
|||||||
ensurePageInRange(pageCount)
|
ensurePageInRange(pageCount)
|
||||||
}, [pageCount, ensurePageInRange])
|
}, [pageCount, ensurePageInRange])
|
||||||
|
|
||||||
|
const isCommon = logCategory === 'common'
|
||||||
|
|
||||||
|
const renderRows = () => {
|
||||||
|
const rows = table.getRowModel().rows
|
||||||
|
if (rows.length === 0) return null
|
||||||
|
|
||||||
|
return rows.map((row) => {
|
||||||
|
const logType = (row.original as Record<string, unknown>).type as
|
||||||
|
| number
|
||||||
|
| undefined
|
||||||
|
const borderClass =
|
||||||
|
isCommon && logType != null
|
||||||
|
? logTypeBorderColor[logType] ?? 'border-l-transparent'
|
||||||
|
: ''
|
||||||
|
const tintClass =
|
||||||
|
isCommon && logType != null ? (logTypeRowTint[logType] ?? '') : ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
className={cn(
|
||||||
|
'!border-l-[3px] transition-colors',
|
||||||
|
borderClass,
|
||||||
|
tintClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className='py-2'>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
@ -184,9 +234,9 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader className='bg-muted/30 sticky top-0 z-10'>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id} className='border-l-[3px] border-l-transparent'>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
@ -212,18 +262,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
table.getRowModel().rows.map((row) => (
|
renderRows()
|
||||||
<TableRow key={row.id}>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<TableCell key={cell.id} className='py-2'>
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
1
web/default/src/lib/format.ts
vendored
1
web/default/src/lib/format.ts
vendored
@ -162,7 +162,6 @@ export function formatTokens(tokens: number): string {
|
|||||||
* Format use time in seconds with appropriate unit
|
* Format use time in seconds with appropriate unit
|
||||||
*/
|
*/
|
||||||
export function formatUseTime(seconds: number): string {
|
export function formatUseTime(seconds: number): string {
|
||||||
if (seconds < 1) return `${(seconds * 1000).toFixed(0)}ms`
|
|
||||||
if (seconds < 60) return `${seconds.toFixed(1)}s`
|
if (seconds < 60) return `${seconds.toFixed(1)}s`
|
||||||
const minutes = Math.floor(seconds / 60)
|
const minutes = Math.floor(seconds / 60)
|
||||||
const remainingSeconds = seconds % 60
|
const remainingSeconds = seconds % 60
|
||||||
|
|||||||
90
web/default/src/styles/theme.css
vendored
90
web/default/src/styles/theme.css
vendored
@ -1,31 +1,31 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.994 0.002 247.858);
|
||||||
--foreground: oklch(0.129 0.042 264.695);
|
--foreground: oklch(0.18 0.035 264.695);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(0.997 0.002 247.858);
|
||||||
--card-foreground: oklch(0.129 0.042 264.695);
|
--card-foreground: oklch(0.18 0.035 264.695);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(0.997 0.002 247.858);
|
||||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
--popover-foreground: oklch(0.18 0.035 264.695);
|
||||||
--primary: oklch(0.208 0.042 265.755);
|
--primary: oklch(0.255 0.042 265.755);
|
||||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
--primary-foreground: oklch(0.985 0.004 247.858);
|
||||||
--secondary: oklch(0.968 0.007 247.896);
|
--secondary: oklch(0.974 0.004 247.896);
|
||||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
--secondary-foreground: oklch(0.255 0.042 265.755);
|
||||||
--muted: oklch(0.968 0.007 247.896);
|
--muted: oklch(0.972 0.004 247.896);
|
||||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
--muted-foreground: oklch(0.49 0.04 257.417);
|
||||||
--accent: oklch(0.968 0.007 247.896);
|
--accent: oklch(0.972 0.004 247.896);
|
||||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
--accent-foreground: oklch(0.255 0.042 265.755);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.929 0.013 255.508);
|
--border: oklch(0.925 0.01 255.508);
|
||||||
--input: oklch(0.929 0.013 255.508);
|
--input: oklch(0.925 0.01 255.508);
|
||||||
--ring: oklch(0.704 0.04 256.788);
|
--ring: oklch(0.64 0.055 256.788);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: var(--background);
|
--sidebar: oklch(0.991 0.002 247.858);
|
||||||
--sidebar-foreground: var(--foreground);
|
--sidebar-foreground: var(--foreground);
|
||||||
--sidebar-primary: var(--primary);
|
--sidebar-primary: var(--primary);
|
||||||
--sidebar-primary-foreground: var(--primary-foreground);
|
--sidebar-primary-foreground: var(--primary-foreground);
|
||||||
@ -33,44 +33,44 @@
|
|||||||
--sidebar-accent-foreground: var(--accent-foreground);
|
--sidebar-accent-foreground: var(--accent-foreground);
|
||||||
--sidebar-border: var(--border);
|
--sidebar-border: var(--border);
|
||||||
--sidebar-ring: var(--ring);
|
--sidebar-ring: var(--ring);
|
||||||
--skeleton-base: oklch(0.943 0.003 264);
|
--skeleton-base: oklch(0.948 0.004 264);
|
||||||
--skeleton-highlight: oklch(0.985 0.001 264);
|
--skeleton-highlight: oklch(0.988 0.002 264);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.11 0 0);
|
--background: oklch(0.165 0.012 258);
|
||||||
--foreground: oklch(0.984 0.003 247.858);
|
--foreground: oklch(0.92 0.008 247.858);
|
||||||
--card: oklch(0.13 0 0);
|
--card: oklch(0.205 0.012 258);
|
||||||
--card-foreground: oklch(0.984 0.003 247.858);
|
--card-foreground: oklch(0.92 0.008 247.858);
|
||||||
--popover: oklch(0.15 0 0);
|
--popover: oklch(0.225 0.014 258);
|
||||||
--popover-foreground: oklch(0.984 0.003 247.858);
|
--popover-foreground: oklch(0.92 0.008 247.858);
|
||||||
--primary: oklch(0.929 0.013 255.508);
|
--primary: oklch(0.87 0.018 255.508);
|
||||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
--primary-foreground: oklch(0.235 0.042 265.755);
|
||||||
--secondary: oklch(0.2 0 0);
|
--secondary: oklch(0.255 0.012 258);
|
||||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
--secondary-foreground: oklch(0.92 0.008 247.858);
|
||||||
--muted: oklch(0.2 0 0);
|
--muted: oklch(0.245 0.012 258);
|
||||||
--muted-foreground: oklch(0.704 0 0);
|
--muted-foreground: oklch(0.68 0.014 257.417);
|
||||||
--accent: oklch(0.2 0 0);
|
--accent: oklch(0.265 0.014 258);
|
||||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
--accent-foreground: oklch(0.92 0.008 247.858);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 12%);
|
--border: oklch(0.31 0.014 258);
|
||||||
--input: oklch(1 0 0 / 16%);
|
--input: oklch(0.34 0.014 258);
|
||||||
--ring: oklch(0.65 0 0);
|
--ring: oklch(0.58 0.025 256.788);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.185 0.012 258);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.92 0.008 247.858);
|
||||||
--sidebar-primary: oklch(0.75 0.14 233);
|
--sidebar-primary: oklch(0.75 0.14 233);
|
||||||
--sidebar-primary-foreground: oklch(0.29 0.06 243);
|
--sidebar-primary-foreground: oklch(0.29 0.06 243);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.255 0.012 258);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.92 0.008 247.858);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(0.3 0.014 258);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: oklch(0.52 0.02 256.788);
|
||||||
--skeleton-base: oklch(0.18 0 0);
|
--skeleton-base: oklch(0.245 0.01 258);
|
||||||
--skeleton-highlight: oklch(0.26 0 0);
|
--skeleton-highlight: oklch(0.32 0.014 258);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user