@@ -1,7 +1,7 @@
import { IconAlarm , IconBell , IconChevronRight , IconClose , IconFilter , IconPlus , IconRefresh , IconSearch , IconSetting } from '@douyinfe/semi-icons' ;
import { Button , Card , CardGroup , Collapse , Descriptions , Empty , Input , Radio , Select , SideSheet , Spin , Table , Tag , TextArea , Timeline , Typography } from '@douyinfe/semi-ui' ;
import { useMutation , useQuery , useQueryClient } from '@tanstack/react-query' ;
import { FormEvent , memo , useCallback , useEffect , useMemo , useState } from 'react' ;
import { FormEvent , memo , useCallback , useEffect , useMemo , useState , type ReactNode } from 'react' ;
import { Link , useSearchParams } from 'react-router-dom' ;
import { api } from '../../api/client' ;
import type { AlertEvent , AlertQuery , AlertRule , AlertRuleInput , AlertStatus , MetricDefinition , Page } from '../../api/types' ;
@@ -17,6 +17,7 @@ import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet' ;
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader' ;
import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel' ;
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet' ;
import { detailTriggerRow } from '../shared/detailTriggerRow' ;
import { usePlatformSession } from '../auth/AuthGate' ;
import { canAdminister , canOperate } from '../auth/session' ;
@@ -369,6 +370,13 @@ function ruleDraft(rule: AlertRule): AlertRuleInput {
} ;
}
function RuleFormSection ( { id , title , description , children } : { id : string ; title : string ; description : string ; children : ReactNode } ) {
return < section className = "v2-rule-form-section" aria-labelledby = { id } >
< header > < span > < Typography.Title heading = { 6 } id = { id } > { title } < / Typography.Title > < small > { description } < / small > < / span > < / header >
< div className = "v2-rule-form-grid" > { children } < / div >
< / section > ;
}
function RulesWorkspace ( { rules , metrics } : { rules : AlertRule [ ] ; metrics : MetricDefinition [ ] } ) {
const queryClient = useQueryClient ( ) ;
const mobileLayout = useMobileLayout ( ) ;
@@ -378,7 +386,6 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
useEffect ( ( ) = > { if ( ! selectedID && rules [ 0 ] ) { setSelectedID ( rules [ 0 ] . id ) ; setDraft ( ruleDraft ( rules [ 0 ] ) ) ; } } , [ rules , selectedID ] ) ;
const save = useMutation ( { mutationFn : api.saveAlertRuleV2 , onSuccess : async ( rule ) = > { setSelectedID ( rule . id ) ; setDraft ( ruleDraft ( rule ) ) ; await queryClient . invalidateQueries ( { queryKey : [ 'alert-rules-v2' ] } ) ; } } ) ;
const toggle = useMutation ( { mutationFn : ( rule : AlertRule ) = > api . setAlertRuleEnabledV2 ( rule . id , { version : rule.version , enabled : ! rule . enabled } ) , onSuccess : async ( ) = > { await queryClient . invalidateQueries ( { queryKey : [ 'alert-rules-v2' ] } ) ; } } ) ;
useSideSheetA11y ( mobileLayout && editorOpen , '.v2-alert-rule-editor-sidesheet' , 'v2-alert-rule-editor' , '告警规则编辑' , '关闭告警规则编辑' ) ;
const mutationError = save . error ? ? toggle . error ;
const operators = draft . valueType === 'boolean' ? BOOLEAN_OPERATORS : NUMERIC_OPERATORS ;
const availableMetrics = metrics . filter ( ( metric ) = > metric . alertable && metric . valueType === draft . valueType ) ;
@@ -417,7 +424,6 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
actions = { toggleButton }
/ > : < div className = { ` v2-alert-rule-sheet-status ${ draft . version ? '' : ' is-new' } ` } >
< span >
< Tag size = "small" color = { draft . version ? draft . enabled ? 'green' : 'grey' : 'blue' } type = "light" > { draft . version ? draft . enabled ? '已启用' : '已停用' : '新规则' } < / Tag >
< small > { draft . version ? ` v ${ draft . version } · 配置变更写入版本审计 ` : '保存后生成首个可审计版本' } < / small >
< / span >
{ toggleButton }
@@ -440,24 +446,30 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
< / Button > ;
} ) } < / nav >
< / section > : null }
< div className = "v2-rule-form-grid " >
< label > < span > 规 则 名 称 < / span > < Input required maxLength = { 80 } value = { draft . name } onChange = { ( value ) = > setDraft ( { . . . draft , name : value } ) } / > < / label >
< label > < span > 严 重 程 度 < / span > < Select value = { draft . severity } onChange = { ( value ) = > setDraft ( { . . . draft , severity : String ( value ) as AlertRuleInput [ 'severity' ] } ) } optionList = { [ { value : 'critical' , label : '紧急' } , { value : 'major' , label : '重要' } , { value : 'minor' , label : '一般' } ] } / > < / label >
< label > < span > 值 类 型 < / span > < Select value = { draft . valueType } onChange = { ( value ) = > { const valueType = String ( value ) as AlertRuleInput [ 'valueType' ] ; const metric = metrics . find ( ( item ) = > item . alertable && item . valueType === valueType ) ? . key ? ? '' ; setDraft ( { . . . draft , valueType , operator : valueType === 'boolean' ? 'eq' : 'gt' , metric } ) ; } } optionList = { [ { value : 'numeric ' , label : '数值 ' } , { value : 'boolean ' , label : '布尔 ' } ] } / > < / label >
< label > < span > 指 标 < / span > < Select disabled = { ! availableMetrics . length } value= { draft . metric } onChange = { ( value ) = > setDraft ( { . . . draft , metric : String ( value ) } ) } optionList = { availableM etrics. map ( ( metric ) = > ( { value : metric.key , label : ` ${ metric . label } ${ metric . unit ? ` ( ${ met ric. unit } ) ` : '' } ` } ) ) } / > < / label >
< label > < span > 触 发 比 较 符 < / span > < Select value = { draft . operator } onChange = { ( value ) = > { const operator = String ( value ) ; setDraft ( { . . . draft , operator , durationSec : operator === 'changed' ? 0 : draft.durationSec } ) ; } } optionList = { operator s. map ( ( value ) = > ( { value , label : operatorLabels [ value ] } ) ) } / > < / label >
{ draft . operator === 'changed' ? < label > < span > 变 化 语 义 < / span > < Input value = "false ↔ true" disabled / > < / label > : draft . valueType === 'boolean' ? < label > < span > 目 标 值 < / span > < Select value = { draft . booleanThreshold ? 'true' : 'false' } onChange = { ( value ) = > setDraft ( { . . . draft , booleanThreshold : value === 'true' } ) } optionList = { [ { value : 'true' , label : '是' } , { value : 'false' , label : '否' } ] } / > < / label > : < label > < span > { draft . operator === 'between' || draft . operator === 'outside' ? '区间下限' : '触发阈值' } < / span > < Input requir ed type = "number" step = "0.01" placeholder = { draft . metric === 'hydrogen_concentration_percent' ? '请按厂家标准填写百分比' : undefined } value = { Number . isFinite ( draft . threshold ) ? String ( draft . threshold ) : '' } onChange = { ( value ) = > setDraft ( { . . . draft , threshold : value === '' ? Number . NaN : Number ( value ) } ) } / > < / label > }
{ draft . operator === 'between' || draft . operator === 'outside' ? < label > < span > 区 间 上 限 < / span > < Input type = "number" step = "0.1" value = { String ( draft . thresholdHigh ) } onChange = { ( value ) = > setDraft ( { . . . draft , thresholdHigh : Number ( value ) } ) } / > < / label > : null }
< label > < span > 持 续 时 间 ( 秒 ) < / span > < Input type = "number" min = "0" max = "86400" disabled = { draft . operator === 'changed' } value = { String ( draft . durationSec ) } onChange = { ( value ) = > setDraft ( { . . . draft , durationSec : Number ( value ) } ) } / > < / label >
< label > < span > 恢 复 比 较 符 < / span > < Select value = { draft . recoveryOperator } onChange = { ( value ) = > setDraft ( { . . . draft , recoveryOperator : String ( value ) } ) } optionList = { [ { value : '' , label : '未配置' } , . . . [ 'gt' , 'gte' , 'lt' , 'lte' , 'eq' , 'neq' ] . map ( ( value ) = > ( { value , label : operatorLabels [ value ] } ) ) ] } / > < / label >
< label > < span > 恢 复 阈 值 < / span > < Input type = "number" step = "0.1" value = { String ( draft . recoveryThreshold ) } onChange = { ( value ) = > setDraft ( { . . . draft , recoveryThreshold : Number ( value ) } ) } / > < / label >
< label > < span > 重 复 间 隔 ( 秒 ) < / span > < Input type = "number" min = "0" max = "604800" value = { String ( draft . repeatIntervalSec ) } onChange = { ( value ) = > setDraft ( { . . . draft , repeatIntervalSec : Number ( value ) } ) } / > < / label >
< label className = "is-wide" > < span > 协 议 范 围 ( 逗 号 分 隔 ; 空 为 全 部 ) < / span > < Inpu t value = { draft . scopeProtocols . jo in( ',' ) } onChange = { ( value ) = > setList ( 'scopeProtoco ls' , value ) } / > < / label >
< label className = "is-wide" > < span > 车 辆 VIN 范 围 ( 逗 号 分 隔 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeVins . join ( ',' ) } onChange = { ( value ) = > setLis t ( 'scopeVins' , value ) } / > < / label >
< label className = "is-wide" > < span > 厂 家 范 围 ( 逗 号 分 隔 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeOems . join ( ',' ) } onChange = { ( value ) = > setLis t ( 'scopeOems' , value ) } / > < / label >
< label className = "is-wide" > < span > 车 型 范 围 ( 来 自 车 辆 主 档 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeModels . join ( ',' ) } onChange = { ( value ) = > setList ( 'scopeModels' , value ) } / > < / label >
< label className = "is-wide" > < span > 企 业 范 围 ( 来 自 车 辆 主 档 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeCompanies . join ( ',' ) } onChange = { ( value ) = > setList ( 'scopeCompanies' , value ) } / > < / label >
< label className = "is-wide" > < span > 说 明 < / span > < TextArea maxCount = { 500 } autosize = { { minRows : 3 , maxRows : 7 } } value = { draft . description } onChange = { ( value ) = > setDraf t ( { . . . draft , description : value } ) } / > < / label >
< div className = "v2-rule-form-sections " >
< RuleFormSection id = "v2-rule-trigger-heading" title = "触发条件" description = "定义监控指标、判断方式与持续时间" >
< label > < span > 规 则 名 称 < / span > < Input required maxLength = { 80 } value = { draft . name } onChange = { ( value ) = > setDraft ( { . . . draft , name : value } ) } / > < / label >
< label > < span > 严 重 程 度 < / span > < Select value = { draft . severity } onChange = { ( value ) = > setDraft ( { . . . draft , severity : String ( value ) as AlertRuleInput [ 'severity' ] } ) } optionList = { [ { value : 'critical' , label : '紧急' } , { value : 'major ' , label : '重要 ' } , { value : 'minor ' , label : '一般 ' } ] } / > < / label >
< label > < span > 值 类 型 < / span > < Select value = { draft . valueType } onChange = { ( value ) = > { const valueType = String ( value ) as AlertRuleInput [ 'valueType' ] ; const metric = m etrics. find ( ( item ) = > item . alertable && item . valueType === valueType ) ? . key ? ? '' ; setDraft ( { . . . draft , valueType , operator : valueType === 'boolean' ? 'eq' : 'gt' , metric } ) ; } } optionList = { [ { value : 'nu meric' , label : '数值' } , { value : 'boolean' , label : '布尔' } ] } / > < / label >
< label > < span > 指 标 < / span > < Select disabled = { ! availableMetrics . length } value= { draft . metric } onChange = { ( value ) = > setDraft ( { . . . draft , metric : String ( value ) } ) } optionList = { availableMetric s. map ( ( metric ) = > ( { value : metric.key , label : ` ${ metric . label } ${ metric . unit ? ` ( ${ metric . unit } ) ` : '' } ` } ) ) } / > < / label >
< label > < span > 触 发 比 较 符 < / span > < Select value = { draft . operator } onChange = { ( value ) = > { const operator = String ( value ) ; setDraft ( { . . . draft , operator , du rationSec : operator === 'chang ed' ? 0 : draft.durationSec } ) ; } } optionList = { operators . map ( ( value ) = > ( { value , label : operatorLabels [ value ] } ) ) } / > < / label >
{ draft . operator === 'changed' ? < label > < span > 变 化 语 义 < / span > < Input value = "false ↔ true" disabled / > < / label > : draft . valueType === 'boolean' ? < label > < span > 目 标 值 < / span > < Select value = { draft . booleanThreshold ? 'true' : 'false' } onChange = { ( value ) = > setDraft ( { . . . draft , booleanThreshold : value === 'true' } ) } optionList = { [ { value : 'true' , label : '是' } , { value : 'false' , label : '否' } ] } / > < / label > : < label > < span > { draft . operator === 'between' || draft . operator === 'outside' ? '区间下限' : '触发阈值' } < / span > < Input required type= "number" step = "0.0 1" placeholder = { draft . metric === 'hydrogen_concentration_percent' ? '请按厂家标准填写百分比' : undefined } value = { Number . isFinite ( draft . threshold ) ? String ( draft . threshold ) : '' } onChange = { ( value ) = > setDraft ( { . . . draft , threshold : value === '' ? Number . NaN : Number( value ) } ) } / > < / label > }
{ draft . operator === 'between' || draft . operator === 'outside' ? < label > < span > 区 间 上 限 < / span > < Input type = "number" step = "0.1" value = { String ( draft . thresholdHigh ) } onChange = { ( value ) = > setDraft ( { . . . draft , thresholdHigh : Number ( value ) } ) } / > < / label > : null }
< label > < span > 持 续 时 间 ( 秒 ) < / span > < Input type = "number" min = "0" max = "86400" disabled = { draft . operator === 'changed' } value = { String ( draft . durationSec ) } onChange = { ( value ) = > setDraft ( { . . . draft , durationSec : Number ( value ) } ) } / > < / label >
< / RuleFormSection >
< RuleFormSection id = "v2-rule-recovery-heading" title = "恢复与提醒" description = "控制恢复条件和重复通知频率" >
< label > < span > 恢 复 比 较 符 < / span > < Selec t value = { draft . recoveryOperator } onChange = { ( value ) = > setDraft ( { . . . draft , recoveryOperator : Str ing ( value ) } ) } optionList = { [ { value : '' , label : '未配置' } , . . . [ 'gt' , 'gte' , 'lt' , 'lte' , 'eq' , 'neq' ] . map ( ( value ) = > ( { value , label : operatorLabe ls[ value ] } ) ) ] } / > < / label >
< label > < span > 恢 复 阈 值 < / span > < Input type = "number" step = "0.1" value = { String ( draft . recoveryThreshold ) } onChange = { ( value ) = > setDraf t ( { . . . draft , recoveryThreshold : Number ( value ) } ) } / > < / label >
< label > < span > 重 复 间 隔 ( 秒 ) < / span > < Input type = "number" min = "0" max = "604800" value = { String ( draft . repeatIntervalSec ) } onChange = { ( value ) = > setDraf t ( { . . . draft , repeatIntervalSec : Number ( value ) } ) } / > < / label >
< / RuleFormSection >
< RuleFormSection id = "v2-rule-scope-heading" title = "适用范围与说明" description = "按协议、车辆与主数据范围精确生效" >
< label className = "is-wide" > < span > 协 议 范 围 ( 逗 号 分 隔 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeProtocols . join ( ',' ) } onChange = { ( value ) = > setLis t ( 'scopeProtocols' , value) } / > < / label >
< label className = "is-wide" > < span > 车 辆 VIN 范 围 ( 逗 号 分 隔 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeVins . join ( ',' ) } onChange = { ( value ) = > setList ( 'scopeVins' , value ) } / > < / label >
< label className = "is-wide" > < span > 厂 家 范 围 ( 逗 号 分 隔 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeOems . join ( ',' ) } onChange = { ( value ) = > setList ( 'scopeOems' , value ) } / > < / label >
< label className = "is-wide" > < span > 车 型 范 围 ( 来 自 车 辆 主 档 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeModels . join ( ',' ) } onChange = { ( value ) = > setList ( 'scopeModels' , value ) } / > < / label >
< label className = "is-wide" > < span > 企 业 范 围 ( 来 自 车 辆 主 档 ; 空 为 全 部 ) < / span > < Input value = { draft . scopeCompanies . join ( ',' ) } onChange = { ( value ) = > setList ( 'scopeCompanies' , value ) } / > < / label >
< label className = "is-wide" > < span > 说 明 < / span > < TextArea maxCount = { 500 } autosize = { { minRows : 3 , maxRows : 7 } } value = { draft . description } onChange = { ( value ) = > setDraft ( { . . . draft , description : value } ) } / > < / label >
< / RuleFormSection >
< / div >
< footer > < div > < b > 通 知 通 道 < / b > < span > 站 内 通 知 已 启 用 ; 短 信 、 邮 件 、 企 微 为 预 留 / 未 启 用 。 < / span > < / div > { mutationError ? < em > { mutationError . message } < / em > : null } < Button className = "v2-primary-button" theme = "solid" htmlType = "submit" disabled = { save . isPending || toggle . isPending } > { save . isPending ? '保存中' : '保存规则' } < / Button > < / footer >
< / form >
@@ -492,17 +504,21 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri
< / div >
< / Card >
{ ! mobileLayout ? editor : null }
{ mobileLayout ? < SideSheet
{ mobileLayout ? < Workspace SideSheet
className = "v2-alert-rule-editor-sidesheet"
variant = "editor"
visible = { editorOpen }
aria-l abel = "告警规则编辑"
ariaL abel = "告警规则编辑"
placement = "bottom"
height = "min(92dvh, 820px)"
title = { < div className = "v2-alert-sheet-title" > < strong > { draft . version ? '编辑规则' : '新建规则' } < / strong > < span > { draft . name || '配置触发条件、范围与通知策略' } < / span > < / div > }
title = { draft . version ? '编辑规则' : '新建规则' }
description = { draft . name || '配置触发条件、范围与通知策略' }
badge = { draft . version ? draft . enabled ? '已启用' : '已停用' : '新规则' }
badgeColor = { draft . version ? draft . enabled ? 'green' : 'grey' : 'blue' }
onCancel = { ( ) = > setEditorOpen ( false ) }
>
{ editorOpen ? editor : null }
< / SideSheet > : null }
< / Workspace SideSheet> : null }
< / div > ;
}