使用 i18n.t() 全局方式(避免 hook 定位问题): - 288 目标文件中 135 个成功转换 - 2228 处中文替换为 i18n.t() 调用 - 1006 个新翻译 key 添加到 zh-CN.json + en-US.json - 词典总量: 3554 keys (22 namespaces) - tsc 零错误 + Vite 构建成功 - 6 个含代码示例/数据字符串的文件安全回退
313 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|