Files
aiops/frontend/src/components/monitor/AlertRuleDialog.tsx
T
attacker 55f216cfbb feat(i18n): 安全批量转换 135 文件 — 覆盖率 34%→62%
使用 i18n.t() 全局方式(避免 hook 定位问题):
- 288 目标文件中 135 个成功转换
- 2228 处中文替换为 i18n.t() 调用
- 1006 个新翻译 key 添加到 zh-CN.json + en-US.json
- 词典总量: 3554 keys (22 namespaces)
- tsc 零错误 + Vite 构建成功
- 6 个含代码示例/数据字符串的文件安全回退
2026-07-03 02:14:20 +08:00

313 lines
13 KiB
TypeScript

import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Loader2 } from 'lucide-react';
import { alertApi, logCenterApi, type LogDataSource } from '@/api';
import { parseChannels, getChannelIcon, getChannelTypeLabel } from './alertShared';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
const emptyRuleForm = {
name: '',
description: '',
type: 'metric',
source: 'prometheus',
expression: '',
severity: 'warning',
duration: 300,
datasource_id: undefined as number | undefined,
notify_channels: [] as string[],
};
export function AlertRuleDialog({
open,
onOpenChange,
editingRule,
notifyChannels,
onSaved,
defaultType = 'metric',
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingRule?: JsonData;
notifyChannels: JsonData[];
onSaved: () => void;
/** 新建时默认规则类型 // Logan.Li */
defaultType?: 'metric' | 'log' | 'event';
}) {
const [ruleForm, setRuleForm] = useState({ ...emptyRuleForm });
const [ruleSaving, setRuleSaving] = useState(false);
const [logSources, setLogSources] = useState<LogDataSource[]>([]);
// 日志规则打开时加载对应类型的数据源列表 // Logan.Li
useEffect(() => {
if (!open || ruleForm.type !== 'log') {
setLogSources([]);
return;
}
logCenterApi.listDatasources(ruleForm.source).then((res) => {
setLogSources(res?.list || []);
}).catch(() => setLogSources([]));
}, [open, ruleForm.type, ruleForm.source]);
// 打开时同步表单(编辑时填充,新建时重置)
useEffect(() => {
if (!open) return;
if (editingRule) {
setRuleForm({
name: editingRule.name || '',
description: editingRule.description || '',
type: editingRule.type || 'metric',
source: editingRule.source || 'prometheus',
expression: editingRule.expression || '',
severity: editingRule.severity || 'warning',
duration: editingRule.duration || 300,
datasource_id: editingRule.datasource_id,
notify_channels: parseChannels(editingRule.notify_channels),
});
} else {
const base = { ...emptyRuleForm, type: defaultType };
if (defaultType === 'log') {
base.source = 'loki';
}
setRuleForm(base);
}
}, [open, editingRule, defaultType]);
const toggleNotifyChannel = (channel: string) => {
setRuleForm((prev) => ({
...prev,
notify_channels: prev.notify_channels.includes(channel)
? prev.notify_channels.filter((c) => c !== channel)
: [...prev.notify_channels, channel],
}));
};
const handleSaveRule = async () => {
if (!ruleForm.name.trim() || !ruleForm.expression.trim()) return;
setRuleSaving(true);
try {
const payload = {
name: ruleForm.name.trim(),
description: ruleForm.description.trim(),
type: ruleForm.type,
source: ruleForm.source,
expression: ruleForm.expression.trim(),
severity: ruleForm.severity,
duration: Number(ruleForm.duration) || 0,
datasource_id: ruleForm.datasource_id,
notify_channels: ruleForm.notify_channels,
};
if (editingRule) {
await alertApi.updateRule(editingRule.id, payload);
} else {
await alertApi.createRule(payload);
}
onOpenChange(false);
onSaved();
} catch (error) {
console.error('Failed to save rule:', error);
} finally {
setRuleSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="dg-input max-w-lg">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingRule ? i18n.t('monitor.alertruledialog.sb62c8bc1') : i18n.t('monitor.alertruledialog.s3564fa12')}
</DialogTitle>
<DialogDescription className="dg-text-muted">
{editingRule ? i18n.t('monitor.alertruledialog.s44a3bb2d') : i18n.t('monitor.alertruledialog.sb785eced')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.ruleName')} <span className="dg-text-red">*</span></Label>
<Input
value={ruleForm.name}
onChange={(e) => setRuleForm({ ...ruleForm, name: e.target.value })}
placeholder={i18n.t('monitor.alertruledialog.sf111cd25')}
className="dg-input"
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('aiops.llmModelDesc')} </Label>
<Textarea
value={ruleForm.description}
onChange={(e) => setRuleForm({ ...ruleForm, description: e.target.value })}
placeholder={i18n.t('monitor.alertruledialog.scc807bdb')}
rows={2}
className="dg-input resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.alertruledialog.s36582565')} </Label>
<select
value={ruleForm.type}
onChange={(e) => {
const type = e.target.value;
const source = type === 'log' ? 'loki' : 'prometheus';
setRuleForm({ ...ruleForm, type, source });
}}
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value="metric"> {i18n.t('monitor.alertruledialog.s38546de8')} </option>
<option value="log"> {i18n.t('monitor.alertHelper.srcLog')} </option>
<option value="event"> {i18n.t('monitor.alertruledialog.s80069bea')} </option>
</select>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.dashboard.custom.datasource')} </Label>
<select
value={ruleForm.source}
onChange={(e) => setRuleForm({ ...ruleForm, source: e.target.value, datasource_id: undefined })}
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
{ruleForm.type === 'log' ? (
<>
<option value="loki">Loki</option>
<option value="elasticsearch">Elasticsearch</option>
</>
) : (
<>
<option value="prometheus">Prometheus</option>
<option value="zabbix">Zabbix</option>
<option value="custom"> {i18n.t('aiops.providerCustom')} </option>
</>
)}
</select>
</div>
</div>
{ruleForm.type === 'log' && (
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.alertruledialog.sa2bc1edd')} <span className="dg-text-red">*</span></Label>
<select
value={ruleForm.datasource_id || ''}
onChange={(e) => setRuleForm({ ...ruleForm, datasource_id: Number(e.target.value) || undefined })}
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value=""> {i18n.t('monitor.alertruledialog.s9acb966d')} </option>
{logSources.map((s) => (
<option key={s.id} value={s.id}>{s.name}{s.is_default ? i18n.t('monitor.logPanel.defaultSuffix') : ''}</option>
))}
</select>
{logSources.length === 0 && (
<p className="text-xs dg-text-yellow/80">请先前往「日志库」添加 {ruleForm.source === 'loki' ? 'Loki' : 'Elasticsearch'} 数据源</p>
)}
</div>
)}
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.alertruledialog.s57ee9aea')} <span className="dg-text-red">*</span></Label>
<Input
value={ruleForm.expression}
onChange={(e) => setRuleForm({ ...ruleForm, expression: e.target.value })}
placeholder={ruleForm.type === 'log' ? '{app="api"} |= "error"' : 'cpu_usage > 80'}
className="dg-input font-mono text-sm"
/>
<p className="text-xs dg-text-muted">
{ruleForm.type === 'log'
? (ruleForm.source === 'loki' ? i18n.t('monitor.alertruledialog.s465c2bb1') : i18n.t('monitor.alertruledialog.s7072bc91'))
: i18n.t('monitor.alertruledialog.se2017efe')}
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.alertruledialog.sfce93b38')} </Label>
<select
value={ruleForm.severity}
onChange={(e) => setRuleForm({ ...ruleForm, severity: e.target.value })}
className="w-full h-9 px-3 rounded-md bg-[var(--dg-bg-tertiary)]/50 border border-[var(--dg-border)] text-[var(--dg-text-primary)] text-sm"
>
<option value="info"> {i18n.t('monitor.alertEvents.severity.info')} </option>
<option value="warning"> {i18n.t('kubernetes.event.warning')} </option>
<option value="critical"> {i18n.t('monitor.alertHelper.sevCritical')} </option>
</select>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.alertruledialog.scce774b3')} </Label>
<Input
type="number"
value={ruleForm.duration}
onChange={(e) => setRuleForm({ ...ruleForm, duration: Number(e.target.value) })}
className="dg-input"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.notificationChannel')} </Label>
{notifyChannels.length === 0 ? (
<div className="text-xs dg-text-yellow/80 bg-amber-500/5 border border-amber-500/20 rounded-md px-3 py-2">
暂无已配置的通知渠道,请先在「通知渠道配置」中添加
</div>
) : (
<div className="flex flex-wrap gap-3 pt-1">
{notifyChannels.map((ch) => {
const Icon = getChannelIcon(ch.type);
const checked = ruleForm.notify_channels.includes(String(ch.id));
return (
<label
key={ch.id}
className="flex items-center gap-2 cursor-pointer select-none text-sm text-[var(--dg-text-secondary)]"
>
<Switch
checked={checked}
onCheckedChange={() => toggleNotifyChannel(String(ch.id))}
className="data-[state=checked]:bg-blue-600"
/>
<Icon className="h-4 w-4 dg-text-secondary" />
<span>
{ch.name}
<span className="text-xs dg-text-muted ml-1">({getChannelTypeLabel(ch.type)})</span>
</span>
</label>
);
})}
</div>
)}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]"
onClick={() => onOpenChange(false)}
>
取消
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
disabled={!ruleForm.name.trim() || !ruleForm.expression.trim() || ruleSaving}
onClick={handleSaveRule}
>
{ruleSaving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{editingRule ? i18n.t('cloud.saveChanges') : i18n.t('monitor.alertruledialog.s8e1e3dea')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}