Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
db48108d
authored
Apr 28, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(logs): enhance usage logs table with log type indicators and improve UI elements
parent
22ef5b2f
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
289 additions
and
185 deletions
+289
-185
web/default/src/components/data-table/column-header.tsx
+1
-1
web/default/src/components/status-badge.tsx
+13
-11
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
+151
-97
web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx
+19
-9
web/default/src/features/usage-logs/components/common-logs-stats.tsx
+6
-6
web/default/src/features/usage-logs/components/usage-logs-table.tsx
+54
-15
web/default/src/lib/format.ts
+0
-1
web/default/src/styles/theme.css
+45
-45
No files found.
web/default/src/components/data-table/column-header.tsx
View file @
db48108d
...
@@ -19,7 +19,7 @@ import {
...
@@ -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
>
({
...
...
web/default/src/components/status-badge.tsx
View file @
db48108d
...
@@ -69,7 +69,7 @@ export interface StatusBadgeProps extends Omit<
...
@@ -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({
...
@@ -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({
...
@@ -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({
...
@@ -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
>
)
)
...
...
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
View file @
db48108d
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 {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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
className={cn(
'size-1.5 shrink-0 rounded-full',
dotColorMap[timeVariant]
)}
aria-hidden='true'
/>
<span
<span
className={cn(
className={cn(
'font-mono font-medium',
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 font-mono text-xs font-medium',
pillBg[timeVariant],
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>[] {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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>[] {
...
@@ -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 (
<>
<>
<
B
utton
<
b
utton
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>
)}
)}
</
B
utton>
</
b
utton>
<DetailsDialog
<DetailsDialog
log={log}
log={log}
isAdmin={isAdmin}
isAdmin={isAdmin}
...
@@ -748,8 +802,8 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
...
@@ -748,8 +802,8 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
)
)
},
},
meta: { label: t('Details'), mobileHidden: true },
meta: { label: t('Details'), mobileHidden: true },
size:
20
0,
size:
18
0,
maxSize: 2
2
0,
maxSize: 2
0
0,
}
}
)
)
...
...
web/default/src/features/usage-logs/components/common-logs-filter-bar.tsx
View file @
db48108d
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({
...
@@ -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({
...
@@ -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({
...
@@ -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({
...
@@ -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
}
...
...
web/default/src/features/usage-logs/components/common-logs-stats.tsx
View file @
db48108d
...
@@ -42,19 +42,19 @@ export function CommonLogsStats() {
...
@@ -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-[126
px] rounded-md'
/>
<
Skeleton
className=
'h-
7 w-[150
px] rounded-md'
/>
<
Skeleton
className=
'h-
6 w-[76
px] rounded-md'
/>
<
Skeleton
className=
'h-
7 w-[100
px] rounded-md'
/>
<
Skeleton
className=
'h-
6 w-[92
px] rounded-md'
/>
<
Skeleton
className=
'h-
7 w-[120
px] 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
,
...
...
web/default/src/features/usage-logs/components/usage-logs-table.tsx
View file @
db48108d
...
@@ -34,7 +34,7 @@ import {
...
@@ -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'
...
@@ -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) {
...
@@ -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) {
...
@@ -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) {
...
@@ -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
>
...
...
web/default/src/lib/format.ts
View file @
db48108d
...
@@ -162,7 +162,6 @@ export function formatTokens(tokens: number): string {
...
@@ -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
...
...
web/default/src/styles/theme.css
View file @
db48108d
@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.1
29
0.042
264.695
);
--foreground
:
oklch
(
0.1
8
0.035
264.695
);
--card
:
oklch
(
1
0
0
);
--card
:
oklch
(
0.997
0.002
247.858
);
--card-foreground
:
oklch
(
0.1
29
0.042
264.695
);
--card-foreground
:
oklch
(
0.1
8
0.035
264.695
);
--popover
:
oklch
(
1
0
0
);
--popover
:
oklch
(
0.997
0.002
247.858
);
--popover-foreground
:
oklch
(
0.1
29
0.042
264.695
);
--popover-foreground
:
oklch
(
0.1
8
0.035
264.695
);
--primary
:
oklch
(
0.2
08
0.042
265.755
);
--primary
:
oklch
(
0.2
55
0.042
265.755
);
--primary-foreground
:
oklch
(
0.98
4
0.003
247.858
);
--primary-foreground
:
oklch
(
0.98
5
0.004
247.858
);
--secondary
:
oklch
(
0.9
68
0.007
247.896
);
--secondary
:
oklch
(
0.9
74
0.004
247.896
);
--secondary-foreground
:
oklch
(
0.2
08
0.042
265.755
);
--secondary-foreground
:
oklch
(
0.2
55
0.042
265.755
);
--muted
:
oklch
(
0.9
68
0.007
247.896
);
--muted
:
oklch
(
0.9
72
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.9
68
0.007
247.896
);
--accent
:
oklch
(
0.9
72
0.004
247.896
);
--accent-foreground
:
oklch
(
0.2
08
0.042
265.755
);
--accent-foreground
:
oklch
(
0.2
55
0.042
265.755
);
--destructive
:
oklch
(
0.577
0.245
27.325
);
--destructive
:
oklch
(
0.577
0.245
27.325
);
--border
:
oklch
(
0.92
9
0.013
255.508
);
--border
:
oklch
(
0.92
5
0.01
255.508
);
--input
:
oklch
(
0.92
9
0.013
255.508
);
--input
:
oklch
(
0.92
5
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 @@
...
@@ -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.94
3
0.003
264
);
--skeleton-base
:
oklch
(
0.94
8
0.004
264
);
--skeleton-highlight
:
oklch
(
0.98
5
0.001
264
);
--skeleton-highlight
:
oklch
(
0.98
8
0.002
264
);
}
}
.dark
{
.dark
{
--background
:
oklch
(
0.1
1
0
0
);
--background
:
oklch
(
0.1
65
0.012
258
);
--foreground
:
oklch
(
0.9
84
0.003
247.858
);
--foreground
:
oklch
(
0.9
2
0.008
247.858
);
--card
:
oklch
(
0.
13
0
0
);
--card
:
oklch
(
0.
205
0.012
258
);
--card-foreground
:
oklch
(
0.9
84
0.003
247.858
);
--card-foreground
:
oklch
(
0.9
2
0.008
247.858
);
--popover
:
oklch
(
0.
15
0
0
);
--popover
:
oklch
(
0.
225
0.014
258
);
--popover-foreground
:
oklch
(
0.9
84
0.003
247.858
);
--popover-foreground
:
oklch
(
0.9
2
0.008
247.858
);
--primary
:
oklch
(
0.
929
0.013
255.508
);
--primary
:
oklch
(
0.
87
0.018
255.508
);
--primary-foreground
:
oklch
(
0.2
08
0.042
265.755
);
--primary-foreground
:
oklch
(
0.2
35
0.042
265.755
);
--secondary
:
oklch
(
0.2
0
0
);
--secondary
:
oklch
(
0.2
55
0.012
258
);
--secondary-foreground
:
oklch
(
0.9
84
0.003
247.858
);
--secondary-foreground
:
oklch
(
0.9
2
0.008
247.858
);
--muted
:
oklch
(
0.2
0
0
);
--muted
:
oklch
(
0.2
45
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.2
65
0.014
258
);
--accent-foreground
:
oklch
(
0.9
84
0.003
247.858
);
--accent-foreground
:
oklch
(
0.9
2
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.9
85
0
0
);
--sidebar-foreground
:
oklch
(
0.9
2
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.2
69
0
0
);
--sidebar-accent
:
oklch
(
0.2
55
0.012
258
);
--sidebar-accent-foreground
:
oklch
(
0.9
85
0
0
);
--sidebar-accent-foreground
:
oklch
(
0.9
2
0.008
247.858
);
--sidebar-border
:
oklch
(
1
0
0
/
10%
);
--sidebar-border
:
oklch
(
0.3
0.014
258
);
--sidebar-ring
:
oklch
(
0.5
56
0
0
);
--sidebar-ring
:
oklch
(
0.5
2
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
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment