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
918a2538
Commit
918a2538
authored
Jan 14, 2026
by
IanShaw027
Browse files
feat(frontend): 完善ops监控面板和组件功能
parent
63711067
Changes
4
Show whitespace changes
Inline
Side-by-side
frontend/src/api/admin/ops.ts
View file @
918a2538
...
...
@@ -54,6 +54,7 @@ export type OpsUpstreamErrorEvent = {
account_name
?:
string
upstream_status_code
?:
number
upstream_request_id
?:
string
upstream_request_body
?:
string
kind
?:
string
message
?:
string
detail
?:
string
...
...
@@ -944,7 +945,9 @@ export async function getErrorDistribution(
return
data
}
export
async
function
listErrorLogs
(
params
:
{
export
type
OpsErrorListView
=
'
errors
'
|
'
excluded
'
|
'
all
'
export
type
OpsErrorListQueryParams
=
{
page
?:
number
page_size
?:
number
time_range
?:
string
...
...
@@ -958,10 +961,14 @@ export async function listErrorLogs(params: {
error_owner
?:
string
error_source
?:
string
resolved
?:
string
view
?:
OpsErrorListView
q
?:
string
status_codes
?:
string
}):
Promise
<
OpsErrorLogsResponse
>
{
}
// Legacy unified endpoints
export
async
function
listErrorLogs
(
params
:
OpsErrorListQueryParams
):
Promise
<
OpsErrorLogsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorLogsResponse
>
(
'
/admin/ops/errors
'
,
{
params
})
return
data
}
...
...
@@ -985,6 +992,50 @@ export async function updateErrorResolved(errorId: number, resolved: boolean): P
await
apiClient
.
put
(
`/admin/ops/errors/
${
errorId
}
/resolve`
,
{
resolved
})
}
// New split endpoints
export
async
function
listRequestErrors
(
params
:
OpsErrorListQueryParams
):
Promise
<
OpsErrorLogsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorLogsResponse
>
(
'
/admin/ops/request-errors
'
,
{
params
})
return
data
}
export
async
function
listUpstreamErrors
(
params
:
OpsErrorListQueryParams
):
Promise
<
OpsErrorLogsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorLogsResponse
>
(
'
/admin/ops/upstream-errors
'
,
{
params
})
return
data
}
export
async
function
getRequestErrorDetail
(
id
:
number
):
Promise
<
OpsErrorDetail
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorDetail
>
(
`/admin/ops/request-errors/
${
id
}
`
)
return
data
}
export
async
function
getUpstreamErrorDetail
(
id
:
number
):
Promise
<
OpsErrorDetail
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsErrorDetail
>
(
`/admin/ops/upstream-errors/
${
id
}
`
)
return
data
}
export
async
function
retryRequestErrorClient
(
id
:
number
):
Promise
<
OpsRetryResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
OpsRetryResult
>
(
`/admin/ops/request-errors/
${
id
}
/retry-client`
,
{})
return
data
}
export
async
function
retryRequestErrorUpstreamEvent
(
id
:
number
,
idx
:
number
):
Promise
<
OpsRetryResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
OpsRetryResult
>
(
`/admin/ops/request-errors/
${
id
}
/upstream-errors/
${
idx
}
/retry`
,
{})
return
data
}
export
async
function
retryUpstreamError
(
id
:
number
):
Promise
<
OpsRetryResult
>
{
const
{
data
}
=
await
apiClient
.
post
<
OpsRetryResult
>
(
`/admin/ops/upstream-errors/
${
id
}
/retry`
,
{})
return
data
}
export
async
function
updateRequestErrorResolved
(
errorId
:
number
,
resolved
:
boolean
):
Promise
<
void
>
{
await
apiClient
.
put
(
`/admin/ops/request-errors/
${
errorId
}
/resolve`
,
{
resolved
})
}
export
async
function
updateUpstreamErrorResolved
(
errorId
:
number
,
resolved
:
boolean
):
Promise
<
void
>
{
await
apiClient
.
put
(
`/admin/ops/upstream-errors/
${
errorId
}
/resolve`
,
{
resolved
})
}
export
async
function
listRequestDetails
(
params
:
OpsRequestDetailsParams
):
Promise
<
OpsRequestDetailsResponse
>
{
const
{
data
}
=
await
apiClient
.
get
<
OpsRequestDetailsResponse
>
(
'
/admin/ops/requests
'
,
{
params
})
return
data
...
...
@@ -1103,11 +1154,25 @@ export const opsAPI = {
getAccountAvailabilityStats
,
getRealtimeTrafficSummary
,
subscribeQPS
,
// Legacy unified endpoints
listErrorLogs
,
getErrorLogDetail
,
retryErrorRequest
,
listRetryAttempts
,
updateErrorResolved
,
// New split endpoints
listRequestErrors
,
listUpstreamErrors
,
getRequestErrorDetail
,
getUpstreamErrorDetail
,
retryRequestErrorClient
,
retryRequestErrorUpstreamEvent
,
retryUpstreamError
,
updateRequestErrorResolved
,
updateUpstreamErrorResolved
,
listRequestDetails
,
listAlertRules
,
createAlertRule
,
...
...
frontend/src/views/admin/ops/OpsDashboard.vue
View file @
918a2538
...
...
@@ -94,7 +94,7 @@
@
openErrorDetail=
"openError"
/>
<OpsErrorDetailModal
v-model:show=
"showErrorModal"
:error-id=
"selectedErrorId"
/>
<OpsErrorDetailModal
v-model:show=
"showErrorModal"
:error-id=
"selectedErrorId"
:error-type=
"errorDetailsType"
/>
<OpsRequestDetailsModal
v-model=
"showRequestDetails"
...
...
frontend/src/views/admin/ops/components/OpsErrorDetailModal.vue
View file @
918a2538
...
...
@@ -247,11 +247,11 @@
{{
t
(
'
admin.ops.errorDetail.retryClient
'
)
}}
<
/button
>
<
button
v
-
if
=
"
props.errorType === 'upstream'
"
type
=
"
button
"
class
=
"
btn btn-secondary btn-sm
"
:
disabled
=
"
retrying
|| !pinnedAccountId
"
:
disabled
=
"
retrying
"
@
click
=
"
openRetryConfirm('upstream')
"
:
title
=
"
pinnedAccountId ? '' : t('admin.ops.errorDetail.retryUpstreamHint')
"
>
{{
t
(
'
admin.ops.errorDetail.retryUpstream
'
)
}}
<
/button
>
...
...
@@ -263,12 +263,12 @@
<
/div
>
<
div
class
=
"
mt-4 grid grid-cols-1 gap-4 md:grid-cols-3
"
>
<
div
v
-
if
=
"
props.errorType === 'upstream'
"
class
=
"
mt-4 grid grid-cols-1 gap-4 md:grid-cols-3
"
>
<
div
class
=
"
md:col-span-1
"
>
<
label
class
=
"
mb-1 block text-xs font-bold uppercase tracking-wider text-gray-400
"
>
{{
t
(
'
admin.ops.errorDetail.pinnedAccountId
'
)
}}
<
/label
>
<
input
v
-
model
=
"
pinnedAccountIdInput
"
type
=
"
text
"
class
=
"
input font-mono text-sm
"
:
placeholder
=
"
t('admin.ops.errorDetail.pinnedAccountIdHint')
"
/>
<
input
v
-
model
=
"
pinnedAccountIdInput
"
type
=
"
text
"
class
=
"
input font-mono text-sm
"
disabled
/>
<
div
class
=
"
mt-1 text-xs text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.ops.errorDetail.retryNote2
'
)
}}
pinned
to
original
account_id
<
/div
>
<
/div
>
<
div
class
=
"
md:col-span-2
"
>
...
...
@@ -327,10 +327,22 @@
<
div
class
=
"
text-xs font-black text-gray-800 dark:text-gray-100
"
>
#
{{
idx
+
1
}}
<
span
v
-
if
=
"
ev.kind
"
class
=
"
font-mono
"
>
{{
ev
.
kind
}}
<
/span
>
<
/div
>
<
div
class
=
"
flex items-center gap-2
"
>
<
button
v
-
if
=
"
props.errorType !== 'upstream'
"
type
=
"
button
"
class
=
"
rounded-md bg-gray-100 px-2 py-1 text-[10px] font-bold text-gray-700 hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-200 dark:hover:bg-dark-600
"
:
disabled
=
"
retrying || !ev.upstream_request_body
"
:
title
=
"
ev.upstream_request_body ? '' : 'missing upstream request body'
"
@
click
.
stop
=
"
retryUpstreamEvent(idx)
"
>
{{
t
(
'
admin.ops.errorDetail.retryUpstream
'
)
}}
#
{{
idx
+
1
}}
<
/button
>
<
div
class
=
"
font-mono text-xs text-gray-500 dark:text-gray-400
"
>
{{
ev
.
at_unix_ms
?
formatDateTime
(
new
Date
(
ev
.
at_unix_ms
))
:
''
}}
<
/div
>
<
/div
>
<
/div
>
<
div
class
=
"
mt-2 grid grid-cols-1 gap-2 text-xs text-gray-600 dark:text-gray-300 sm:grid-cols-2
"
>
<
div
>
...
...
@@ -526,13 +538,14 @@ import { useI18n } from 'vue-i18n'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
ConfirmDialog
from
'
@/components/common/ConfirmDialog.vue
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
opsAPI
,
type
OpsErrorDetail
,
type
OpsRetryMode
,
type
OpsRetryAttempt
}
from
'
@/api/admin/ops
'
import
{
opsAPI
,
type
OpsErrorDetail
,
type
OpsRetryAttempt
}
from
'
@/api/admin/ops
'
import
{
formatDateTime
}
from
'
@/utils/format
'
import
{
getSeverityClass
}
from
'
../utils/opsFormatters
'
interface
Props
{
show
:
boolean
errorId
:
number
|
null
errorType
?:
'
request
'
|
'
upstream
'
}
interface
Emits
{
...
...
@@ -552,7 +565,7 @@ const activeTab = ref<'overview' | 'retries' | 'request' | 'response'>('overview
const
retrying
=
ref
(
false
)
const
showRetryConfirm
=
ref
(
false
)
const
pendingRetryMode
=
ref
<
OpsRetryMode
>
(
'
client
'
)
const
pendingRetryMode
=
ref
<
'
client
'
|
'
upstream
'
|
'
upstream_event
'
>
(
'
client
'
)
const
forceRetryAck
=
ref
(
false
)
const
retryHistory
=
ref
<
OpsRetryAttempt
[]
>
([])
...
...
@@ -563,12 +576,6 @@ const compareA = ref<number | null>(null)
const
compareB
=
ref
<
number
|
null
>
(
null
)
const
pinnedAccountIdInput
=
ref
(
''
)
const
pinnedAccountId
=
computed
<
number
|
null
>
(()
=>
{
const
raw
=
String
(
pinnedAccountIdInput
.
value
||
''
).
trim
()
if
(
!
raw
)
return
null
const
n
=
Number
.
parseInt
(
raw
,
10
)
return
Number
.
isFinite
(
n
)
&&
n
>
0
?
n
:
null
}
)
const
title
=
computed
(()
=>
{
if
(
!
props
.
errorId
)
return
'
Error Detail
'
...
...
@@ -584,6 +591,7 @@ type UpstreamErrorEvent = {
account_name
?:
string
upstream_status_code
?:
number
upstream_request_id
?:
string
upstream_request_body
?:
string
kind
?:
string
message
?:
string
detail
?:
string
...
...
@@ -641,15 +649,12 @@ const handlingSuggestion = computed(() => {
async
function
fetchDetail
(
id
:
number
)
{
loading
.
value
=
true
try
{
const
d
=
await
opsAPI
.
getErrorLogDetail
(
id
)
const
kind
=
props
.
errorType
||
(
detail
.
value
?.
phase
===
'
upstream
'
?
'
upstream
'
:
'
request
'
)
const
d
=
kind
===
'
upstream
'
?
await
opsAPI
.
getUpstreamErrorDetail
(
id
)
:
await
opsAPI
.
getRequestErrorDetail
(
id
)
detail
.
value
=
d
// Default pinned account from error log if present.
if
(
d
.
account_id
&&
d
.
account_id
>
0
)
{
pinnedAccountIdInput
.
value
=
String
(
d
.
account_id
)
}
else
{
pinnedAccountIdInput
.
value
=
''
}
// Keep showing original account_id (read-only hint for upstream retries).
pinnedAccountIdInput
.
value
=
d
.
account_id
&&
d
.
account_id
>
0
?
String
(
d
.
account_id
)
:
''
}
catch
(
err
:
any
)
{
detail
.
value
=
null
appStore
.
showError
(
err
?.
message
||
t
(
'
admin.ops.failedToLoadErrorDetail
'
))
...
...
@@ -679,7 +684,7 @@ watch(
{
immediate
:
true
}
)
function
openRetryConfirm
(
mode
:
OpsRetryMode
)
{
function
openRetryConfirm
(
mode
:
'
client
'
|
'
upstream
'
|
'
upstream_event
'
)
{
pendingRetryMode
.
value
=
mode
// Force-ack required only when backend says not retryable.
forceRetryAck
.
value
=
false
...
...
@@ -733,7 +738,12 @@ const responseTabHint = computed(() => {
async
function
markResolved
(
resolved
:
boolean
)
{
if
(
!
props
.
errorId
)
return
try
{
await
opsAPI
.
updateErrorResolved
(
props
.
errorId
,
resolved
)
const
kind
=
props
.
errorType
||
(
detail
.
value
?.
phase
===
'
upstream
'
?
'
upstream
'
:
'
request
'
)
if
(
kind
===
'
upstream
'
)
{
await
opsAPI
.
updateUpstreamErrorResolved
(
props
.
errorId
,
resolved
)
}
else
{
await
opsAPI
.
updateRequestErrorResolved
(
props
.
errorId
,
resolved
)
}
await
fetchDetail
(
props
.
errorId
)
appStore
.
showSuccess
(
resolved
?
(
t
(
'
admin.ops.errorDetails.resolved
'
)
||
'
Resolved
'
)
:
(
t
(
'
admin.ops.errorDetails.unresolved
'
)
||
'
Unresolved
'
))
}
catch
(
err
:
any
)
{
...
...
@@ -779,12 +789,20 @@ async function runConfirmedRetry() {
retrying
.
value
=
true
try
{
const
req
=
mode
===
'
upstream
'
?
{
mode
,
pinned_account_id
:
pinnedAccountId
.
value
??
undefined
,
force
:
!
retryable
?
true
:
undefined
}
:
{
mode
,
force
:
!
retryable
?
true
:
undefined
}
const
kind
=
props
.
errorType
||
(
detail
.
value
?.
phase
===
'
upstream
'
?
'
upstream
'
:
'
request
'
)
let
res
if
(
kind
===
'
upstream
'
)
{
// Upstream error retries always pin the original account_id.
res
=
await
opsAPI
.
retryUpstreamError
(
props
.
errorId
)
}
else
{
if
(
mode
===
'
client
'
)
{
res
=
await
opsAPI
.
retryRequestErrorClient
(
props
.
errorId
)
}
else
{
throw
new
Error
(
'
Unsupported retry mode
'
)
}
}
const
res
=
await
opsAPI
.
retryErrorRequest
(
props
.
errorId
,
req
)
const
summary
=
res
.
status
===
'
succeeded
'
?
t
(
'
admin.ops.errorDetail.retrySuccess
'
)
:
t
(
'
admin.ops.errorDetail.retryFailed
'
)
appStore
.
showSuccess
(
summary
)
...
...
@@ -798,6 +816,22 @@ async function runConfirmedRetry() {
}
}
async
function
retryUpstreamEvent
(
idx
:
number
)
{
if
(
!
props
.
errorId
)
return
try
{
retrying
.
value
=
true
const
res
=
await
opsAPI
.
retryRequestErrorUpstreamEvent
(
props
.
errorId
,
idx
)
const
summary
=
res
.
status
===
'
succeeded
'
?
t
(
'
admin.ops.errorDetail.retrySuccess
'
)
:
t
(
'
admin.ops.errorDetail.retryFailed
'
)
appStore
.
showSuccess
(
summary
)
await
fetchDetail
(
props
.
errorId
)
await
loadRetryHistory
()
}
catch
(
err
:
any
)
{
appStore
.
showError
(
err
?.
message
||
t
(
'
admin.ops.retryFailed
'
))
}
finally
{
retrying
.
value
=
false
}
}
function
cancelRetry
()
{
showRetryConfirm
.
value
=
false
}
...
...
frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue
View file @
918a2538
...
...
@@ -32,7 +32,9 @@ const q = ref('')
const
statusCode
=
ref
<
number
|
null
>
(
null
)
const
phase
=
ref
<
string
>
(
''
)
const
errorOwner
=
ref
<
string
>
(
''
)
const
resolvedStatus
=
ref
<
string
>
(
'
unresolved
'
)
const
resolvedStatus
=
ref
<
string
>
(
'
unresolved
'
)
const
viewMode
=
ref
<
'
errors
'
|
'
excluded
'
|
'
all
'
>
(
'
errors
'
)
const
modalTitle
=
computed
(()
=>
{
return
props
.
errorType
===
'
upstream
'
?
t
(
'
admin.ops.errorDetails.upstreamErrors
'
)
:
t
(
'
admin.ops.errorDetails.requestErrors
'
)
...
...
@@ -63,6 +65,14 @@ const resolvedSelectOptions = computed(() => {
]
})
const
viewModeSelectOptions
=
computed
(()
=>
{
return
[
{
value
:
'
errors
'
,
label
:
t
(
'
admin.ops.errorDetails.viewErrors
'
)
||
'
errors
'
},
{
value
:
'
excluded
'
,
label
:
t
(
'
admin.ops.errorDetails.viewExcluded
'
)
||
'
excluded
'
},
{
value
:
'
all
'
,
label
:
t
(
'
common.all
'
)
}
]
})
const
phaseSelectOptions
=
computed
(()
=>
{
const
options
=
[
{
value
:
''
,
label
:
t
(
'
common.all
'
)
},
...
...
@@ -88,7 +98,8 @@ async function fetchErrorLogs() {
const
params
:
Record
<
string
,
any
>
=
{
page
:
page
.
value
,
page_size
:
pageSize
.
value
,
time_range
:
props
.
timeRange
time_range
:
props
.
timeRange
,
view
:
viewMode
.
value
}
const
platform
=
String
(
props
.
platform
||
''
).
trim
()
...
...
@@ -109,7 +120,9 @@ async function fetchErrorLogs() {
else
if
(
resolvedVal
===
'
unresolved
'
)
params
.
resolved
=
'
false
'
// 'all' -> omit
const
res
=
await
opsAPI
.
listErrorLogs
(
params
)
const
res
=
props
.
errorType
===
'
upstream
'
?
await
opsAPI
.
listUpstreamErrors
(
params
)
:
await
opsAPI
.
listRequestErrors
(
params
)
rows
.
value
=
res
.
items
||
[]
total
.
value
=
res
.
total
||
0
}
catch
(
err
)
{
...
...
@@ -121,15 +134,17 @@ async function fetchErrorLogs() {
}
}
function
resetFilters
()
{
function
resetFilters
()
{
q
.
value
=
''
statusCode
.
value
=
null
phase
.
value
=
props
.
errorType
===
'
upstream
'
?
'
upstream
'
:
''
errorOwner
.
value
=
''
resolvedStatus
.
value
=
'
unresolved
'
viewMode
.
value
=
'
errors
'
page
.
value
=
1
fetchErrorLogs
()
}
}
watch
(
()
=>
props
.
show
,
...
...
@@ -172,7 +187,7 @@ watch(
)
watch
(
()
=>
[
statusCode
.
value
,
phase
.
value
,
errorOwner
.
value
,
resolvedStatus
.
value
]
as
const
,
()
=>
[
statusCode
.
value
,
phase
.
value
,
errorOwner
.
value
,
resolvedStatus
.
value
,
viewMode
.
value
]
as
const
,
()
=>
{
if
(
!
props
.
show
)
return
page
.
value
=
1
...
...
@@ -186,7 +201,7 @@ watch(
<div
class=
"flex h-full min-h-0 flex-col"
>
<!-- Filters -->
<div
class=
"mb-4 flex-shrink-0 border-b border-gray-200 pb-4 dark:border-dark-700"
>
<div
class=
"grid grid-cols-
7
gap-2"
>
<div
class=
"grid grid-cols-
8
gap-2"
>
<div
class=
"col-span-2 compact-select"
>
<div
class=
"relative group"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"
>
...
...
@@ -224,6 +239,10 @@ watch(
<Select
:model-value=
"resolvedStatus"
:options=
"resolvedSelectOptions"
@
update:model-value=
"resolvedStatus = String($event ?? 'unresolved')"
/>
</div>
<div
class=
"compact-select"
>
<Select
:model-value=
"viewMode"
:options=
"viewModeSelectOptions"
@
update:model-value=
"viewMode = $event as any"
/>
</div>
<div
class=
"flex items-center justify-end"
>
<button
type=
"button"
class=
"rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-dark-700 dark:text-gray-300 dark:hover:bg-dark-600"
@
click=
"resetFilters"
>
{{
t
(
'
common.reset
'
)
}}
...
...
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