Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
8147866c
Commit
8147866c
authored
Mar 16, 2026
by
Peter
Browse files
fix(admin): polish spending ranking and usage defaults
parent
6da5fa01
Changes
10
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/admin/dashboard_handler.go
View file @
8147866c
...
@@ -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"
),
}
}
...
...
backend/internal/handler/admin/dashboard_handler_request_type_test.go
View file @
8147866c
...
@@ -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
)
...
...
backend/internal/pkg/usagestats/usage_log_types.go
View file @
8147866c
...
@@ -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
...
...
backend/internal/repository/usage_log_repo.go
View file @
8147866c
...
@@ -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
}
}
...
...
backend/internal/repository/usage_log_repo_request_type_test.go
View file @
8147866c
...
@@ -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
())
}
}
...
...
frontend/src/components/charts/ModelDistributionChart.vue
View file @
8147866c
...
@@ -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=
"ranking
Display
Items.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=
"getRanking
User
Label(item)"
:
title
=
"
getRanking
Row
Label(item)
"
>
>
{{
getRanking
User
Label
(
item
)
}}
{{
getRanking
Row
Label
(
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
(
other
ActualCost
>
0.000001
)
{
if
(
other
RankingItem
.
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
'
...
...
frontend/src/components/charts/__tests__/ModelDistributionChart.spec.ts
View file @
8147866c
...
@@ -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
'
)
})
})
})
frontend/src/types/index.ts
View file @
8147866c
...
@@ -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
}
}
...
...
frontend/src/views/admin/DashboardView.vue
View file @
8147866c
...
@@ -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
)
{
...
...
frontend/src/views/admin/UsageView.vue
View file @
8147866c
...
@@ -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
()
...
...
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