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
0e448297
Commit
0e448297
authored
Jan 13, 2026
by
yangjianbo
Browse files
Merge branch 'main' into dev
parents
9618cb56
93db889a
Changes
47
Hide whitespace changes
Inline
Side-by-side
frontend/src/views/admin/ops/components/OpsErrorDetailsModal.vue
View file @
0e448297
...
...
@@ -174,69 +174,75 @@ watch(
<
template
>
<BaseDialog
:show=
"show"
:title=
"modalTitle"
width=
"full"
@
close=
"close"
>
<!-- Filters -->
<div
class=
"border-b border-gray-200 pb-4 mb-4 dark:border-dark-700"
>
<div
class=
"grid grid-cols-1 gap-4 lg:grid-cols-12"
>
<div
class=
"lg:col-span-5"
>
<div
class=
"relative group"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5"
>
<svg
class=
"h-4 w-4 text-gray-400 transition-colors group-focus-within:text-blue-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2.5"
d=
"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<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-1 gap-4 lg:grid-cols-12"
>
<div
class=
"lg:col-span-5"
>
<div
class=
"relative group"
>
<div
class=
"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5"
>
<svg
class=
"h-4 w-4 text-gray-400 transition-colors group-focus-within:text-blue-500"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
>
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2.5"
d=
"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
<input
v-model=
"q"
type=
"text"
class=
"w-full rounded-2xl border-gray-200 bg-gray-50/50 py-2 pl-10 pr-4 text-sm font-medium text-gray-700 transition-all focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-500/10 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:focus:bg-dark-800"
:placeholder=
"t('admin.ops.errorDetails.searchPlaceholder')"
/>
</div>
</div>
<div
class=
"lg:col-span-2"
>
<Select
:model-value=
"statusCode"
:options=
"statusCodeSelectOptions"
class=
"w-full"
@
update:model-value=
"statusCode = $event as any"
/>
</div>
<div
class=
"lg:col-span-2"
>
<Select
:model-value=
"phase"
:options=
"phaseSelectOptions"
class=
"w-full"
@
update:model-value=
"phase = String($event ?? '')"
/>
</div>
<div
class=
"lg:col-span-2"
>
<input
v-model=
"
q
"
v-model=
"
accountIdInput
"
type=
"text"
class=
"w-full rounded-2xl border-gray-200 bg-gray-50/50 py-2 pl-10 pr-4 text-sm font-medium text-gray-700 transition-all focus:border-blue-500 focus:bg-white focus:ring-4 focus:ring-blue-500/10 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:focus:bg-dark-800"
:placeholder=
"t('admin.ops.errorDetails.searchPlaceholder')"
inputmode=
"numeric"
class=
"input w-full text-sm"
:placeholder=
"t('admin.ops.errorDetails.accountIdPlaceholder')"
/>
</div>
</div>
<div
class=
"lg:col-span-2"
>
<Select
:model-value=
"statusCode"
:options=
"statusCodeSelectOptions"
class=
"w-full"
@
update:model-value=
"statusCode = $event as any"
/>
</div>
<div
class=
"lg:col-span-2"
>
<Select
:model-value=
"phase"
:options=
"phaseSelectOptions"
class=
"w-full"
@
update:model-value=
"phase = String($event ?? '')"
/>
<div
class=
"lg:col-span-1 flex items-center justify-end"
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"resetFilters"
>
{{
t
(
'
common.reset
'
)
}}
</button>
</div>
</div>
</div>
<div
class=
"lg:col-span-2"
>
<input
v-model=
"accountIdInput"
type=
"text"
inputmode=
"numeric"
class=
"input w-full text-sm"
:placeholder=
"t('admin.ops.errorDetails.accountIdPlaceholder')"
/>
<!-- Body -->
<div
class=
"flex min-h-0 flex-1 flex-col"
>
<div
class=
"mb-2 flex-shrink-0 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.errorDetails.total
'
)
}}
{{
total
}}
</div>
<div
class=
"lg:col-span-1 flex items-center justify-end"
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"resetFilters"
>
{{
t
(
'
common.reset
'
)
}}
</button>
</div>
<OpsErrorLogTable
class=
"min-h-0 flex-1"
:rows=
"rows"
:total=
"total"
:loading=
"loading"
:page=
"page"
:page-size=
"pageSize"
@
openErrorDetail=
"emit('openErrorDetail', $event)"
@
update:page=
"page = $event"
@
update:pageSize=
"pageSize = $event"
/>
</div>
</div>
<!-- Body -->
<div
class=
"text-xs text-gray-500 dark:text-gray-400 mb-2"
>
{{
t
(
'
admin.ops.errorDetails.total
'
)
}}
{{
total
}}
</div>
<OpsErrorLogTable
:rows=
"rows"
:total=
"total"
:loading=
"loading"
:page=
"page"
:page-size=
"pageSize"
@
openErrorDetail=
"emit('openErrorDetail', $event)"
@
update:page=
"page = $event"
@
update:pageSize=
"pageSize = $event"
/>
</BaseDialog>
</
template
>
frontend/src/views/admin/ops/components/OpsErrorLogTable.vue
View file @
0e448297
<
template
>
<div>
<div
v-if=
"loading"
class=
"flex items-center justify-center py-10"
>
<div
class=
"flex h-full min-h-0 flex-col"
>
<div
v-if=
"loading"
class=
"flex
flex-1
items-center justify-center py-10"
>
<div
class=
"h-8 w-8 animate-spin rounded-full border-b-2 border-primary-600"
></div>
</div>
<div
v-else
class=
"overflow-x-auto"
>
<table
class=
"min-w-full divide-y divide-gray-200 dark:divide-dark-700"
>
<thead
class=
"sticky top-0 z-10 bg-gray-50/50 dark:bg-dark-800/50"
>
<tr>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
<div
v-else
class=
"flex min-h-0 flex-1 flex-col"
>
<div
class=
"min-h-0 flex-1 overflow-auto"
>
<table
class=
"min-w-full divide-y divide-gray-200 dark:divide-dark-700"
>
<thead
class=
"sticky top-0 z-10 bg-gray-50/50 dark:bg-dark-800/50"
>
<tr>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.timeId
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.context
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.status
'
)
}}
</th>
<th
scope=
"col"
class=
"px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.message
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.latency
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.action
'
)
}}
</th>
</tr>
</thead>
<tbody
class=
"divide-y divide-gray-100 dark:divide-dark-700"
>
<tr
v-if=
"rows.length === 0"
class=
"bg-white dark:bg-dark-900"
>
<td
colspan=
"6"
class=
"py-16 text-center text-sm text-gray-400 dark:text-dark-500"
>
{{
t
(
'
admin.ops.errorLog.noErrors
'
)
}}
</td>
</tr>
<tr
v-for=
"log in rows"
:key=
"log.id"
class=
"group cursor-pointer transition-all duration-200 hover:bg-gray-50/80 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:hover:bg-dark-800/50 dark:focus:ring-offset-dark-900"
tabindex=
"0"
role=
"button"
@
click=
"emit('openErrorDetail', log.id)"
@
keydown.enter.prevent=
"emit('openErrorDetail', log.id)"
@
keydown.space.prevent=
"emit('openErrorDetail', log.id)"
>
{{
t
(
'
admin.ops.errorLog.timeId
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.context
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.status
'
)
}}
</th>
<th
scope=
"col"
class=
"px-6 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.message
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.latency
'
)
}}
</th>
<th
scope=
"col"
class=
"whitespace-nowrap px-6 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-500 dark:text-dark-400"
>
{{
t
(
'
admin.ops.errorLog.action
'
)
}}
</th>
</tr>
</thead>
<tbody
class=
"divide-y divide-gray-100 dark:divide-dark-700"
>
<tr
v-if=
"rows.length === 0"
class=
"bg-white dark:bg-dark-900"
>
<td
colspan=
"6"
class=
"py-16 text-center text-sm text-gray-400 dark:text-dark-500"
>
{{
t
(
'
admin.ops.errorLog.noErrors
'
)
}}
</td>
</tr>
<tr
v-for=
"log in rows"
:key=
"log.id"
class=
"group cursor-pointer transition-all duration-200 hover:bg-gray-50/80 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:hover:bg-dark-800/50 dark:focus:ring-offset-dark-900"
tabindex=
"0"
role=
"button"
@
click=
"emit('openErrorDetail', log.id)"
@
keydown.enter.prevent=
"emit('openErrorDetail', log.id)"
@
keydown.space.prevent=
"emit('openErrorDetail', log.id)"
>
<!-- Time & ID -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-col gap-0.5"
>
<span
class=
"font-mono text-xs font-bold text-gray-900 dark:text-gray-200"
>
{{
formatDateTime
(
log
.
created_at
).
split
(
'
'
)[
1
]
}}
</span>
<span
class=
"font-mono text-[10px] text-gray-400 transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400"
:title=
"log.request_id || log.client_request_id"
>
{{
(
log
.
request_id
||
log
.
client_request_id
||
''
).
substring
(
0
,
12
)
}}
</span>
</div>
</td>
<!-- Context (Platform/Model) -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-col items-start gap-1.5"
>
<span
class=
"inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-tight text-gray-600 dark:bg-dark-700 dark:text-gray-300"
>
{{
log
.
platform
||
'
-
'
}}
</span>
<span
v-if=
"log.model"
class=
"max-w-[160px] truncate font-mono text-[10px] text-gray-500 dark:text-dark-400"
:title=
"log.model"
>
{{
log
.
model
}}
</span>
<div
v-if=
"log.group_id || log.account_id"
class=
"flex flex-wrap items-center gap-2 font-mono text-[10px] font-semibold text-gray-400 dark:text-dark-500"
>
<span
v-if=
"log.group_id"
>
{{
t
(
'
admin.ops.errorLog.grp
'
)
}}
{{
log
.
group_id
}}
</span>
<span
v-if=
"log.account_id"
>
{{
t
(
'
admin.ops.errorLog.acc
'
)
}}
{{
log
.
account_id
}}
</span>
</div>
</div>
</td>
<!-- Status & Severity -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-wrap items-center gap-2"
>
<span
:class=
"[
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-black ring-1 ring-inset shadow-sm',
getStatusClass(log.status_code)
]"
>
{{
log
.
status_code
}}
</span>
<span
v-if=
"log.severity"
:class=
"['rounded-md px-2 py-0.5 text-[10px] font-black shadow-sm', getSeverityClass(log.severity)]"
>
{{
log
.
severity
}}
</span>
</div>
</td>
<!-- Message -->
<td
class=
"px-6 py-4"
>
<div
class=
"max-w-md lg:max-w-2xl"
>
<p
class=
"truncate text-xs font-semibold text-gray-700 dark:text-gray-300"
:title=
"log.message"
>
{{
formatSmartMessage
(
log
.
message
)
||
'
-
'
}}
</p>
<div
class=
"mt-1.5 flex flex-wrap gap-x-3 gap-y-1"
>
<div
v-if=
"log.phase"
class=
"flex items-center gap-1"
>
<span
class=
"h-1 w-1 rounded-full bg-gray-300"
></span>
<span
class=
"text-[9px] font-black uppercase tracking-tighter text-gray-400"
>
{{
log
.
phase
}}
</span>
<!-- Time & ID -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-col gap-0.5"
>
<span
class=
"font-mono text-xs font-bold text-gray-900 dark:text-gray-200"
>
{{
formatDateTime
(
log
.
created_at
).
split
(
'
'
)[
1
]
}}
</span>
<span
class=
"font-mono text-[10px] text-gray-400 transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400"
:title=
"log.request_id || log.client_request_id"
>
{{
(
log
.
request_id
||
log
.
client_request_id
||
''
).
substring
(
0
,
12
)
}}
</span>
</div>
</td>
<!-- Context (Platform/Model) -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-col items-start gap-1.5"
>
<span
class=
"inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-tight text-gray-600 dark:bg-dark-700 dark:text-gray-300"
>
{{
log
.
platform
||
'
-
'
}}
</span>
<span
v-if=
"log.model"
class=
"max-w-[160px] truncate font-mono text-[10px] text-gray-500 dark:text-dark-400"
:title=
"log.model"
>
{{
log
.
model
}}
</span>
<div
v-if=
"log.group_id || log.account_id"
class=
"flex flex-wrap items-center gap-2 font-mono text-[10px] font-semibold text-gray-400 dark:text-dark-500"
>
<span
v-if=
"log.group_id"
>
{{
t
(
'
admin.ops.errorLog.grp
'
)
}}
{{
log
.
group_id
}}
</span>
<span
v-if=
"log.account_id"
>
{{
t
(
'
admin.ops.errorLog.acc
'
)
}}
{{
log
.
account_id
}}
</span>
</div>
<div
v-if=
"log.client_ip"
class=
"flex items-center gap-1"
>
<span
class=
"h-1 w-1 rounded-full bg-gray-300"
></span>
<span
class=
"text-[9px] font-mono font-bold text-gray-400"
>
{{
log
.
client_ip
}}
</span>
</div>
</td>
<!-- Status & Severity -->
<td
class=
"px-6 py-4"
>
<div
class=
"flex flex-wrap items-center gap-2"
>
<span
:class=
"[
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-black ring-1 ring-inset shadow-sm',
getStatusClass(log.status_code)
]"
>
{{
log
.
status_code
}}
</span>
<span
v-if=
"log.severity"
:class=
"['rounded-md px-2 py-0.5 text-[10px] font-black shadow-sm', getSeverityClass(log.severity)]"
>
{{
log
.
severity
}}
</span>
</div>
</td>
<!-- Message -->
<td
class=
"px-6 py-4"
>
<div
class=
"max-w-md lg:max-w-2xl"
>
<p
class=
"truncate text-xs font-semibold text-gray-700 dark:text-gray-300"
:title=
"log.message"
>
{{
formatSmartMessage
(
log
.
message
)
||
'
-
'
}}
</p>
<div
class=
"mt-1.5 flex flex-wrap gap-x-3 gap-y-1"
>
<div
v-if=
"log.phase"
class=
"flex items-center gap-1"
>
<span
class=
"h-1 w-1 rounded-full bg-gray-300"
></span>
<span
class=
"text-[9px] font-black uppercase tracking-tighter text-gray-400"
>
{{
log
.
phase
}}
</span>
</div>
<div
v-if=
"log.client_ip"
class=
"flex items-center gap-1"
>
<span
class=
"h-1 w-1 rounded-full bg-gray-300"
></span>
<span
class=
"text-[9px] font-mono font-bold text-gray-400"
>
{{
log
.
client_ip
}}
</span>
</div>
</div>
</div>
</div>
</td>
<!-- Latency -->
<td
class=
"px-6 py-4 text-right"
>
<div
class=
"flex flex-col items-end"
>
<span
class=
"font-mono text-xs font-black"
:class=
"getLatencyClass(log.latency_ms ?? null)"
>
{{
log
.
latency_ms
!=
null
?
Math
.
round
(
log
.
latency_ms
)
+
'
ms
'
:
'
--
'
}}
</span>
</div>
</td>
<!-- Actions -->
<td
class=
"px-6 py-4 text-right"
@
click.stop
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"emit('openErrorDetail', log.id)"
>
{{
t
(
'
admin.ops.errorLog.details
'
)
}}
</button>
</td>
</tr>
</tbody>
</table>
</td>
<!-- Latency -->
<td
class=
"px-6 py-4 text-right"
>
<div
class=
"flex flex-col items-end"
>
<span
class=
"font-mono text-xs font-black"
:class=
"getLatencyClass(log.latency_ms ?? null)"
>
{{
log
.
latency_ms
!=
null
?
Math
.
round
(
log
.
latency_ms
)
+
'
ms
'
:
'
--
'
}}
</span>
</div>
</td>
<!-- Actions -->
<td
class=
"px-6 py-4 text-right"
@
click.stop
>
<button
type=
"button"
class=
"btn btn-secondary btn-sm"
@
click=
"emit('openErrorDetail', log.id)"
>
{{
t
(
'
admin.ops.errorLog.details
'
)
}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<Pagination
v-if=
"total > 0"
:total=
"total"
:page=
"page"
:page-size=
"pageSize"
:page-size-options=
"[10, 20, 50, 100, 200, 500]"
@
update:page=
"emit('update:page', $event)"
@
update:pageSize=
"emit('update:pageSize', $event)"
/>
</div>
<Pagination
v-if=
"total > 0"
:total=
"total"
:page=
"page"
:page-size=
"pageSize"
:page-size-options=
"[10, 20, 50, 100, 200, 500]"
@
update:page=
"emit('update:page', $event)"
@
update:pageSize=
"emit('update:pageSize', $event)"
/>
</div>
</
template
>
...
...
frontend/src/views/admin/ops/components/OpsRequestDetailsModal.vue
View file @
0e448297
...
...
@@ -95,6 +95,7 @@ watch(
(
open
)
=>
{
if
(
open
)
{
page
.
value
=
1
pageSize
.
value
=
20
fetchData
()
}
}
...
...
@@ -150,45 +151,46 @@ const kindBadgeClass = (kind: string) => {
<
template
>
<BaseDialog
:show=
"modelValue"
:title=
"props.preset.title || t('admin.ops.requestDetails.title')"
width=
"full"
@
close=
"close"
>
<template
#default
>
<div
class=
"flex items-center justify-between mb-4"
>
<div
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.requestDetails.rangeLabel
'
,
{
range
:
rangeLabel
}
)
}}
<div
class=
"flex h-full min-h-0 flex-col"
>
<div
class=
"mb-4 flex flex-shrink-0 items-center justify-between"
>
<div
class=
"text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.requestDetails.rangeLabel
'
,
{
range
:
rangeLabel
}
)
}}
<
/div
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary btn-sm
"
@
click
=
"
fetchData
"
>
{{
t
(
'
common.refresh
'
)
}}
<
/button
>
<
/div
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary btn-sm
"
@
click
=
"
fetchData
"
>
{{
t
(
'
common.refresh
'
)
}}
<
/button
>
<
/div
>
<!--
Loading
-->
<
div
v
-
if
=
"
loading
"
class
=
"
flex items-center justify-center py-16
"
>
<
div
class
=
"
flex flex-col items-center gap-3
"
>
<
svg
class
=
"
h-8 w-8 animate-spin text-blue-500
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
>
<
circle
class
=
"
opacity-25
"
cx
=
"
12
"
cy
=
"
12
"
r
=
"
10
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
4
"
><
/circle
>
<
path
class
=
"
opacity-75
"
fill
=
"
currentColor
"
d
=
"
M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z
"
><
/path
>
<
/svg
>
<
span
class
=
"
text-sm font-medium text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
common.loading
'
)
}}
<
/span
>
<!--
Loading
-->
<
div
v
-
if
=
"
loading
"
class
=
"
flex flex-1 items-center justify-center py-16
"
>
<
div
class
=
"
flex flex-col items-center gap-3
"
>
<
svg
class
=
"
h-8 w-8 animate-spin text-blue-500
"
fill
=
"
none
"
viewBox
=
"
0 0 24 24
"
>
<
circle
class
=
"
opacity-25
"
cx
=
"
12
"
cy
=
"
12
"
r
=
"
10
"
stroke
=
"
currentColor
"
stroke
-
width
=
"
4
"
><
/circle
>
<
path
class
=
"
opacity-75
"
fill
=
"
currentColor
"
d
=
"
M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z
"
><
/path
>
<
/svg
>
<
span
class
=
"
text-sm font-medium text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
common.loading
'
)
}}
<
/span
>
<
/div
>
<
/div
>
<
/div
>
<!--
Table
-->
<
div
v
-
else
>
<
div
v
-
if
=
"
items.length === 0
"
class
=
"
rounded-xl border border-dashed border-gray-200 p-10 text-center dark:border-dark-700
"
>
<
div
class
=
"
text-sm font-medium text-gray-600 dark:text-gray-300
"
>
{{
t
(
'
admin.ops.requestDetails.empty
'
)
}}
<
/div
>
<
div
class
=
"
mt-1 text-xs text-gray-400
"
>
{{
t
(
'
admin.ops.requestDetails.emptyHint
'
)
}}
<
/div
>
<
/div
>
<!--
Table
-->
<
div
v
-
else
class
=
"
flex min-h-0 flex-1 flex-col
"
>
<
div
v
-
if
=
"
items.length === 0
"
class
=
"
rounded-xl border border-dashed border-gray-200 p-10 text-center dark:border-dark-700
"
>
<
div
class
=
"
text-sm font-medium text-gray-600 dark:text-gray-300
"
>
{{
t
(
'
admin.ops.requestDetails.empty
'
)
}}
<
/div
>
<
div
class
=
"
mt-1 text-xs text-gray-400
"
>
{{
t
(
'
admin.ops.requestDetails.emptyHint
'
)
}}
<
/div
>
<
/div
>
<
div
v
-
else
class
=
"
overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700
"
>
<
div
class
=
"
overflow-
x-
auto
"
>
<
table
class
=
"
min-w-full divide-y divide-gray-200 dark:divide-dark-700
"
>
<
thead
class
=
"
bg-gray-50 dark:bg-dark-900
"
>
<
div
v
-
else
class
=
"
flex min-h-0 flex-1 flex-col
overflow-hidden rounded-xl border border-gray-200 dark:border-dark-700
"
>
<
div
class
=
"
min-h-0 flex-1
overflow-auto
"
>
<
table
class
=
"
min-w-full divide-y divide-gray-200 dark:divide-dark-700
"
>
<
thead
class
=
"
sticky top-0 z-10
bg-gray-50 dark:bg-dark-900
"
>
<
tr
>
<
th
class
=
"
px-4 py-3 text-left text-[11px] font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400
"
>
{{
t
(
'
admin.ops.requestDetails.table.time
'
)
}}
...
...
@@ -265,15 +267,16 @@ const kindBadgeClass = (kind: string) => {
<
/tr
>
<
/tbody
>
<
/table
>
<
/div
>
<
/div
>
<
Pagination
:
total
=
"
total
"
:
page
=
"
page
"
:
page
-
size
=
"
pageSize
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
pageSize
=
"
handlePageSizeChange
"
/>
<
Pagination
:
total
=
"
total
"
:
page
=
"
page
"
:
page
-
size
=
"
pageSize
"
@
update
:
page
=
"
handlePageChange
"
@
update
:
pageSize
=
"
handlePageSizeChange
"
/>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
...
...
frontend/src/views/admin/ops/components/OpsRuntimeSettingsCard.vue
View file @
0e448297
...
...
@@ -45,6 +45,36 @@ function validateRuntimeSettings(settings: OpsAlertRuntimeSettings): ValidationR
errors
.
push
(
t
(
'
admin.ops.runtime.validation.evalIntervalRange
'
))
}
// Thresholds validation
const
thresholds
=
settings
.
thresholds
if
(
thresholds
)
{
if
(
thresholds
.
sla_percent_min
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
sla_percent_min
)
||
thresholds
.
sla_percent_min
<
0
||
thresholds
.
sla_percent_min
>
100
)
{
errors
.
push
(
'
SLA 最低值必须在 0-100 之间
'
)
}
}
if
(
thresholds
.
latency_p99_ms_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
latency_p99_ms_max
)
||
thresholds
.
latency_p99_ms_max
<
0
)
{
errors
.
push
(
'
延迟 P99 最大值必须大于或等于 0
'
)
}
}
if
(
thresholds
.
ttft_p99_ms_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
ttft_p99_ms_max
)
||
thresholds
.
ttft_p99_ms_max
<
0
)
{
errors
.
push
(
'
TTFT P99 最大值必须大于或等于 0
'
)
}
}
if
(
thresholds
.
request_error_rate_percent_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
request_error_rate_percent_max
)
||
thresholds
.
request_error_rate_percent_max
<
0
||
thresholds
.
request_error_rate_percent_max
>
100
)
{
errors
.
push
(
'
请求错误率最大值必须在 0-100 之间
'
)
}
}
if
(
thresholds
.
upstream_error_rate_percent_max
!=
null
)
{
if
(
!
Number
.
isFinite
(
thresholds
.
upstream_error_rate_percent_max
)
||
thresholds
.
upstream_error_rate_percent_max
<
0
||
thresholds
.
upstream_error_rate_percent_max
>
100
)
{
errors
.
push
(
'
上游错误率最大值必须在 0-100 之间
'
)
}
}
}
const
lock
=
settings
.
distributed_lock
if
(
lock
?.
enabled
)
{
if
(
!
lock
.
key
||
lock
.
key
.
trim
().
length
<
3
)
{
...
...
@@ -130,6 +160,15 @@ function openAlertEditor() {
if
(
!
Array
.
isArray
(
draftAlert
.
value
.
silencing
.
entries
))
{
draftAlert
.
value
.
silencing
.
entries
=
[]
}
if
(
!
draftAlert
.
value
.
thresholds
)
{
draftAlert
.
value
.
thresholds
=
{
sla_percent_min
:
99.5
,
latency_p99_ms_max
:
2000
,
ttft_p99_ms_max
:
500
,
request_error_rate_percent_max
:
5
,
upstream_error_rate_percent_max
:
5
}
}
}
showAlertEditor
.
value
=
true
...
...
@@ -295,6 +334,81 @@ onMounted(() => {
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.runtime.evalIntervalHint
'
)
}}
</p>
</div>
<div
class=
"rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50"
>
<div
class=
"mb-2 text-sm font-semibold text-gray-900 dark:text-white"
>
指标阈值配置
</div>
<p
class=
"mb-4 text-xs text-gray-500 dark:text-gray-400"
>
配置各项指标的告警阈值。超出阈值的指标将在看板上以红色显示。
</p>
<div
class=
"grid grid-cols-1 gap-4 md:grid-cols-2"
>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
SLA 最低值 (%)
</div>
<input
v-model.number=
"draftAlert.thresholds.sla_percent_min"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
placeholder=
"99.5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
SLA 低于此值时将显示为红色
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
延迟 P99 最大值 (ms)
</div>
<input
v-model.number=
"draftAlert.thresholds.latency_p99_ms_max"
type=
"number"
min=
"0"
step=
"100"
class=
"input"
placeholder=
"2000"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
延迟 P99 高于此值时将显示为红色
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
TTFT P99 最大值 (ms)
</div>
<input
v-model.number=
"draftAlert.thresholds.ttft_p99_ms_max"
type=
"number"
min=
"0"
step=
"100"
class=
"input"
placeholder=
"500"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
TTFT P99 高于此值时将显示为红色
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
请求错误率最大值 (%)
</div>
<input
v-model.number=
"draftAlert.thresholds.request_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
placeholder=
"5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
请求错误率高于此值时将显示为红色
</p>
</div>
<div>
<div
class=
"mb-1 text-xs font-medium text-gray-600 dark:text-gray-300"
>
上游错误率最大值 (%)
</div>
<input
v-model.number=
"draftAlert.thresholds.upstream_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
placeholder=
"5"
/>
<p
class=
"mt-1 text-xs text-gray-500 dark:text-gray-400"
>
上游错误率高于此值时将显示为红色
</p>
</div>
</div>
</div>
<div
class=
"rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50"
>
<div
class=
"mb-2 text-sm font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.ops.runtime.silencing.title
'
)
}}
</div>
...
...
frontend/src/views/admin/ops/components/OpsSettingsDialog.vue
View file @
0e448297
...
...
@@ -6,7 +6,7 @@ import { opsAPI } from '@/api/admin/ops'
import
BaseDialog
from
'
@/components/common/BaseDialog.vue
'
import
Select
from
'
@/components/common/Select.vue
'
import
Toggle
from
'
@/components/common/Toggle.vue
'
import
type
{
OpsAlertRuntimeSettings
,
EmailNotificationConfig
,
AlertSeverity
,
OpsAdvancedSettings
}
from
'
../types
'
import
type
{
OpsAlertRuntimeSettings
,
EmailNotificationConfig
,
AlertSeverity
,
OpsAdvancedSettings
,
OpsMetricThresholds
}
from
'
../types
'
const
{
t
}
=
useI18n
()
const
appStore
=
useAppStore
()
...
...
@@ -29,19 +29,38 @@ const runtimeSettings = ref<OpsAlertRuntimeSettings | null>(null)
const
emailConfig
=
ref
<
EmailNotificationConfig
|
null
>
(
null
)
// 高级设置
const
advancedSettings
=
ref
<
OpsAdvancedSettings
|
null
>
(
null
)
// 指标阈值配置
const
metricThresholds
=
ref
<
OpsMetricThresholds
>
({
sla_percent_min
:
99.5
,
latency_p99_ms_max
:
2000
,
ttft_p99_ms_max
:
500
,
request_error_rate_percent_max
:
5
,
upstream_error_rate_percent_max
:
5
})
// 加载所有配置
async
function
loadAllSettings
()
{
loading
.
value
=
true
try
{
const
[
runtime
,
email
,
advanced
]
=
await
Promise
.
all
([
const
[
runtime
,
email
,
advanced
,
thresholds
]
=
await
Promise
.
all
([
opsAPI
.
getAlertRuntimeSettings
(),
opsAPI
.
getEmailNotificationConfig
(),
opsAPI
.
getAdvancedSettings
()
opsAPI
.
getAdvancedSettings
(),
opsAPI
.
getMetricThresholds
()
])
runtimeSettings
.
value
=
runtime
emailConfig
.
value
=
email
advancedSettings
.
value
=
advanced
// 如果后端返回了阈值,使用后端的值;否则保持默认值
if
(
thresholds
&&
Object
.
keys
(
thresholds
).
length
>
0
)
{
metricThresholds
.
value
=
{
sla_percent_min
:
thresholds
.
sla_percent_min
??
99.5
,
latency_p99_ms_max
:
thresholds
.
latency_p99_ms_max
??
2000
,
ttft_p99_ms_max
:
thresholds
.
ttft_p99_ms_max
??
500
,
request_error_rate_percent_max
:
thresholds
.
request_error_rate_percent_max
??
5
,
upstream_error_rate_percent_max
:
thresholds
.
upstream_error_rate_percent_max
??
5
}
}
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSettingsDialog] Failed to load settings
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
t
(
'
admin.ops.settings.loadFailed
'
))
...
...
@@ -138,6 +157,23 @@ const validation = computed(() => {
}
}
// 验证指标阈值
if
(
metricThresholds
.
value
.
sla_percent_min
!=
null
&&
(
metricThresholds
.
value
.
sla_percent_min
<
0
||
metricThresholds
.
value
.
sla_percent_min
>
100
))
{
errors
.
push
(
'
SLA最低百分比必须在0-100之间
'
)
}
if
(
metricThresholds
.
value
.
latency_p99_ms_max
!=
null
&&
metricThresholds
.
value
.
latency_p99_ms_max
<
0
)
{
errors
.
push
(
'
延迟P99最大值必须大于等于0
'
)
}
if
(
metricThresholds
.
value
.
ttft_p99_ms_max
!=
null
&&
metricThresholds
.
value
.
ttft_p99_ms_max
<
0
)
{
errors
.
push
(
'
TTFT P99最大值必须大于等于0
'
)
}
if
(
metricThresholds
.
value
.
request_error_rate_percent_max
!=
null
&&
(
metricThresholds
.
value
.
request_error_rate_percent_max
<
0
||
metricThresholds
.
value
.
request_error_rate_percent_max
>
100
))
{
errors
.
push
(
'
请求错误率最大值必须在0-100之间
'
)
}
if
(
metricThresholds
.
value
.
upstream_error_rate_percent_max
!=
null
&&
(
metricThresholds
.
value
.
upstream_error_rate_percent_max
<
0
||
metricThresholds
.
value
.
upstream_error_rate_percent_max
>
100
))
{
errors
.
push
(
'
上游错误率最大值必须在0-100之间
'
)
}
return
{
valid
:
errors
.
length
===
0
,
errors
}
})
...
...
@@ -153,14 +189,15 @@ async function saveAllSettings() {
await
Promise
.
all
([
runtimeSettings
.
value
?
opsAPI
.
updateAlertRuntimeSettings
(
runtimeSettings
.
value
)
:
Promise
.
resolve
(),
emailConfig
.
value
?
opsAPI
.
updateEmailNotificationConfig
(
emailConfig
.
value
)
:
Promise
.
resolve
(),
advancedSettings
.
value
?
opsAPI
.
updateAdvancedSettings
(
advancedSettings
.
value
)
:
Promise
.
resolve
()
advancedSettings
.
value
?
opsAPI
.
updateAdvancedSettings
(
advancedSettings
.
value
)
:
Promise
.
resolve
(),
opsAPI
.
updateMetricThresholds
(
metricThresholds
.
value
)
])
appStore
.
showSuccess
(
t
(
'
admin.ops.settings.saveSuccess
'
))
emit
(
'
saved
'
)
emit
(
'
close
'
)
}
catch
(
err
:
any
)
{
console
.
error
(
'
[OpsSettingsDialog] Failed to save settings
'
,
err
)
appStore
.
showError
(
err
?.
response
?.
data
?.
detail
||
t
(
'
admin.ops.settings.saveFailed
'
))
appStore
.
showError
(
err
?.
response
?.
data
?.
message
||
err
?.
response
?.
data
?.
detail
||
t
(
'
admin.ops.settings.saveFailed
'
))
}
finally
{
saving
.
value
=
false
}
...
...
@@ -306,6 +343,77 @@ async function saveAllSettings() {
</div>
</div>
<!-- 指标阈值配置 -->
<div
class=
"rounded-2xl bg-gray-50 p-4 dark:bg-dark-700/50"
>
<h4
class=
"mb-3 text-sm font-semibold text-gray-900 dark:text-white"
>
{{
t
(
'
admin.ops.settings.metricThresholds
'
)
}}
</h4>
<p
class=
"mb-4 text-xs text-gray-500 dark:text-gray-400"
>
{{
t
(
'
admin.ops.settings.metricThresholdsHint
'
)
}}
</p>
<div
class=
"space-y-4"
>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.slaMinPercent
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.sla_percent_min"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.slaMinPercentHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.latencyP99MaxMs
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.latency_p99_ms_max"
type=
"number"
min=
"0"
step=
"100"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.latencyP99MaxMsHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.ttftP99MaxMs
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.ttft_p99_ms_max"
type=
"number"
min=
"0"
step=
"50"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.ttftP99MaxMsHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.requestErrorRateMaxPercent
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.request_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.requestErrorRateMaxPercentHint
'
)
}}
</p>
</div>
<div>
<label
class=
"input-label"
>
{{
t
(
'
admin.ops.settings.upstreamErrorRateMaxPercent
'
)
}}
</label>
<input
v-model.number=
"metricThresholds.upstream_error_rate_percent_max"
type=
"number"
min=
"0"
max=
"100"
step=
"0.1"
class=
"input"
/>
<p
class=
"mt-1 text-xs text-gray-500"
>
{{
t
(
'
admin.ops.settings.upstreamErrorRateMaxPercentHint
'
)
}}
</p>
</div>
</div>
</div>
<!-- 高级设置 -->
<details
class=
"rounded-2xl bg-gray-50 dark:bg-dark-700/50"
>
<summary
class=
"cursor-pointer p-4 text-sm font-semibold text-gray-900 dark:text-white"
>
...
...
@@ -379,6 +487,48 @@ async function saveAllSettings() {
<Toggle
v-model=
"advancedSettings.aggregation.aggregation_enabled"
/>
</div>
</div>
<!-- 错误过滤 -->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
错误过滤
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
忽略 count_tokens 错误
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
启用后,count_tokens 请求的错误将不计入运维监控的统计和告警中(但仍会存储在数据库中)
</p>
</div>
<Toggle
v-model=
"advancedSettings.ignore_count_tokens_errors"
/>
</div>
</div>
<!-- 自动刷新 -->
<div
class=
"space-y-3"
>
<h5
class=
"text-xs font-semibold text-gray-700 dark:text-gray-300"
>
自动刷新
</h5>
<div
class=
"flex items-center justify-between"
>
<div>
<label
class=
"text-sm font-medium text-gray-700 dark:text-gray-300"
>
启用自动刷新
</label>
<p
class=
"mt-1 text-xs text-gray-500"
>
自动刷新仪表板数据,启用后会定期拉取最新数据
</p>
</div>
<Toggle
v-model=
"advancedSettings.auto_refresh_enabled"
/>
</div>
<div
v-if=
"advancedSettings.auto_refresh_enabled"
>
<label
class=
"input-label"
>
刷新间隔
</label>
<Select
v-model=
"advancedSettings.auto_refresh_interval_seconds"
:options=
"[
{ value: 15, label: '15 秒' },
{ value: 30, label: '30 秒' },
{ value: 60, label: '60 秒' }
]"
/>
</div>
</div>
</div>
</details>
</div>
...
...
frontend/src/views/admin/ops/components/OpsThroughputTrendChart.vue
View file @
0e448297
...
...
@@ -19,6 +19,7 @@ interface Props {
timeRange
:
string
byPlatform
?:
OpsThroughputPlatformBreakdownItem
[]
topGroups
?:
OpsThroughputGroupBreakdownItem
[]
fullscreen
?:
boolean
}
const
props
=
defineProps
<
Props
>
()
...
...
@@ -179,38 +180,40 @@ function downloadChart() {
<path
stroke-linecap=
"round"
stroke-linejoin=
"round"
stroke-width=
"2"
d=
"M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
{{
t
(
'
admin.ops.throughputTrend
'
)
}}
<HelpTooltip
:content=
"t('admin.ops.tooltips.throughputTrend')"
/>
<HelpTooltip
v-if=
"!props.fullscreen"
:content=
"t('admin.ops.tooltips.throughputTrend')"
/>
</h3>
<div
class=
"flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400"
>
<span
class=
"flex items-center gap-1"
><span
class=
"h-2 w-2 rounded-full bg-blue-500"
></span>
{{
t
(
'
admin.ops.qps
'
)
}}
</span>
<span
class=
"flex items-center gap-1"
><span
class=
"h-2 w-2 rounded-full bg-green-500"
></span>
{{
t
(
'
admin.ops.tpsK
'
)
}}
</span>
<button
type=
"button"
class=
"ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.requestDetails.title')"
@
click=
"emit('openDetails')"
>
{{
t
(
'
admin.ops.requestDetails.details
'
)
}}
</button>
<button
type=
"button"
class=
"ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.resetZoomHint')"
@
click=
"resetZoom"
>
{{
t
(
'
admin.ops.charts.resetZoom
'
)
}}
</button>
<button
type=
"button"
class=
"inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.downloadChartHint')"
@
click=
"downloadChart"
>
{{
t
(
'
admin.ops.charts.downloadChart
'
)
}}
</button>
<template
v-if=
"!props.fullscreen"
>
<button
type=
"button"
class=
"ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.requestDetails.title')"
@
click=
"emit('openDetails')"
>
{{
t
(
'
admin.ops.requestDetails.details
'
)
}}
</button>
<button
type=
"button"
class=
"ml-2 inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.resetZoomHint')"
@
click=
"resetZoom"
>
{{
t
(
'
admin.ops.charts.resetZoom
'
)
}}
</button>
<button
type=
"button"
class=
"inline-flex items-center rounded-lg border border-gray-200 bg-white px-2 py-1 text-[11px] font-semibold text-gray-600 hover:bg-gray-50 disabled:opacity-50 dark:border-dark-700 dark:bg-dark-900 dark:text-gray-300 dark:hover:bg-dark-800"
:disabled=
"state !== 'ready'"
:title=
"t('admin.ops.charts.downloadChartHint')"
@
click=
"downloadChart"
>
{{
t
(
'
admin.ops.charts.downloadChart
'
)
}}
</button>
</
template
>
</div>
</div>
...
...
frontend/src/views/admin/ops/types.ts
View file @
0e448297
...
...
@@ -14,6 +14,7 @@ export type {
EmailNotificationConfig
,
OpsDistributedLockSettings
,
OpsAlertRuntimeSettings
,
OpsMetricThresholds
,
OpsAdvancedSettings
,
OpsDataRetentionSettings
,
OpsAggregationSettings
...
...
Prev
1
2
3
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