Merge pull request #1040 from 0xObjc/codex/fix-user-spending-ranking-others
fix(admin): polish spending ranking and usage defaults
This commit is contained in:
@@ -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(
|
|||||||
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) {
|
|||||||
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)
|
||||||
|
|||||||
@@ -125,6 +125,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
|
||||||
|
|||||||
@@ -2161,7 +2161,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
|
||||||
@@ -2172,7 +2174,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
|
||||||
`
|
`
|
||||||
@@ -2190,9 +2194,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)
|
||||||
@@ -2204,6 +2210,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,10 +259,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).
|
||||||
@@ -277,6 +277,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 @@
|
|||||||
>
|
>
|
||||||
{{ 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 @@
|
|||||||
</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)
|
|||||||
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<{
|
|||||||
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(() => {
|
|||||||
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(() => {
|
|||||||
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 => {
|
|||||||
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'
|
|||||||
|
|
||||||
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> = {
|
|||||||
'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', () => {
|
|||||||
})
|
})
|
||||||
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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1209,6 +1209,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 @@
|
|||||||
: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[]>([])
|
|||||||
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 => {
|
|||||||
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 () => {
|
|||||||
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) {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ type DistributionMetric = 'tokens' | 'actual_cost'
|
|||||||
type EndpointSource = 'inbound' | 'upstream' | 'path'
|
type EndpointSource = 'inbound' | 'upstream' | 'path'
|
||||||
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')
|
||||||
const endpointDistributionMetric = ref<DistributionMetric>('tokens')
|
const endpointDistributionMetric = ref<DistributionMetric>('tokens')
|
||||||
@@ -159,6 +159,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 })
|
||||||
@@ -193,6 +194,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 () => {
|
||||||
@@ -258,7 +260,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()
|
||||||
|
|||||||
Reference in New Issue
Block a user