143 lines
4.6 KiB
TypeScript
Vendored

import { useEffect, useState } from 'react'
import { formatNumber, formatQuota } from '@/lib/format'
import { computeTimeRange } from '@/lib/time'
import { useAuthStore } from '@/stores/auth-store'
import { Skeleton } from '@/components/ui/skeleton'
import { getUserQuotaDates } from '@/features/dashboard/api'
import { useModelStatCardsConfig } from '@/features/dashboard/hooks/use-dashboard-config'
import {
buildQueryParams,
calculateDashboardStats,
getDefaultDays,
} from '@/features/dashboard/lib'
import type {
QuotaDataItem,
DashboardFilters,
} from '@/features/dashboard/types'
interface LogStatCardsProps {
filters?: DashboardFilters
onDataUpdate?: (data: QuotaDataItem[], loading: boolean) => void
}
export function LogStatCards(props: LogStatCardsProps) {
const statCardsConfig = useModelStatCardsConfig()
const user = useAuthStore((state) => state.auth.user)
const isAdmin = !!(user?.role && user.role >= 10)
const [stats, setStats] = useState<{
totalQuota: number
totalCount: number
totalTokens: number
} | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)
const [timeRangeMinutes, setTimeRangeMinutes] = useState(0)
const { filters, onDataUpdate } = props
useEffect(() => {
const abortController = new AbortController()
// eslint-disable-next-line react-hooks/set-state-in-effect
setLoading(true)
setError(false)
onDataUpdate?.([], true)
const timeRange = computeTimeRange(
getDefaultDays(filters?.time_granularity),
filters?.start_timestamp,
filters?.end_timestamp
)
const timeDiff = (timeRange.end_timestamp - timeRange.start_timestamp) / 60
setTimeRangeMinutes(timeDiff)
getUserQuotaDates(buildQueryParams(timeRange, filters), isAdmin)
.then((res) => {
if (abortController.signal.aborted) return
const data = res?.data || []
setStats(calculateDashboardStats(data))
onDataUpdate?.(data, false)
})
.catch(() => {
if (abortController.signal.aborted) return
setStats(null)
setError(true)
onDataUpdate?.([], false)
})
.finally(() => {
if (!abortController.signal.aborted) {
setLoading(false)
}
})
return () => {
abortController.abort()
}
}, [filters, isAdmin, onDataUpdate])
const adaptedStats = {
rpm: stats?.totalCount ?? 0,
quota: stats?.totalQuota ?? 0,
tpm: stats?.totalTokens ?? 0,
}
const items = statCardsConfig.map((config) => ({
title: config.title,
value:
config.key === 'quota'
? formatQuota(config.getValue(adaptedStats, timeRangeMinutes))
: formatNumber(config.getValue(adaptedStats, timeRangeMinutes)),
desc: config.description,
icon: config.icon,
}))
return (
<div className='overflow-hidden rounded-lg border'>
<div className='divide-border/60 grid grid-cols-2 divide-x sm:grid-cols-3 lg:grid-cols-5'>
{items.map((it, idx) => {
const Icon = it.icon
return (
<div
key={it.title}
className={`px-3 py-2.5 sm:px-5 sm:py-4 ${idx === items.length - 1 && items.length % 2 !== 0 ? 'col-span-2 sm:col-span-1' : ''}`}
>
<div className='flex items-center gap-2'>
<Icon className='text-muted-foreground/60 size-3.5 shrink-0' />
<div className='text-muted-foreground truncate text-xs font-medium tracking-wider uppercase'>
{it.title}
</div>
</div>
{loading ? (
<div className='mt-2 space-y-1.5'>
<Skeleton className='h-7 w-20' />
<Skeleton className='h-3.5 w-28' />
</div>
) : error ? (
<>
<div className='text-muted-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
--
</div>
<div className='text-muted-foreground/40 mt-1 hidden text-xs md:block'>
{it.desc}
</div>
</>
) : (
<>
<div className='text-foreground mt-1.5 font-mono text-lg font-bold tracking-tight tabular-nums sm:mt-2 sm:text-2xl'>
{it.value}
</div>
<div className='text-muted-foreground/60 mt-1 hidden text-xs md:block'>
{it.desc}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}