Compare commits
4 Commits
947fa2f9b9
...
8c1772c784
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c1772c784 | |||
| c9da54829d | |||
| db767de8bb | |||
| 9cbacf5998 |
@@ -68,10 +68,11 @@ export const createRule = (data: {
|
||||
severity_id: number
|
||||
enabled?: boolean
|
||||
metric_name?: string
|
||||
query_expr?: string
|
||||
data_source?: string
|
||||
threshold?: number
|
||||
compare_op?: string
|
||||
duration?: number
|
||||
eval_interval?: number
|
||||
baseline_config?: string
|
||||
labels?: string
|
||||
annotations?: string
|
||||
@@ -86,10 +87,11 @@ export const updateRule = (data: {
|
||||
severity_id?: number
|
||||
enabled?: boolean
|
||||
metric_name?: string
|
||||
query_expr?: string
|
||||
data_source?: string
|
||||
threshold?: number
|
||||
compare_op?: string
|
||||
duration?: number
|
||||
eval_interval?: number
|
||||
baseline_config?: string
|
||||
labels?: string
|
||||
annotations?: string
|
||||
|
||||
@@ -28,8 +28,6 @@ export const fetchAlertRecordDetail = (id: number) => request.get(`/Alert/v1/rec
|
||||
export const createAlertProcess = (data: {
|
||||
alert_record_id: number
|
||||
action: string // ack / resolve / silence / comment / assign / escalate / close
|
||||
operator: string
|
||||
operator_id?: string
|
||||
comment?: string
|
||||
assign_to?: string
|
||||
assign_to_id?: string
|
||||
|
||||
@@ -20,12 +20,13 @@ export interface AlertRule {
|
||||
name: string
|
||||
data_source: string
|
||||
metric_name: string
|
||||
rule_type: 'static' | 'dynamic' | 'promql'
|
||||
rule_type: 'static' | 'dynamic'
|
||||
compare_op: '>' | '>=' | '<' | '<=' | '==' | '!='
|
||||
threshold: string | number
|
||||
duration: number
|
||||
eval_interval: number
|
||||
severity_code: string
|
||||
baseline_config?: string
|
||||
labels?: Record<string, string>
|
||||
annotations?: Record<string, string>
|
||||
}
|
||||
@@ -105,12 +106,11 @@ export const TEMPLATE_CATEGORIES = [
|
||||
// 数据源选项
|
||||
export const DATA_SOURCES = [
|
||||
{ value: 'dc-host', label: '主机/操作系统指标' },
|
||||
{ value: 'dc-hardware', label: '服务器硬件指标' },
|
||||
{ value: 'dc-network', label: '网络设备指标' },
|
||||
{ value: 'dc-database', label: '数据库指标' },
|
||||
{ value: 'dc-middleware', label: '中间件指标' },
|
||||
{ value: 'dc-virtualization', label: '虚拟化指标' },
|
||||
{ value: 'dc-env', label: '动力/环境/安防指标' },
|
||||
{ value: 'dc-security', label: '安全设备指标' },
|
||||
{ value: 'dc-storage', label: '存储设备指标' },
|
||||
{ value: 'dc-room-device', label: '机房设备指标' },
|
||||
]
|
||||
|
||||
// 比较运算符选项
|
||||
@@ -127,7 +127,6 @@ export const COMPARE_OPERATORS = [
|
||||
export const RULE_TYPES = [
|
||||
{ value: 'static', label: '静态阈值' },
|
||||
{ value: 'dynamic', label: '动态阈值' },
|
||||
{ value: 'promql', label: 'PromQL 表达式' },
|
||||
]
|
||||
|
||||
// ==================== 告警模板接口 ====================
|
||||
|
||||
@@ -48,5 +48,5 @@ export const fetchIncidentAlerts = (id: number) => request.get(`/Alert/v1/incide
|
||||
|
||||
export const fetchIncidentTimeline = (id: number) => request.get(`/Alert/v1/incidents/${id}/timeline`)
|
||||
|
||||
export const transitionIncident = (id: number, data: { status: string; comment?: string; operator?: string }) =>
|
||||
export const transitionIncident = (id: number, data: { status: string; comment?: string }) =>
|
||||
request.post(`/Alert/v1/incidents/${id}/transition`, data)
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
'menu.server.monitor': 'Monitor-Server',
|
||||
'menu.list': 'List',
|
||||
'menu.ops': 'Operations',
|
||||
'menu.ops.alert.suppression': 'Alert Suppression',
|
||||
'menu.ops.netarch': 'Network Architecture',
|
||||
'menu.ops.netarch.topoGroup': 'Topology Group Management',
|
||||
'menu.ops.netarch.topo': 'Topology',
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
'menu.list': '列表页',
|
||||
'menu.ops': '运维管理',
|
||||
'menu.ops.alert.suppression': '告警抑制',
|
||||
'menu.ops.netarch': '网络架构',
|
||||
'menu.ops.netarch.topoGroup': '拓扑组管理',
|
||||
'menu.ops.netarch.topo': '拓扑图',
|
||||
|
||||
@@ -45,6 +45,17 @@ const OPS: AppRouteRecordRaw = {
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'alert/suppression',
|
||||
alias: ['/alert/suppression'],
|
||||
name: 'OpsAlertSuppression',
|
||||
component: () => import('@/views/ops/pages/alert/suppress/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.ops.alert.suppression',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'business-system',
|
||||
alias: ['/business-system'],
|
||||
|
||||
@@ -247,14 +247,15 @@ const loadUserOptions = async (keyword = '') => {
|
||||
keyword: keyword || undefined,
|
||||
status: 1,
|
||||
})
|
||||
const rawList = Array.isArray(res?.data?.data)
|
||||
? res.data.data
|
||||
: Array.isArray(res?.details?.data)
|
||||
? res.details.data
|
||||
: Array.isArray(res?.details)
|
||||
? res.details
|
||||
: Array.isArray(res?.data)
|
||||
? res.data
|
||||
const { data, details } = res
|
||||
const rawList = !Array.isArray(data) && Array.isArray(data?.data)
|
||||
? data.data
|
||||
: !Array.isArray(details) && Array.isArray(details?.data)
|
||||
? details.data
|
||||
: Array.isArray(details)
|
||||
? details
|
||||
: Array.isArray(data)
|
||||
? data
|
||||
: []
|
||||
userOptions.value = rawList.map((item: any) => ({
|
||||
id: Number(item.id),
|
||||
|
||||
@@ -273,14 +273,15 @@ const loadUserOptions = async (keyword = '') => {
|
||||
keyword: keyword || undefined,
|
||||
status: 1,
|
||||
})
|
||||
const rawList = Array.isArray(res?.data?.data)
|
||||
? res.data.data
|
||||
: Array.isArray(res?.details?.data)
|
||||
? res.details.data
|
||||
: Array.isArray(res?.details)
|
||||
? res.details
|
||||
: Array.isArray(res?.data)
|
||||
? res.data
|
||||
const { data, details } = res
|
||||
const rawList = !Array.isArray(data) && Array.isArray(data?.data)
|
||||
? data.data
|
||||
: !Array.isArray(details) && Array.isArray(details?.data)
|
||||
? details.data
|
||||
: Array.isArray(details)
|
||||
? details
|
||||
: Array.isArray(data)
|
||||
? data
|
||||
: []
|
||||
userOptions.value = rawList.map((item: any) => ({
|
||||
id: Number(item.id),
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="rule_type" label="规则类型" required>
|
||||
<a-select v-model="formData.rule_type" placeholder="请选择规则类型">
|
||||
<a-select v-model="formData.rule_type" placeholder="请选择规则类型" @change="handleRuleTypeChange">
|
||||
<a-option value="static">静态规则</a-option>
|
||||
<a-option value="dynamic">动态基线</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
@@ -50,13 +51,12 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 指标/表达式 -->
|
||||
<a-form-item v-if="formData.rule_type === 'static'" field="metric_name" label="指标名称" required>
|
||||
<a-form-item field="metric_name" label="指标名称" required>
|
||||
<a-input v-model="formData.metric_name" placeholder="请输入指标名称,如:cpu_usage" allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
<!-- 阈值条件 -->
|
||||
<a-row v-if="formData.rule_type !== 'promql'" :gutter="16">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-form-item field="compare_op" label="比较操作符" required>
|
||||
<a-select v-model="formData.compare_op" placeholder="请选择">
|
||||
@@ -70,8 +70,15 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item field="threshold" label="阈值" required>
|
||||
<a-input-number v-model="formData.threshold" placeholder="请输入阈值" :precision="2" allow-clear :style="{ width: '100%' }" />
|
||||
<a-form-item field="threshold" :label="formData.rule_type === 'dynamic' ? '偏离比例(%)' : '阈值'" required>
|
||||
<a-input-number
|
||||
v-model="formData.threshold"
|
||||
:placeholder="formData.rule_type === 'dynamic' ? '请输入偏离比例' : '请输入阈值'"
|
||||
:min="formData.rule_type === 'dynamic' ? 0 : undefined"
|
||||
:precision="2"
|
||||
allow-clear
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
@@ -89,6 +96,45 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="eval_interval" label="评估间隔(秒)" required>
|
||||
<a-input-number v-model="formData.eval_interval" :min="1" :precision="0" :style="{ width: '100%' }" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row v-if="formData.rule_type === 'dynamic'" :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-form-item field="lookback_seconds" label="基线窗口(秒)" required>
|
||||
<a-input-number v-model="formData.lookback_seconds" :min="formData.eval_interval" :precision="0" :style="{ width: '100%' }" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item field="baseline_aggregation" label="基线聚合" required>
|
||||
<a-select v-model="formData.baseline_aggregation">
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item field="current_aggregation" label="当前值聚合" required>
|
||||
<a-select v-model="formData.current_aggregation">
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item field="min_samples" label="最少样本数" required>
|
||||
<a-input-number v-model="formData.min_samples" :min="1" :precision="0" :style="{ width: '100%' }" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="description" label="规则描述">
|
||||
<a-textarea v-model="formData.description" placeholder="请输入规则描述" :auto-size="{ minRows: 2, maxRows: 4 }" allow-clear />
|
||||
</a-form-item>
|
||||
@@ -134,10 +180,15 @@ const formData = reactive({
|
||||
enabled: true,
|
||||
severity_id: 0 as number,
|
||||
metric_name: '',
|
||||
query_expr: '',
|
||||
data_source: 'dc-control',
|
||||
threshold: undefined as number | undefined,
|
||||
compare_op: '>',
|
||||
duration: 60,
|
||||
eval_interval: 60,
|
||||
lookback_seconds: 3600,
|
||||
baseline_aggregation: 'avg',
|
||||
current_aggregation: 'avg',
|
||||
min_samples: 1,
|
||||
description: '',
|
||||
})
|
||||
|
||||
@@ -149,7 +200,7 @@ const formRules = {
|
||||
metric_name: [
|
||||
{
|
||||
validator: (value: string, callback: any) => {
|
||||
if (formData.rule_type !== 'promql' && !value) {
|
||||
if (!value) {
|
||||
callback('请输入指标名称')
|
||||
return
|
||||
}
|
||||
@@ -157,21 +208,10 @@ const formRules = {
|
||||
},
|
||||
},
|
||||
],
|
||||
query_expr: [
|
||||
{
|
||||
validator: (value: string, callback: any) => {
|
||||
if (formData.rule_type === 'promql' && !value) {
|
||||
callback('请输入 PromQL 表达式')
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
],
|
||||
compare_op: [
|
||||
{
|
||||
validator: (value: string, callback: any) => {
|
||||
if (formData.rule_type !== 'promql' && !value) {
|
||||
if (!value) {
|
||||
callback('请选择比较操作符')
|
||||
return
|
||||
}
|
||||
@@ -182,7 +222,7 @@ const formRules = {
|
||||
threshold: [
|
||||
{
|
||||
validator: (value: number, callback: any) => {
|
||||
if (formData.rule_type !== 'promql' && value === undefined) {
|
||||
if (value === undefined) {
|
||||
callback('请输入阈值')
|
||||
return
|
||||
}
|
||||
@@ -246,16 +286,8 @@ const handleSeveritySearch = (keyword: string) => {
|
||||
|
||||
// 规则类型变化处理
|
||||
const handleRuleTypeChange = (value: string) => {
|
||||
// 根据规则类型清空相关字段
|
||||
if (value === 'promql') {
|
||||
formData.metric_name = ''
|
||||
formData.compare_op = ''
|
||||
formData.threshold = undefined
|
||||
} else {
|
||||
formData.query_expr = ''
|
||||
if (!formData.compare_op) {
|
||||
formData.compare_op = '>'
|
||||
}
|
||||
if (!formData.compare_op || (value === 'dynamic' && ['==', '!='].includes(formData.compare_op))) {
|
||||
formData.compare_op = '>'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,15 +301,29 @@ const loadRuleDetail = async () => {
|
||||
const rule = res.details
|
||||
|
||||
formData.name = rule.name
|
||||
// 当前产品只保留静态规则:强制按 static 渲染
|
||||
formData.rule_type = 'static'
|
||||
formData.rule_type = rule.rule_type === 'dynamic' ? 'dynamic' : 'static'
|
||||
formData.enabled = rule.enabled
|
||||
formData.severity_id = rule.severity_id
|
||||
formData.metric_name = rule.metric_name || ''
|
||||
formData.query_expr = rule.query_expr || ''
|
||||
formData.data_source = rule.data_source || 'dc-control'
|
||||
formData.threshold = rule.threshold
|
||||
formData.compare_op = rule.compare_op || '>'
|
||||
formData.duration = rule.duration || 60
|
||||
formData.duration = rule.duration ?? 60
|
||||
formData.eval_interval = rule.eval_interval ?? 60
|
||||
if (rule.rule_type === 'dynamic') {
|
||||
try {
|
||||
const baseline = JSON.parse(rule.baseline_config || '{}')
|
||||
formData.lookback_seconds = baseline.lookback_seconds || 3600
|
||||
formData.baseline_aggregation = baseline.aggregation || 'avg'
|
||||
formData.current_aggregation = baseline.current_aggregation || 'avg'
|
||||
formData.min_samples = baseline.min_samples || 1
|
||||
} catch {
|
||||
formData.lookback_seconds = 3600
|
||||
formData.baseline_aggregation = 'avg'
|
||||
formData.current_aggregation = 'avg'
|
||||
formData.min_samples = 1
|
||||
}
|
||||
}
|
||||
formData.description = rule.description || ''
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -293,10 +339,15 @@ const resetForm = () => {
|
||||
formData.enabled = true
|
||||
formData.severity_id = 0
|
||||
formData.metric_name = ''
|
||||
formData.query_expr = ''
|
||||
formData.data_source = 'dc-control'
|
||||
formData.threshold = undefined
|
||||
formData.compare_op = '>'
|
||||
formData.duration = 60
|
||||
formData.eval_interval = 60
|
||||
formData.lookback_seconds = 3600
|
||||
formData.baseline_aggregation = 'avg'
|
||||
formData.current_aggregation = 'avg'
|
||||
formData.min_samples = 1
|
||||
formData.description = ''
|
||||
}
|
||||
|
||||
@@ -327,10 +378,20 @@ const handleSubmit = async () => {
|
||||
submitLoading.value = true
|
||||
|
||||
try {
|
||||
const { lookback_seconds, baseline_aggregation, current_aggregation, min_samples, ...ruleFields } = formData
|
||||
const data = {
|
||||
...formData,
|
||||
...ruleFields,
|
||||
policy_id: props.policyId,
|
||||
severity_id: formData.severity_id, // 确保 severity_id 是 number 类型
|
||||
severity_id: formData.severity_id,
|
||||
baseline_config:
|
||||
formData.rule_type === 'dynamic'
|
||||
? JSON.stringify({
|
||||
lookback_seconds,
|
||||
aggregation: baseline_aggregation,
|
||||
current_aggregation,
|
||||
min_samples,
|
||||
})
|
||||
: '{}',
|
||||
}
|
||||
|
||||
// 按契约:rule/create 不做 jsonb 默认值归一化。
|
||||
@@ -338,7 +399,6 @@ const handleSubmit = async () => {
|
||||
const createPayload = !isEdit.value
|
||||
? {
|
||||
...data,
|
||||
baseline_config: '{}',
|
||||
labels: '{}',
|
||||
annotations: '{}',
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
</a-table-column>
|
||||
<a-table-column title="指标/表达式" data-index="metric_expr" :width="200">
|
||||
<template #cell="{ record }">
|
||||
<span :title="record.metric_name || record.query_expr">
|
||||
{{ record.metric_name || record.query_expr || '-' }}
|
||||
<span :title="record.metric_name">
|
||||
{{ record.metric_name || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
@@ -134,7 +134,6 @@ const currentRuleId = ref<number | undefined>(undefined)
|
||||
const ruleTypeMap: Record<string, string> = {
|
||||
static: '静态规则',
|
||||
dynamic: '动态规则',
|
||||
promql: 'PromQL',
|
||||
}
|
||||
|
||||
// 比较操作符映射
|
||||
|
||||
@@ -59,7 +59,6 @@ const handleOk = async () => {
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'ack',
|
||||
operator: getCurrentUser(),
|
||||
comment: form.value.comment,
|
||||
metadata: '{}',
|
||||
})
|
||||
@@ -77,9 +76,4 @@ const handleOk = async () => {
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,7 +64,6 @@ const handleOk = async () => {
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'comment',
|
||||
operator: getCurrentUser(),
|
||||
comment: form.value.comment,
|
||||
metadata: '{}',
|
||||
})
|
||||
@@ -82,9 +81,4 @@ const handleOk = async () => {
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -69,7 +69,6 @@ const handleOk = async () => {
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'resolve',
|
||||
operator: getCurrentUser(),
|
||||
comment: form.value.comment,
|
||||
root_cause: form.value.root_cause,
|
||||
solution: form.value.solution,
|
||||
@@ -89,9 +88,4 @@ const handleOk = async () => {
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -84,7 +84,6 @@ const handleOk = async () => {
|
||||
await createAlertProcess({
|
||||
alert_record_id: props.alertRecordId,
|
||||
action: 'silence',
|
||||
operator: getCurrentUser(),
|
||||
silence_until: silenceUntil,
|
||||
silence_reason: form.value.silence_reason,
|
||||
comment: form.value.comment,
|
||||
@@ -104,9 +103,4 @@ const handleOk = async () => {
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
function getCurrentUser() {
|
||||
// TODO: 从全局状态获取当前用户
|
||||
return 'admin'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<a-row :gutter="16">
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-form-item label="规则类型" :field="`rules[${index}].rule_type`" :rules="getRuleFieldRules('rule_type')">
|
||||
<a-select v-model="rule.rule_type" placeholder="请选择" @change="syncUpdate(index)">
|
||||
<a-select v-model="rule.rule_type" placeholder="请选择" @change="handleRuleTypeChange(index)">
|
||||
<a-option v-for="item in RULE_TYPES" :key="item.value" :value="item.value">
|
||||
{{ item.label }}
|
||||
</a-option>
|
||||
@@ -92,8 +92,18 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-form-item label="阈值" :field="`rules[${index}].threshold`" :rules="getRuleFieldRules('threshold')">
|
||||
<a-input-number v-model="rule.threshold" placeholder="请输入阈值" style="width: 100%" @change="syncUpdate(index)" />
|
||||
<a-form-item
|
||||
:label="rule.rule_type === 'dynamic' ? '偏离比例(%)' : '阈值'"
|
||||
:field="`rules[${index}].threshold`"
|
||||
:rules="getRuleFieldRules('threshold')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="rule.threshold"
|
||||
:placeholder="rule.rule_type === 'dynamic' ? '请输入偏离比例' : '请输入阈值'"
|
||||
:min="rule.rule_type === 'dynamic' ? 0 : undefined"
|
||||
style="width: 100%"
|
||||
@change="syncUpdate(index)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
@@ -115,7 +125,7 @@
|
||||
<a-input-number
|
||||
v-model="rule.eval_interval"
|
||||
placeholder="请输入"
|
||||
:min="0"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
@change="syncUpdate(index)"
|
||||
>
|
||||
@@ -123,6 +133,43 @@
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<template v-if="rule.rule_type === 'dynamic' && rule._baseline">
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-form-item label="基线回看时长" :field="`rules[${index}]._baseline.lookback_seconds`" required>
|
||||
<a-input-number
|
||||
v-model="rule._baseline.lookback_seconds"
|
||||
:min="Math.max(rule.eval_interval || 1, 1)"
|
||||
style="width: 100%"
|
||||
@change="syncUpdate(index)"
|
||||
>
|
||||
<template #suffix>秒</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-form-item label="历史基线聚合" :field="`rules[${index}]._baseline.aggregation`" required>
|
||||
<a-select v-model="rule._baseline.aggregation" @change="syncUpdate(index)">
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-form-item label="当前窗口聚合" :field="`rules[${index}]._baseline.current_aggregation`" required>
|
||||
<a-select v-model="rule._baseline.current_aggregation" @change="syncUpdate(index)">
|
||||
<a-option value="avg">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :md="6">
|
||||
<a-form-item label="基线最小样本数" :field="`rules[${index}]._baseline.min_samples`" required>
|
||||
<a-input-number v-model="rule._baseline.min_samples" :min="1" style="width: 100%" @change="syncUpdate(index)" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</template>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-card>
|
||||
@@ -148,6 +195,14 @@ import {
|
||||
interface RuleItem extends AlertRule {
|
||||
_metrics?: MetricMeta[]
|
||||
_metricsLoading?: boolean
|
||||
_baseline?: BaselineForm
|
||||
}
|
||||
|
||||
interface BaselineForm {
|
||||
lookback_seconds: number
|
||||
aggregation: 'avg' | 'max' | 'min'
|
||||
current_aggregation: 'avg' | 'max' | 'min'
|
||||
min_samples: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -171,11 +226,36 @@ const baseSeverityOptions = ref<AlertSeverity[]>([])
|
||||
const localSeverityOptions = ref<AlertSeverity[]>([])
|
||||
const severityLoading = ref(false)
|
||||
|
||||
const defaultBaselineConfig = (): BaselineForm => ({
|
||||
lookback_seconds: 3600,
|
||||
aggregation: 'avg',
|
||||
current_aggregation: 'avg',
|
||||
min_samples: 1,
|
||||
})
|
||||
|
||||
const parseBaselineConfig = (raw?: string): BaselineForm => {
|
||||
try {
|
||||
const parsed = JSON.parse(raw || '{}')
|
||||
return {
|
||||
lookback_seconds: Number(parsed.lookback_seconds) > 0 ? Number(parsed.lookback_seconds) : 3600,
|
||||
aggregation: ['avg', 'max', 'min'].includes(parsed.aggregation) ? parsed.aggregation : 'avg',
|
||||
current_aggregation: ['avg', 'max', 'min'].includes(parsed.current_aggregation) ? parsed.current_aggregation : 'avg',
|
||||
min_samples: Number(parsed.min_samples) > 0 ? Number(parsed.min_samples) : 1,
|
||||
}
|
||||
} catch {
|
||||
return defaultBaselineConfig()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 props 变化,深拷贝到本地
|
||||
watch(
|
||||
() => props.rules,
|
||||
(val) => {
|
||||
localRules.value = JSON.parse(JSON.stringify(val))
|
||||
const copied: RuleItem[] = JSON.parse(JSON.stringify(val))
|
||||
localRules.value = copied.map((rule) => ({
|
||||
...rule,
|
||||
_baseline: parseBaselineConfig(rule.baseline_config),
|
||||
}))
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
@@ -268,8 +348,9 @@ defineExpose({
|
||||
// 同步更新到父组件
|
||||
const syncUpdate = (index: number) => {
|
||||
const newRules = [...props.rules]
|
||||
const { _metrics, _metricsLoading, ...rest } = localRules.value[index]
|
||||
newRules[index] = { ...rest, _metrics, _metricsLoading } as RuleItem
|
||||
const { _metrics, _metricsLoading, _baseline, ...rest } = localRules.value[index]
|
||||
const baseline_config = rest.rule_type === 'dynamic' ? JSON.stringify(_baseline || defaultBaselineConfig()) : '{}'
|
||||
newRules[index] = { ...rest, baseline_config, _metrics, _metricsLoading } as RuleItem
|
||||
emit('update:rules', newRules)
|
||||
}
|
||||
|
||||
@@ -287,6 +368,17 @@ const handleRuleDataSourceChange = (index: number) => {
|
||||
handleLoadMetrics(index)
|
||||
}
|
||||
|
||||
const handleRuleTypeChange = (index: number) => {
|
||||
const rule = localRules.value[index]
|
||||
if (rule.rule_type === 'dynamic') {
|
||||
if (rule.compare_op === '==' || rule.compare_op === '!=') {
|
||||
rule.compare_op = '>'
|
||||
}
|
||||
rule._baseline ||= parseBaselineConfig(rule.baseline_config)
|
||||
}
|
||||
syncUpdate(index)
|
||||
}
|
||||
|
||||
// 下拉框展开时加载指标(备用触发)
|
||||
const handleMetricsDropdownChange = (visible: boolean, index: number) => {
|
||||
if (visible) {
|
||||
|
||||
Reference in New Issue
Block a user