Commit f38a3e75 authored by IanShaw027's avatar IanShaw027
Browse files

feat(ui): 优化ops监控面板和组件功能

- 增强告警事件卡片的交互和静默功能
- 完善错误详情弹窗的展示和操作
- 优化错误日志表格的筛选和排序
- 新增重试和解决状态的UI支持
parent b8da5d45
......@@ -169,7 +169,13 @@ const QUERY_KEYS = {
platform: 'platform',
groupId: 'group_id',
queryMode: 'mode',
fullscreen: 'fullscreen'
fullscreen: 'fullscreen',
// Deep links
openErrorDetails: 'open_error_details',
errorType: 'error_type',
alertRuleId: 'alert_rule_id',
openAlertRules: 'open_alert_rules'
} as const
const isApplyingRouteQuery = ref(false)
......@@ -249,6 +255,24 @@ const applyRouteQueryToState = () => {
const fallback = adminSettingsStore.opsQueryModeDefault || 'auto'
queryMode.value = allowedQueryModes.has(fallback as QueryMode) ? (fallback as QueryMode) : 'auto'
}
// Deep links
const openRules = readQueryString(QUERY_KEYS.openAlertRules)
if (openRules === '1' || openRules === 'true') {
showAlertRulesCard.value = true
}
const ruleID = readQueryNumber(QUERY_KEYS.alertRuleId)
if (typeof ruleID === 'number' && ruleID > 0) {
showAlertRulesCard.value = true
}
const openErr = readQueryString(QUERY_KEYS.openErrorDetails)
if (openErr === '1' || openErr === 'true') {
const typ = readQueryString(QUERY_KEYS.errorType)
errorDetailsType.value = typ === 'upstream' ? 'upstream' : 'request'
showErrorDetails.value = true
}
}
applyRouteQueryToState()
......
......@@ -31,6 +31,8 @@ const pageSize = ref(20)
const q = ref('')
const statusCode = ref<number | null>(null)
const phase = ref<string>('')
const errorOwner = ref<string>('')
const resolvedStatus = ref<string>('unresolved')
const accountIdInput = ref<string>('')
const accountId = computed<number | null>(() => {
......@@ -52,15 +54,31 @@ const statusCodeSelectOptions = computed(() => {
]
})
const ownerSelectOptions = computed(() => {
return [
{ value: '', label: t('common.all') },
{ value: 'provider', label: 'provider' },
{ value: 'client', label: 'client' },
{ value: 'platform', label: 'platform' }
]
})
const resolvedSelectOptions = computed(() => {
return [
{ value: 'unresolved', label: t('admin.ops.errorDetails.unresolved') || 'unresolved' },
{ value: 'all', label: t('common.all') },
{ value: 'resolved', label: t('admin.ops.errorDetails.resolved') || 'resolved' }
]
})
const phaseSelectOptions = computed(() => {
const options = [
{ value: '', label: t('common.all') },
{ value: 'request', label: 'request' },
{ value: 'auth', label: 'auth' },
{ value: 'routing', label: 'routing' },
{ value: 'upstream', label: 'upstream' },
{ value: 'network', label: 'network' },
{ value: 'routing', label: 'routing' },
{ value: 'auth', label: 'auth' },
{ value: 'billing', label: 'billing' },
{ value: 'concurrency', label: 'concurrency' },
{ value: 'internal', label: 'internal' }
]
return options
......@@ -92,6 +110,14 @@ async function fetchErrorLogs() {
const phaseVal = String(phase.value || '').trim()
if (phaseVal) params.phase = phaseVal
const ownerVal = String(errorOwner.value || '').trim()
if (ownerVal) params.error_owner = ownerVal
const resolvedVal = String(resolvedStatus.value || '').trim()
if (resolvedVal === 'resolved') params.resolved = 'true'
else if (resolvedVal === 'unresolved') params.resolved = 'false'
// 'all' -> omit
const res = await opsAPI.listErrorLogs(params)
rows.value = res.items || []
total.value = res.total || 0
......@@ -108,6 +134,8 @@ function resetFilters() {
q.value = ''
statusCode.value = null
phase.value = props.errorType === 'upstream' ? 'upstream' : ''
errorOwner.value = ''
resolvedStatus.value = 'unresolved'
accountIdInput.value = ''
page.value = 1
fetchErrorLogs()
......@@ -154,7 +182,7 @@ watch(
)
watch(
() => [statusCode.value, phase.value] as const,
() => [statusCode.value, phase.value, errorOwner.value, resolvedStatus.value] as const,
() => {
if (!props.show) return
page.value = 1
......@@ -177,8 +205,8 @@ 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-1 gap-4 lg:grid-cols-12">
<div class="lg:col-span-5">
<div class="grid grid-cols-1 gap-4 lg:grid-cols-14">
<div class="lg:col-span-4">
<div class="relative group">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
<svg
......@@ -208,6 +236,14 @@ watch(
</div>
<div class="lg:col-span-2">
<Select :model-value="errorOwner" :options="ownerSelectOptions" class="w-full" @update:model-value="errorOwner = String($event ?? '')" />
</div>
<div class="lg:col-span-2">
<Select :model-value="resolvedStatus" :options="resolvedSelectOptions" class="w-full" @update:model-value="resolvedStatus = String($event ?? 'unresolved')" />
</div>
<div class="lg:col-span-1">
<input
v-model="accountIdInput"
type="text"
......
......@@ -15,6 +15,12 @@
>
{{ 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.type') }}
</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"
......@@ -49,7 +55,7 @@
</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">
<td colspan="7" class="py-16 text-center text-sm text-gray-400 dark:text-dark-500">
{{ t('admin.ops.errorLog.noErrors') }}
</td>
</tr>
......@@ -79,6 +85,30 @@
</div>
</td>
<!-- Type -->
<td class="px-6 py-4">
<div class="flex flex-col gap-1">
<span
:class="[
'inline-flex items-center rounded-lg px-2 py-1 text-xs font-black ring-1 ring-inset shadow-sm',
getTypeBadge(log).className
]"
>
{{ getTypeBadge(log).label }}
</span>
<div class="flex flex-wrap gap-x-3 gap-y-1">
<div v-if="(log as any).error_owner" 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 as any).error_owner }}</span>
</div>
<div v-if="(log as any).error_source" 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 as any).error_source }}</span>
</div>
</div>
</div>
</td>
<!-- Context (Platform/Model) -->
<td class="px-6 py-4">
<div class="flex flex-col items-start gap-1.5">
......@@ -182,6 +212,37 @@ import Pagination from '@/components/common/Pagination.vue'
import type { OpsErrorLog } from '@/api/admin/ops'
import { getSeverityClass, formatDateTime } from '../utils/opsFormatters'
function getTypeBadge(log: OpsErrorLog): { label: string; className: string } {
const phase = String(log.phase || '').toLowerCase()
const owner = String((log as any).error_owner || '').toLowerCase()
// Mapping aligned with the design:
// - upstream/provider => 🔴 上游
// - request/client => 🟡 请求
// - auth/client => 🔵 认证
// - routing/platform => 🟣 路由
// - internal/platform => ⚫ 内部
if (phase === 'upstream' && owner === 'provider') {
return { label: '🔴 上游', className: 'bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-500/30' }
}
if (phase === 'request' && owner === 'client') {
return { label: '🟡 请求', className: 'bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-400 dark:ring-amber-500/30' }
}
if (phase === 'auth' && owner === 'client') {
return { label: '🔵 认证', className: 'bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-900/30 dark:text-blue-400 dark:ring-blue-500/30' }
}
if (phase === 'routing' && owner === 'platform') {
return { label: '🟣 路由', className: 'bg-purple-50 text-purple-700 ring-purple-600/20 dark:bg-purple-900/30 dark:text-purple-400 dark:ring-purple-500/30' }
}
if (phase === 'internal' && owner === 'platform') {
return { label: '⚫ 内部', className: 'bg-gray-100 text-gray-800 ring-gray-600/20 dark:bg-dark-700 dark:text-gray-200 dark:ring-dark-500/40' }
}
// Fallback: show phase/owner for unknown combos.
const fallback = phase || owner || 'unknown'
return { label: fallback, className: 'bg-gray-50 text-gray-700 ring-gray-600/10 dark:bg-dark-900 dark:text-gray-300 dark:ring-dark-700' }
}
const { t } = useI18n()
interface Props {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment