Commit 8147866c authored by Peter's avatar Peter
Browse files

fix(admin): polish spending ranking and usage defaults

parent 6da5fa01
...@@ -512,6 +512,8 @@ func (h *DashboardHandler) GetUserSpendingRanking(c *gin.Context) { ...@@ -512,6 +512,8 @@ func (h *DashboardHandler) GetUserSpendingRanking(c *gin.Context) {
payload := gin.H{ payload := gin.H{
"ranking": ranking.Ranking, "ranking": ranking.Ranking,
"total_actual_cost": ranking.TotalActualCost, "total_actual_cost": ranking.TotalActualCost,
"total_requests": ranking.TotalRequests,
"total_tokens": ranking.TotalTokens,
"start_date": startTime.Format("2006-01-02"), "start_date": startTime.Format("2006-01-02"),
"end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"),
} }
......
...@@ -61,6 +61,8 @@ func (s *dashboardUsageRepoCapture) GetUserSpendingRanking( ...@@ -61,6 +61,8 @@ func (s *dashboardUsageRepoCapture) GetUserSpendingRanking(
return &usagestats.UserSpendingRankingResponse{ return &usagestats.UserSpendingRankingResponse{
Ranking: s.ranking, Ranking: s.ranking,
TotalActualCost: s.rankingTotal, TotalActualCost: s.rankingTotal,
TotalRequests: 44,
TotalTokens: 1234,
}, nil }, nil
} }
...@@ -164,6 +166,8 @@ func TestDashboardUsersRankingLimitAndCache(t *testing.T) { ...@@ -164,6 +166,8 @@ func TestDashboardUsersRankingLimitAndCache(t *testing.T) {
require.Equal(t, http.StatusOK, rec.Code) require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, 50, repo.rankingLimit) require.Equal(t, 50, repo.rankingLimit)
require.Contains(t, rec.Body.String(), "\"total_actual_cost\":88.8") require.Contains(t, rec.Body.String(), "\"total_actual_cost\":88.8")
require.Contains(t, rec.Body.String(), "\"total_requests\":44")
require.Contains(t, rec.Body.String(), "\"total_tokens\":1234")
require.Equal(t, "miss", rec.Header().Get("X-Snapshot-Cache")) require.Equal(t, "miss", rec.Header().Get("X-Snapshot-Cache"))
req2 := httptest.NewRequest(http.MethodGet, "/admin/dashboard/users-ranking?limit=100&start_date=2025-01-01&end_date=2025-01-02", nil) req2 := httptest.NewRequest(http.MethodGet, "/admin/dashboard/users-ranking?limit=100&start_date=2025-01-01&end_date=2025-01-02", nil)
......
...@@ -116,6 +116,8 @@ type UserSpendingRankingItem struct { ...@@ -116,6 +116,8 @@ type UserSpendingRankingItem struct {
type UserSpendingRankingResponse struct { type UserSpendingRankingResponse struct {
Ranking []UserSpendingRankingItem `json:"ranking"` Ranking []UserSpendingRankingItem `json:"ranking"`
TotalActualCost float64 `json:"total_actual_cost"` TotalActualCost float64 `json:"total_actual_cost"`
TotalRequests int64 `json:"total_requests"`
TotalTokens int64 `json:"total_tokens"`
} }
// APIKeyUsageTrendPoint represents API key usage trend data point // APIKeyUsageTrendPoint represents API key usage trend data point
......
...@@ -2139,7 +2139,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi ...@@ -2139,7 +2139,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
actual_cost, actual_cost,
requests, requests,
tokens, tokens,
COALESCE(SUM(actual_cost) OVER (), 0) as total_actual_cost COALESCE(SUM(actual_cost) OVER (), 0) as total_actual_cost,
COALESCE(SUM(requests) OVER (), 0) as total_requests,
COALESCE(SUM(tokens) OVER (), 0) as total_tokens
FROM user_spend FROM user_spend
ORDER BY actual_cost DESC, tokens DESC, user_id ASC ORDER BY actual_cost DESC, tokens DESC, user_id ASC
LIMIT $3 LIMIT $3
...@@ -2150,7 +2152,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi ...@@ -2150,7 +2152,9 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
actual_cost, actual_cost,
requests, requests,
tokens, tokens,
total_actual_cost total_actual_cost,
total_requests,
total_tokens
FROM ranked FROM ranked
ORDER BY actual_cost DESC, tokens DESC, user_id ASC ORDER BY actual_cost DESC, tokens DESC, user_id ASC
` `
...@@ -2168,9 +2172,11 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi ...@@ -2168,9 +2172,11 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
ranking := make([]UserSpendingRankingItem, 0) ranking := make([]UserSpendingRankingItem, 0)
totalActualCost := 0.0 totalActualCost := 0.0
totalRequests := int64(0)
totalTokens := int64(0)
for rows.Next() { for rows.Next() {
var row UserSpendingRankingItem var row UserSpendingRankingItem
if err = rows.Scan(&row.UserID, &row.Email, &row.ActualCost, &row.Requests, &row.Tokens, &totalActualCost); err != nil { if err = rows.Scan(&row.UserID, &row.Email, &row.ActualCost, &row.Requests, &row.Tokens, &totalActualCost, &totalRequests, &totalTokens); err != nil {
return nil, err return nil, err
} }
ranking = append(ranking, row) ranking = append(ranking, row)
...@@ -2182,6 +2188,8 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi ...@@ -2182,6 +2188,8 @@ func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTi
return &UserSpendingRankingResponse{ return &UserSpendingRankingResponse{
Ranking: ranking, Ranking: ranking,
TotalActualCost: totalActualCost, TotalActualCost: totalActualCost,
TotalRequests: totalRequests,
TotalTokens: totalTokens,
}, nil }, nil
} }
......
...@@ -255,10 +255,10 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) { ...@@ -255,10 +255,10 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
end := start.Add(24 * time.Hour) end := start.Add(24 * time.Hour)
rows := sqlmock.NewRows([]string{"user_id", "email", "actual_cost", "requests", "tokens", "total_actual_cost"}). rows := sqlmock.NewRows([]string{"user_id", "email", "actual_cost", "requests", "tokens", "total_actual_cost", "total_requests", "total_tokens"}).
AddRow(int64(2), "beta@example.com", 12.5, int64(9), int64(900), 40.0). AddRow(int64(2), "beta@example.com", 12.5, int64(9), int64(900), 40.0, int64(30), int64(2600)).
AddRow(int64(1), "alpha@example.com", 12.5, int64(8), int64(800), 40.0). AddRow(int64(1), "alpha@example.com", 12.5, int64(8), int64(800), 40.0, int64(30), int64(2600)).
AddRow(int64(3), "gamma@example.com", 4.25, int64(5), int64(300), 40.0) AddRow(int64(3), "gamma@example.com", 4.25, int64(5), int64(300), 40.0, int64(30), int64(2600))
mock.ExpectQuery("WITH user_spend AS \\("). mock.ExpectQuery("WITH user_spend AS \\(").
WithArgs(start, end, 12). WithArgs(start, end, 12).
...@@ -273,6 +273,8 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) { ...@@ -273,6 +273,8 @@ func TestUsageLogRepositoryGetUserSpendingRanking(t *testing.T) {
{UserID: 3, Email: "gamma@example.com", ActualCost: 4.25, Requests: 5, Tokens: 300}, {UserID: 3, Email: "gamma@example.com", ActualCost: 4.25, Requests: 5, Tokens: 300},
}, },
TotalActualCost: 40.0, TotalActualCost: 40.0,
TotalRequests: 30,
TotalTokens: 2600,
}, got) }, got)
require.NoError(t, mock.ExpectationsWereMet()) require.NoError(t, mock.ExpectationsWereMet())
} }
......
...@@ -127,7 +127,7 @@ ...@@ -127,7 +127,7 @@
> >
{{ t('admin.dashboard.failedToLoad') }} {{ t('admin.dashboard.failedToLoad') }}
</div> </div>
<div v-else-if="rankingItems.length > 0 && rankingChartData" class="flex items-center gap-6"> <div v-else-if="rankingDisplayItems.length > 0 && rankingChartData" class="flex items-center gap-6">
<div class="h-48 w-48"> <div class="h-48 w-48">
<Doughnut :data="rankingChartData" :options="rankingDoughnutOptions" /> <Doughnut :data="rankingChartData" :options="rankingDoughnutOptions" />
</div> </div>
...@@ -143,21 +143,24 @@ ...@@ -143,21 +143,24 @@
</thead> </thead>
<tbody> <tbody>
<tr <tr
v-for="(item, index) in rankingItems" v-for="(item, index) in rankingDisplayItems"
:key="`${item.user_id}-${index}`" :key="item.isOther ? 'others' : `${item.user_id}-${index}`"
class="cursor-pointer border-t border-gray-100 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-dark-700/40" class="border-t border-gray-100 transition-colors dark:border-gray-700"
@click="emit('ranking-click', item)" :class="item.isOther
? 'bg-gray-50/70 dark:bg-dark-700/20'
: 'cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-700/40'"
@click="item.isOther ? undefined : emit('ranking-click', item)"
> >
<td class="py-1.5"> <td class="py-1.5">
<div class="flex min-w-0 items-center gap-2"> <div class="flex min-w-0 items-center gap-2">
<span class="shrink-0 text-[11px] font-semibold text-gray-500 dark:text-gray-400"> <span class="shrink-0 text-[11px] font-semibold text-gray-500 dark:text-gray-400">
#{{ index + 1 }} {{ item.isOther ? 'Σ' : `#${index + 1}` }}
</span> </span>
<span <span
class="block max-w-[140px] truncate font-medium text-gray-900 dark:text-white" class="block max-w-[140px] truncate font-medium text-gray-900 dark:text-white"
:title="getRankingUserLabel(item)" :title="getRankingRowLabel(item)"
> >
{{ getRankingUserLabel(item) }} {{ getRankingRowLabel(item) }}
</span> </span>
</div> </div>
</td> </td>
...@@ -197,11 +200,14 @@ ChartJS.register(ArcElement, Tooltip, Legend) ...@@ -197,11 +200,14 @@ ChartJS.register(ArcElement, Tooltip, Legend)
const { t } = useI18n() const { t } = useI18n()
type DistributionMetric = 'tokens' | 'actual_cost' type DistributionMetric = 'tokens' | 'actual_cost'
type RankingDisplayItem = UserSpendingRankingItem & { isOther?: boolean }
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
modelStats: ModelStat[] modelStats: ModelStat[]
enableRankingView?: boolean enableRankingView?: boolean
rankingItems?: UserSpendingRankingItem[] rankingItems?: UserSpendingRankingItem[]
rankingTotalActualCost?: number rankingTotalActualCost?: number
rankingTotalRequests?: number
rankingTotalTokens?: number
loading?: boolean loading?: boolean
metric?: DistributionMetric metric?: DistributionMetric
showMetricToggle?: boolean showMetricToggle?: boolean
...@@ -211,6 +217,8 @@ const props = withDefaults(defineProps<{ ...@@ -211,6 +217,8 @@ const props = withDefaults(defineProps<{
enableRankingView: false, enableRankingView: false,
rankingItems: () => [], rankingItems: () => [],
rankingTotalActualCost: 0, rankingTotalActualCost: 0,
rankingTotalRequests: 0,
rankingTotalTokens: 0,
loading: false, loading: false,
metric: 'tokens', metric: 'tokens',
showMetricToggle: false, showMetricToggle: false,
...@@ -266,14 +274,14 @@ const chartData = computed(() => { ...@@ -266,14 +274,14 @@ const chartData = computed(() => {
const rankingChartData = computed(() => { const rankingChartData = computed(() => {
if (!props.rankingItems?.length) return null if (!props.rankingItems?.length) return null
const rankedTotal = props.rankingItems.reduce((sum, item) => sum + item.actual_cost, 0)
const otherActualCost = Math.max((props.rankingTotalActualCost || 0) - rankedTotal, 0)
const labels = props.rankingItems.map((item, index) => `#${index + 1} ${getRankingUserLabel(item)}`) const labels = props.rankingItems.map((item, index) => `#${index + 1} ${getRankingUserLabel(item)}`)
const data = props.rankingItems.map((item) => item.actual_cost) const data = props.rankingItems.map((item) => item.actual_cost)
const backgroundColor = chartColors.slice(0, props.rankingItems.length)
if (otherActualCost > 0.000001) { if (otherRankingItem.value) {
labels.push(t('admin.dashboard.spendingRankingOther')) labels.push(t('admin.dashboard.spendingRankingOther'))
data.push(otherActualCost) data.push(otherRankingItem.value.actual_cost)
backgroundColor.push('#94a3b8')
} }
return { return {
...@@ -281,13 +289,43 @@ const rankingChartData = computed(() => { ...@@ -281,13 +289,43 @@ const rankingChartData = computed(() => {
datasets: [ datasets: [
{ {
data, data,
backgroundColor: chartColors.slice(0, data.length), backgroundColor,
borderWidth: 0 borderWidth: 0
} }
] ]
} }
}) })
const otherRankingItem = computed<RankingDisplayItem | null>(() => {
if (!props.rankingItems?.length) return null
const rankedActualCost = props.rankingItems.reduce((sum, item) => sum + item.actual_cost, 0)
const rankedRequests = props.rankingItems.reduce((sum, item) => sum + item.requests, 0)
const rankedTokens = props.rankingItems.reduce((sum, item) => sum + item.tokens, 0)
const otherActualCost = Math.max((props.rankingTotalActualCost || 0) - rankedActualCost, 0)
const otherRequests = Math.max((props.rankingTotalRequests || 0) - rankedRequests, 0)
const otherTokens = Math.max((props.rankingTotalTokens || 0) - rankedTokens, 0)
if (otherActualCost <= 0.000001 && otherRequests <= 0 && otherTokens <= 0) return null
return {
user_id: 0,
email: '',
actual_cost: otherActualCost,
requests: otherRequests,
tokens: otherTokens,
isOther: true
}
})
const rankingDisplayItems = computed<RankingDisplayItem[]>(() => {
if (!props.rankingItems?.length) return []
return otherRankingItem.value
? [...props.rankingItems, otherRankingItem.value]
: [...props.rankingItems]
})
const doughnutOptions = computed(() => ({ const doughnutOptions = computed(() => ({
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
...@@ -351,6 +389,11 @@ const getRankingUserLabel = (item: UserSpendingRankingItem): string => { ...@@ -351,6 +389,11 @@ const getRankingUserLabel = (item: UserSpendingRankingItem): string => {
return t('admin.redeem.userPrefix', { id: item.user_id }) return t('admin.redeem.userPrefix', { id: item.user_id })
} }
const getRankingRowLabel = (item: RankingDisplayItem): string => {
if (item.isOther) return t('admin.dashboard.spendingRankingOther')
return getRankingUserLabel(item)
}
const formatCost = (value: number): string => { const formatCost = (value: number): string => {
if (value >= 1000) { if (value >= 1000) {
return (value / 1000).toFixed(2) + 'K' return (value / 1000).toFixed(2) + 'K'
......
...@@ -5,6 +5,14 @@ import ModelDistributionChart from '../ModelDistributionChart.vue' ...@@ -5,6 +5,14 @@ import ModelDistributionChart from '../ModelDistributionChart.vue'
const messages: Record<string, string> = { const messages: Record<string, string> = {
'admin.dashboard.modelDistribution': 'Model Distribution', 'admin.dashboard.modelDistribution': 'Model Distribution',
'admin.dashboard.spendingRankingTitle': 'User Spending Ranking',
'admin.dashboard.viewModelDistribution': 'Model Distribution',
'admin.dashboard.viewSpendingRanking': 'User Spending Ranking',
'admin.dashboard.spendingRankingUser': 'User',
'admin.dashboard.spendingRankingRequests': 'Requests',
'admin.dashboard.spendingRankingTokens': 'Tokens',
'admin.dashboard.spendingRankingSpend': 'Spend',
'admin.dashboard.spendingRankingOther': 'Others',
'admin.dashboard.model': 'Model', 'admin.dashboard.model': 'Model',
'admin.dashboard.requests': 'Requests', 'admin.dashboard.requests': 'Requests',
'admin.dashboard.tokens': 'Tokens', 'admin.dashboard.tokens': 'Tokens',
...@@ -13,6 +21,7 @@ const messages: Record<string, string> = { ...@@ -13,6 +21,7 @@ const messages: Record<string, string> = {
'admin.dashboard.metricTokens': 'By Tokens', 'admin.dashboard.metricTokens': 'By Tokens',
'admin.dashboard.metricActualCost': 'By Actual Cost', 'admin.dashboard.metricActualCost': 'By Actual Cost',
'admin.dashboard.noDataAvailable': 'No data available', 'admin.dashboard.noDataAvailable': 'No data available',
'admin.redeem.userPrefix': 'User #{id}',
} }
vi.mock('vue-i18n', async () => { vi.mock('vue-i18n', async () => {
...@@ -116,4 +125,47 @@ describe('ModelDistributionChart', () => { ...@@ -116,4 +125,47 @@ describe('ModelDistributionChart', () => {
}) })
expect(label).toBe('model-b: $1.40 (87.5%)') expect(label).toBe('model-b: $1.40 (87.5%)')
}) })
it('renders Others in the spending ranking table and uses a dedicated chart color', async () => {
const wrapper = mount(ModelDistributionChart, {
props: {
modelStats: [],
enableRankingView: true,
rankingItems: [
{ user_id: 1, email: 'alpha@example.com', actual_cost: 12, requests: 10, tokens: 1000 },
{ user_id: 2, email: 'beta@example.com', actual_cost: 8, requests: 6, tokens: 600 },
],
rankingTotalActualCost: 30,
rankingTotalRequests: 20,
rankingTotalTokens: 2000,
},
global: {
stubs: {
LoadingSpinner: true,
},
},
})
const rankingButton = wrapper.findAll('button').find((button) => button.text() === 'User Spending Ranking')
expect(rankingButton).toBeTruthy()
await rankingButton!.trigger('click')
const chartData = JSON.parse(wrapper.find('.chart-data').text())
expect(chartData.labels).toEqual([
'#1 alpha@example.com',
'#2 beta@example.com',
'Others',
])
expect(chartData.datasets[0].data).toEqual([12, 8, 10])
expect(chartData.datasets[0].backgroundColor[0]).toBe('#3b82f6')
expect(chartData.datasets[0].backgroundColor[2]).toBe('#94a3b8')
expect(chartData.datasets[0].backgroundColor[2]).not.toBe(chartData.datasets[0].backgroundColor[0])
const rows = wrapper.findAll('tbody tr')
expect(rows).toHaveLength(3)
expect(rows[2].text()).toContain('Others')
expect(rows[2].text()).toContain('4')
expect(rows[2].text()).toContain('400')
expect(rows[2].text()).toContain('$10.00')
})
}) })
...@@ -1199,6 +1199,8 @@ export interface UserSpendingRankingItem { ...@@ -1199,6 +1199,8 @@ export interface UserSpendingRankingItem {
export interface UserSpendingRankingResponse { export interface UserSpendingRankingResponse {
ranking: UserSpendingRankingItem[] ranking: UserSpendingRankingItem[]
total_actual_cost: number total_actual_cost: number
total_requests: number
total_tokens: number
start_date: string start_date: string
end_date: string end_date: string
} }
......
...@@ -241,6 +241,8 @@ ...@@ -241,6 +241,8 @@
:enable-ranking-view="true" :enable-ranking-view="true"
:ranking-items="rankingItems" :ranking-items="rankingItems"
:ranking-total-actual-cost="rankingTotalActualCost" :ranking-total-actual-cost="rankingTotalActualCost"
:ranking-total-requests="rankingTotalRequests"
:ranking-total-tokens="rankingTotalTokens"
:loading="chartsLoading" :loading="chartsLoading"
:ranking-loading="rankingLoading" :ranking-loading="rankingLoading"
:ranking-error="rankingError" :ranking-error="rankingError"
...@@ -334,6 +336,8 @@ const modelStats = ref<ModelStat[]>([]) ...@@ -334,6 +336,8 @@ const modelStats = ref<ModelStat[]>([])
const userTrend = ref<UserUsageTrendPoint[]>([]) const userTrend = ref<UserUsageTrendPoint[]>([])
const rankingItems = ref<UserSpendingRankingItem[]>([]) const rankingItems = ref<UserSpendingRankingItem[]>([])
const rankingTotalActualCost = ref(0) const rankingTotalActualCost = ref(0)
const rankingTotalRequests = ref(0)
const rankingTotalTokens = ref(0)
let chartLoadSeq = 0 let chartLoadSeq = 0
let usersTrendLoadSeq = 0 let usersTrendLoadSeq = 0
let rankingLoadSeq = 0 let rankingLoadSeq = 0
...@@ -347,7 +351,7 @@ const formatLocalDate = (date: Date): string => { ...@@ -347,7 +351,7 @@ const formatLocalDate = (date: Date): string => {
const getTodayLocalDate = () => formatLocalDate(new Date()) const getTodayLocalDate = () => formatLocalDate(new Date())
// Date range // Date range
const granularity = ref<'day' | 'hour'>('day') const granularity = ref<'day' | 'hour'>('hour')
const startDate = ref(getTodayLocalDate()) const startDate = ref(getTodayLocalDate())
const endDate = ref(getTodayLocalDate()) const endDate = ref(getTodayLocalDate())
...@@ -630,11 +634,15 @@ const loadUserSpendingRanking = async () => { ...@@ -630,11 +634,15 @@ const loadUserSpendingRanking = async () => {
if (currentSeq !== rankingLoadSeq) return if (currentSeq !== rankingLoadSeq) return
rankingItems.value = response.ranking || [] rankingItems.value = response.ranking || []
rankingTotalActualCost.value = response.total_actual_cost || 0 rankingTotalActualCost.value = response.total_actual_cost || 0
rankingTotalRequests.value = response.total_requests || 0
rankingTotalTokens.value = response.total_tokens || 0
} catch (error) { } catch (error) {
if (currentSeq !== rankingLoadSeq) return if (currentSeq !== rankingLoadSeq) return
console.error('Error loading user spending ranking:', error) console.error('Error loading user spending ranking:', error)
rankingItems.value = [] rankingItems.value = []
rankingTotalActualCost.value = 0 rankingTotalActualCost.value = 0
rankingTotalRequests.value = 0
rankingTotalTokens.value = 0
rankingError.value = true rankingError.value = true
} finally { } finally {
if (currentSeq === rankingLoadSeq) { if (currentSeq === rankingLoadSeq) {
......
...@@ -107,7 +107,7 @@ const appStore = useAppStore() ...@@ -107,7 +107,7 @@ const appStore = useAppStore()
type DistributionMetric = 'tokens' | 'actual_cost' type DistributionMetric = 'tokens' | 'actual_cost'
const route = useRoute() const route = useRoute()
const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs = ref<AdminUsageLog[]>([]); const loading = ref(false); const exporting = ref(false) const usageStats = ref<AdminUsageStatsResponse | null>(null); const usageLogs = ref<AdminUsageLog[]>([]); const loading = ref(false); const exporting = ref(false)
const trendData = ref<TrendDataPoint[]>([]); const modelStats = ref<ModelStat[]>([]); const groupStats = ref<GroupStat[]>([]); const chartsLoading = ref(false); const granularity = ref<'day' | 'hour'>('day') const trendData = ref<TrendDataPoint[]>([]); const modelStats = ref<ModelStat[]>([]); const groupStats = ref<GroupStat[]>([]); const chartsLoading = ref(false); const granularity = ref<'day' | 'hour'>('hour')
const modelDistributionMetric = ref<DistributionMetric>('tokens') const modelDistributionMetric = ref<DistributionMetric>('tokens')
const groupDistributionMetric = ref<DistributionMetric>('tokens') const groupDistributionMetric = ref<DistributionMetric>('tokens')
let abortController: AbortController | null = null; let exportAbortController: AbortController | null = null let abortController: AbortController | null = null; let exportAbortController: AbortController | null = null
...@@ -137,6 +137,7 @@ const formatLD = (d: Date) => { ...@@ -137,6 +137,7 @@ const formatLD = (d: Date) => {
return `${year}-${month}-${day}` return `${year}-${month}-${day}`
} }
const getTodayLocalDate = () => formatLD(new Date()) const getTodayLocalDate = () => formatLD(new Date())
const getGranularityForRange = (start: string, end: string): 'day' | 'hour' => start === end ? 'hour' : 'day'
const startDate = ref(getTodayLocalDate()); const endDate = ref(getTodayLocalDate()) const startDate = ref(getTodayLocalDate()); const endDate = ref(getTodayLocalDate())
const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, request_type: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value }) const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, request_type: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value })
const pagination = reactive({ page: 1, page_size: 20, total: 0 }) const pagination = reactive({ page: 1, page_size: 20, total: 0 })
...@@ -171,6 +172,7 @@ const applyRouteQueryFilters = () => { ...@@ -171,6 +172,7 @@ const applyRouteQueryFilters = () => {
start_date: startDate.value, start_date: startDate.value,
end_date: endDate.value end_date: endDate.value
} }
granularity.value = getGranularityForRange(startDate.value, endDate.value)
} }
const loadLogs = async () => { const loadLogs = async () => {
...@@ -224,7 +226,7 @@ const loadChartData = async () => { ...@@ -224,7 +226,7 @@ const loadChartData = async () => {
} }
const applyFilters = () => { pagination.page = 1; loadLogs(); loadStats(); loadChartData() } const applyFilters = () => { pagination.page = 1; loadLogs(); loadStats(); loadChartData() }
const refreshData = () => { loadLogs(); loadStats(); loadChartData() } const refreshData = () => { loadLogs(); loadStats(); loadChartData() }
const resetFilters = () => { startDate.value = getTodayLocalDate(); endDate.value = getTodayLocalDate(); filters.value = { start_date: startDate.value, end_date: endDate.value, request_type: undefined, billing_type: null }; granularity.value = 'day'; applyFilters() } const resetFilters = () => { startDate.value = getTodayLocalDate(); endDate.value = getTodayLocalDate(); filters.value = { start_date: startDate.value, end_date: endDate.value, request_type: undefined, billing_type: null }; granularity.value = getGranularityForRange(startDate.value, endDate.value); applyFilters() }
const handlePageChange = (p: number) => { pagination.page = p; loadLogs() } const handlePageChange = (p: number) => { pagination.page = p; loadLogs() }
const handlePageSizeChange = (s: number) => { pagination.page_size = s; pagination.page = 1; loadLogs() } const handlePageSizeChange = (s: number) => { pagination.page_size = s; pagination.page = 1; loadLogs() }
const cancelExport = () => exportAbortController?.abort() const cancelExport = () => exportAbortController?.abort()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment