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
80ae592c
Commit
80ae592c
authored
Mar 04, 2026
by
xvhuan
Browse files
perf(admin): optimize large-dataset loading for dashboard/users/accounts/ops
parent
46ea9170
Changes
27
Hide whitespace changes
Inline
Side-by-side
frontend/src/api/admin/dashboard.ts
View file @
80ae592c
...
...
@@ -120,6 +120,31 @@ export interface GroupStatsResponse {
end_date
:
string
}
export
interface
DashboardSnapshotV2Params
extends
TrendParams
{
include_stats
?:
boolean
include_trend
?:
boolean
include_model_stats
?:
boolean
include_group_stats
?:
boolean
include_users_trend
?:
boolean
users_trend_limit
?:
number
}
export
interface
DashboardSnapshotV2Stats
extends
DashboardStats
{
uptime
:
number
}
export
interface
DashboardSnapshotV2Response
{
generated_at
:
string
start_date
:
string
end_date
:
string
granularity
:
string
stats
?:
DashboardSnapshotV2Stats
trend
?:
TrendDataPoint
[]
models
?:
ModelStat
[]
groups
?:
GroupStat
[]
users_trend
?:
UserUsageTrendPoint
[]
}
/**
* Get group usage statistics
* @param params - Query parameters for filtering
...
...
@@ -130,6 +155,16 @@ export async function getGroupStats(params?: GroupStatsParams): Promise<GroupSta
return
data
}
/**
* Get dashboard snapshot v2 (aggregated response for heavy admin pages).
*/
export
async
function
getSnapshotV2
(
params
?:
DashboardSnapshotV2Params
):
Promise
<
DashboardSnapshotV2Response
>
{
const
{
data
}
=
await
apiClient
.
get
<
DashboardSnapshotV2Response
>
(
'
/admin/dashboard/snapshot-v2
'
,
{
params
})
return
data
}
export
interface
ApiKeyTrendParams
extends
TrendParams
{
limit
?:
number
}
...
...
@@ -233,6 +268,7 @@ export const dashboardAPI = {
getUsageTrend
,
getModelStats
,
getGroupStats
,
getSnapshotV2
,
getApiKeyUsageTrend
,
getUserUsageTrend
,
getBatchUsersUsage
,
...
...
frontend/src/api/admin/ops.ts
View file @
80ae592c
...
...
@@ -259,6 +259,13 @@ export interface OpsErrorDistributionResponse {
items
:
OpsErrorDistributionItem
[]
}
export
interface
OpsDashboardSnapshotV2Response
{
generated_at
:
string
overview
:
OpsDashboardOverview
throughput_trend
:
OpsThroughputTrendResponse
error_trend
:
OpsErrorTrendResponse
}
export
type
OpsOpenAITokenStatsTimeRange
=
'
30m
'
|
'
1h
'
|
'
1d
'
|
'
15d
'
|
'
30d
'
export
interface
OpsOpenAITokenStatsItem
{
...
...
@@ -1004,6 +1011,24 @@ export async function getDashboardOverview(
return
data
}
export
async
function
getDashboardSnapshotV2
(
params
:
{
time_range
?:
'
5m
'
|
'
30m
'
|
'
1h
'
|
'
6h
'
|
'
24h
'
start_time
?:
string
end_time
?:
string
platform
?:
string
group_id
?:
number
|
null
mode
?:
OpsQueryMode
},
options
:
OpsRequestOptions
=
{}
):
Promise
<
OpsDashboardSnapshotV2Response
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsDashboardSnapshotV2Response
>
(
'
/admin/ops/dashboard/snapshot-v2
'
,
{
params
,
signal
:
options
.
signal
})
return
data
}
export
async
function
getThroughputTrend
(
params
:
{
time_range
?:
'
5m
'
|
'
30m
'
|
'
1h
'
|
'
6h
'
|
'
24h
'
...
...
@@ -1329,6 +1354,7 @@ async function updateMetricThresholds(thresholds: OpsMetricThresholds): Promise<
}
export
const
opsAPI
=
{
getDashboardSnapshotV2
,
getDashboardOverview
,
getThroughputTrend
,
getLatencyHistogram
,
...
...
frontend/src/api/admin/users.ts
View file @
80ae592c
...
...
@@ -22,6 +22,7 @@ export async function list(
role
?:
'
admin
'
|
'
user
'
search
?:
string
attributes
?:
Record
<
number
,
string
>
// attributeId -> value
include_subscriptions
?:
boolean
},
options
?:
{
signal
?:
AbortSignal
...
...
@@ -33,7 +34,8 @@ export async function list(
page_size
:
pageSize
,
status
:
filters
?.
status
,
role
:
filters
?.
role
,
search
:
filters
?.
search
search
:
filters
?.
search
,
include_subscriptions
:
filters
?.
include_subscriptions
}
// Add attribute filters as attr[id]=value
...
...
frontend/src/views/admin/AccountsView.vue
View file @
80ae592c
...
...
@@ -359,7 +359,7 @@ const exportingData = ref(false)
const
showColumnDropdown
=
ref
(
false
)
const
columnDropdownRef
=
ref
<
HTMLElement
|
null
>
(
null
)
const
hiddenColumns
=
reactive
<
Set
<
string
>>
(
new
Set
())
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
proxy
'
,
'
notes
'
,
'
priority
'
,
'
rate_multiplier
'
]
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
today_stats
'
,
'
proxy
'
,
'
notes
'
,
'
priority
'
,
'
rate_multiplier
'
]
const
HIDDEN_COLUMNS_KEY
=
'
account-hidden-columns
'
// Sorting settings
...
...
@@ -546,7 +546,7 @@ const {
handlePageSizeChange
:
baseHandlePageSizeChange
}
=
useTableLoader
<
Account
,
any
>
({
fetchFn
:
adminAPI
.
accounts
.
list
,
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
group
:
''
,
search
:
''
}
initialParams
:
{
platform
:
''
,
type
:
''
,
status
:
''
,
group
:
''
,
search
:
''
,
lite
:
'
1
'
}
}
)
const
resetAutoRefreshCache
=
()
=>
{
...
...
@@ -689,6 +689,7 @@ const refreshAccountsIncrementally = async () => {
type
?:
string
status
?:
string
search
?:
string
lite
?:
string
}
,
{
etag
:
autoRefreshETag
.
value
}
)
...
...
frontend/src/views/admin/DashboardView.vue
View file @
80ae592c
...
...
@@ -316,6 +316,7 @@ const trendData = ref<TrendDataPoint[]>([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
let
chartLoadSeq
=
0
let
usersTrendLoadSeq
=
0
// Helper function to format date in local timezone
const
formatLocalDate
=
(
date
:
Date
):
string
=>
{
...
...
@@ -523,67 +524,74 @@ const onDateRangeChange = (range: {
}
// Load data
const
loadDashboardStats
=
async
()
=>
{
loading
.
value
=
true
try
{
stats
.
value
=
await
adminAPI
.
dashboard
.
getStats
()
}
catch
(
error
)
{
appStore
.
showError
(
t
(
'
admin.dashboard.failedToLoad
'
))
console
.
error
(
'
Error loading dashboard stats:
'
,
error
)
}
finally
{
loading
.
value
=
false
}
}
const
loadChartData
=
async
()
=>
{
const
loadDashboardSnapshot
=
async
(
includeStats
:
boolean
)
=>
{
const
currentSeq
=
++
chartLoadSeq
if
(
includeStats
&&
!
stats
.
value
)
{
loading
.
value
=
true
}
chartsLoading
.
value
=
true
userTrendLoading
.
value
=
true
try
{
const
params
=
{
const
response
=
await
adminAPI
.
dashboard
.
getSnapshotV2
(
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
granularity
:
granularity
.
value
}
const
[
trendResponse
,
modelResponse
]
=
await
Promise
.
all
([
adminAPI
.
dashboard
.
getUsageTrend
(
params
),
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
])
granularity
:
granularity
.
value
,
include_stats
:
includeStats
,
include_trend
:
true
,
include_model_stats
:
true
,
include_group_stats
:
false
,
include_users_trend
:
false
})
if
(
currentSeq
!==
chartLoadSeq
)
return
trendData
.
value
=
trendResponse
.
trend
||
[]
modelStats
.
value
=
modelResponse
.
models
||
[]
if
(
includeStats
&&
response
.
stats
)
{
stats
.
value
=
response
.
stats
}
trendData
.
value
=
response
.
trend
||
[]
modelStats
.
value
=
response
.
models
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading chart data:
'
,
error
)
appStore
.
showError
(
t
(
'
admin.dashboard.failedToLoad
'
))
console
.
error
(
'
Error loading dashboard snapshot:
'
,
error
)
}
finally
{
if
(
currentSeq
!==
chartLoadSeq
)
return
loading
.
value
=
false
chartsLoading
.
value
=
false
}
}
const
loadUsersTrend
=
async
()
=>
{
const
currentSeq
=
++
usersTrendLoadSeq
userTrendLoading
.
value
=
true
try
{
const
params
=
{
const
response
=
await
adminAPI
.
dashboard
.
getUserUsageTrend
(
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
granularity
:
granularity
.
value
,
limit
:
12
}
const
userResponse
=
await
adminAPI
.
dashboard
.
getUserUsageTrend
(
params
)
if
(
currentSeq
!==
chartLoadSeq
)
return
userTrend
.
value
=
userResponse
.
trend
||
[]
})
if
(
currentSeq
!==
usersTrendLoadSeq
)
return
userTrend
.
value
=
response
.
trend
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading user trend:
'
,
error
)
if
(
currentSeq
!==
usersTrendLoadSeq
)
return
console
.
error
(
'
Error loading users trend:
'
,
error
)
userTrend
.
value
=
[]
}
finally
{
if
(
currentSeq
!==
chart
LoadSeq
)
return
if
(
currentSeq
!==
usersTrend
LoadSeq
)
return
userTrendLoading
.
value
=
false
}
}
const
loadDashboardStats
=
async
()
=>
{
await
loadDashboardSnapshot
(
true
)
void
loadUsersTrend
()
}
const
loadChartData
=
async
()
=>
{
await
loadDashboardSnapshot
(
false
)
void
loadUsersTrend
()
}
onMounted
(()
=>
{
loadDashboardStats
()
loadChartData
()
})
</
script
>
...
...
frontend/src/views/admin/UsersView.vue
View file @
80ae592c
...
...
@@ -655,16 +655,28 @@ const saveColumnsToStorage = () => {
// Toggle column visibility
const
toggleColumn
=
(
key
:
string
)
=>
{
const
wasHidden
=
hiddenColumns
.
has
(
key
)
if
(
hiddenColumns
.
has
(
key
))
{
hiddenColumns
.
delete
(
key
)
}
else
{
hiddenColumns
.
add
(
key
)
}
saveColumnsToStorage
()
if
(
wasHidden
&&
(
key
===
'
usage
'
||
key
.
startsWith
(
'
attr_
'
)))
{
refreshCurrentPageSecondaryData
()
}
if
(
key
===
'
subscriptions
'
)
{
loadUsers
()
}
}
// Check if column is visible (not in hidden set)
const
isColumnVisible
=
(
key
:
string
)
=>
!
hiddenColumns
.
has
(
key
)
const
hasVisibleUsageColumn
=
computed
(()
=>
!
hiddenColumns
.
has
(
'
usage
'
))
const
hasVisibleSubscriptionsColumn
=
computed
(()
=>
!
hiddenColumns
.
has
(
'
subscriptions
'
))
const
hasVisibleAttributeColumns
=
computed
(()
=>
attributeDefinitions
.
value
.
some
((
def
)
=>
def
.
enabled
&&
!
hiddenColumns
.
has
(
`attr_
${
def
.
id
}
`
))
)
// Filtered columns based on visibility
const
columns
=
computed
<
Column
[]
>
(()
=>
...
...
@@ -776,6 +788,60 @@ const editingUser = ref<AdminUser | null>(null)
const
deletingUser
=
ref
<
AdminUser
|
null
>
(
null
)
const
viewingUser
=
ref
<
AdminUser
|
null
>
(
null
)
let
abortController
:
AbortController
|
null
=
null
let
secondaryDataSeq
=
0
const
loadUsersSecondaryData
=
async
(
userIds
:
number
[],
signal
?:
AbortSignal
,
expectedSeq
?:
number
)
=>
{
if
(
userIds
.
length
===
0
)
return
const
tasks
:
Promise
<
void
>
[]
=
[]
if
(
hasVisibleUsageColumn
.
value
)
{
tasks
.
push
(
(
async
()
=>
{
try
{
const
usageResponse
=
await
adminAPI
.
dashboard
.
getBatchUsersUsage
(
userIds
)
if
(
signal
?.
aborted
)
return
if
(
typeof
expectedSeq
===
'
number
'
&&
expectedSeq
!==
secondaryDataSeq
)
return
usageStats
.
value
=
usageResponse
.
stats
}
catch
(
e
)
{
if
(
signal
?.
aborted
)
return
console
.
error
(
'
Failed to load usage stats:
'
,
e
)
}
})()
)
}
if
(
attributeDefinitions
.
value
.
length
>
0
&&
hasVisibleAttributeColumns
.
value
)
{
tasks
.
push
(
(
async
()
=>
{
try
{
const
attrResponse
=
await
adminAPI
.
userAttributes
.
getBatchUserAttributes
(
userIds
)
if
(
signal
?.
aborted
)
return
if
(
typeof
expectedSeq
===
'
number
'
&&
expectedSeq
!==
secondaryDataSeq
)
return
userAttributeValues
.
value
=
attrResponse
.
attributes
}
catch
(
e
)
{
if
(
signal
?.
aborted
)
return
console
.
error
(
'
Failed to load user attribute values:
'
,
e
)
}
})()
)
}
if
(
tasks
.
length
>
0
)
{
await
Promise
.
allSettled
(
tasks
)
}
}
const
refreshCurrentPageSecondaryData
=
()
=>
{
const
userIds
=
users
.
value
.
map
((
u
)
=>
u
.
id
)
if
(
userIds
.
length
===
0
)
return
const
seq
=
++
secondaryDataSeq
void
loadUsersSecondaryData
(
userIds
,
undefined
,
seq
)
}
// Action Menu State
const
activeMenuId
=
ref
<
number
|
null
>
(
null
)
...
...
@@ -913,7 +979,8 @@ const loadUsers = async () => {
role
:
filters
.
role
as
any
,
status
:
filters
.
status
as
any
,
search
:
searchQuery
.
value
||
undefined
,
attributes
:
Object
.
keys
(
attrFilters
).
length
>
0
?
attrFilters
:
undefined
attributes
:
Object
.
keys
(
attrFilters
).
length
>
0
?
attrFilters
:
undefined
,
include_subscriptions
:
hasVisibleSubscriptionsColumn
.
value
},
{
signal
}
)
...
...
@@ -923,38 +990,17 @@ const loadUsers = async () => {
users
.
value
=
response
.
items
pagination
.
total
=
response
.
total
pagination
.
pages
=
response
.
pages
usageStats
.
value
=
{}
userAttributeValues
.
value
=
{}
//
Load usage stats and attribute values for all users in the li
st
//
Defer heavy secondary data so table can render fir
st
.
if
(
response
.
items
.
length
>
0
)
{
const
userIds
=
response
.
items
.
map
((
u
)
=>
u
.
id
)
// Load usage stats
try
{
const
usageResponse
=
await
adminAPI
.
dashboard
.
getBatchUsersUsage
(
userIds
)
if
(
signal
.
aborted
)
{
return
}
usageStats
.
value
=
usageResponse
.
stats
}
catch
(
e
)
{
if
(
signal
.
aborted
)
{
return
}
console
.
error
(
'
Failed to load usage stats:
'
,
e
)
}
// Load attribute values
if
(
attributeDefinitions
.
value
.
length
>
0
)
{
try
{
const
attrResponse
=
await
adminAPI
.
userAttributes
.
getBatchUserAttributes
(
userIds
)
if
(
signal
.
aborted
)
{
return
}
userAttributeValues
.
value
=
attrResponse
.
attributes
}
catch
(
e
)
{
if
(
signal
.
aborted
)
{
return
}
console
.
error
(
'
Failed to load user attribute values:
'
,
e
)
}
}
const
seq
=
++
secondaryDataSeq
window
.
setTimeout
(()
=>
{
if
(
signal
.
aborted
||
seq
!==
secondaryDataSeq
)
return
void
loadUsersSecondaryData
(
userIds
,
signal
,
seq
)
},
50
)
}
}
catch
(
error
:
any
)
{
const
errorInfo
=
error
as
{
name
?:
string
;
code
?:
string
}
...
...
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
80ae592c
...
...
@@ -586,6 +586,32 @@ async function refreshThroughputTrendWithCancel(fetchSeq: number, signal: AbortS
}
}
async
function
refreshCoreSnapshotWithCancel
(
fetchSeq
:
number
,
signal
:
AbortSignal
)
{
if
(
!
opsEnabled
.
value
)
return
loadingTrend
.
value
=
true
loadingErrorTrend
.
value
=
true
try
{
const
data
=
await
opsAPI
.
getDashboardSnapshotV2
(
buildApiParams
(),
{
signal
})
if
(
fetchSeq
!==
dashboardFetchSeq
)
return
overview
.
value
=
data
.
overview
throughputTrend
.
value
=
data
.
throughput_trend
errorTrend
.
value
=
data
.
error_trend
}
catch
(
err
:
any
)
{
if
(
fetchSeq
!==
dashboardFetchSeq
||
isCanceledRequest
(
err
))
return
// Fallback to legacy split endpoints when snapshot endpoint is unavailable.
await
Promise
.
all
([
refreshOverviewWithCancel
(
fetchSeq
,
signal
),
refreshThroughputTrendWithCancel
(
fetchSeq
,
signal
),
refreshErrorTrendWithCancel
(
fetchSeq
,
signal
)
])
}
finally
{
if
(
fetchSeq
===
dashboardFetchSeq
)
{
loadingTrend
.
value
=
false
loadingErrorTrend
.
value
=
false
}
}
}
async
function
refreshLatencyHistogramWithCancel
(
fetchSeq
:
number
,
signal
:
AbortSignal
)
{
if
(
!
opsEnabled
.
value
)
return
loadingLatency
.
value
=
true
...
...
@@ -640,6 +666,14 @@ async function refreshErrorDistributionWithCancel(fetchSeq: number, signal: Abor
}
}
async
function
refreshDeferredPanels
(
fetchSeq
:
number
,
signal
:
AbortSignal
)
{
if
(
!
opsEnabled
.
value
)
return
await
Promise
.
all
([
refreshLatencyHistogramWithCancel
(
fetchSeq
,
signal
),
refreshErrorDistributionWithCancel
(
fetchSeq
,
signal
)
])
}
function
isOpsDisabledError
(
err
:
unknown
):
boolean
{
return
(
!!
err
&&
...
...
@@ -662,12 +696,8 @@ async function fetchData() {
errorMessage
.
value
=
''
try
{
await
Promise
.
all
([
refreshOverviewWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshThroughputTrendWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshCoreSnapshotWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshSwitchTrendWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshLatencyHistogramWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshErrorTrendWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
),
refreshErrorDistributionWithCancel
(
fetchSeq
,
dashboardFetchController
.
signal
)
])
if
(
fetchSeq
!==
dashboardFetchSeq
)
return
...
...
@@ -680,6 +710,9 @@ async function fetchData() {
if
(
autoRefreshEnabled
.
value
)
{
autoRefreshCountdown
.
value
=
Math
.
floor
(
autoRefreshIntervalMs
.
value
/
1000
)
}
// Defer non-core visual panels to reduce initial blocking.
void
refreshDeferredPanels
(
fetchSeq
,
dashboardFetchController
.
signal
)
}
catch
(
err
)
{
if
(
!
isOpsDisabledError
(
err
))
{
console
.
error
(
'
[ops] failed to fetch dashboard data
'
,
err
)
...
...
Prev
1
2
Next
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