Files
aiops/frontend/src/components/monitor/log-center/LogAlertsPanel.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

120 lines
5.2 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
// LogAlertsPanel — 日志告警规则列表 // Logan.Li
import { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { Plus, Pencil, Trash2, RefreshCw, Loader2 } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { alertApi } from '@/api';
import { AlertRuleDialog } from '@/components/monitor/AlertRuleDialog';
import i18n from '@/i18n';
export function LogAlertsPanel() {
const { toast } = useToast();
const confirm = useConfirm();
const [rules, setRules] = useState<any[]>([]);
const [channels, setChannels] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<any>(null);
const load = useCallback(async () => {
setLoading(true);
try {
const [rulesRes, chRes] = await Promise.all([
alertApi.getRules(),
alertApi.getNotifyChannels(),
]);
const all = (rulesRes as any)?.list || (Array.isArray(rulesRes) ? rulesRes : []);
setRules(all.filter((r: any) => r.type === 'log' || r.source === 'loki' || r.source === 'elasticsearch'));
setChannels((chRes as any)?.list || []);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
const handleDelete = async (rule: any) => {
const ok = await confirm({ title: i18n.t('monitor.deleteRule'), description: `删除「${rule.name}」?`, variant: 'destructive', confirmText: i18n.t('kubernetes.storage.delete2') });
if (!ok) return;
try {
await alertApi.deleteRule(rule.id);
toast({ title: i18n.t('monitor.silenceRules.deleted') });
load();
} catch (e: any) {
toast({ title: i18n.t('kubernetes.msg.deleteFailed2'), description: e?.message || String(e), variant: 'destructive' });
}
};
const severityColor: Record<string, string> = {
critical: 'bg-red-500/15 dg-text-red',
warning: 'bg-amber-500/15 dg-text-yellow',
info: 'bg-blue-500/15 dg-text-blue',
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-[var(--dg-text-muted)]"> {i18n.t('monitor.logalertspanelt.s5bc514d8')} </p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={load} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />刷新
</Button>
<Button size="sm" onClick={() => { setEditing(null); setDialogOpen(true); }}>
<Plus className="h-4 w-4 mr-1" />新建
</Button>
</div>
</div>
{loading ? (
<div className="flex justify-center py-16"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>
) : rules.length === 0 ? (
<Card className="dg-card">
<CardContent className="py-12 text-center text-[var(--dg-text-muted)] text-sm">
暂无日志告警。在 Loki/ES 查询页点击右下角「叮当」可从当前查询快速创建。
</CardContent>
</Card>
) : (
<div className="space-y-2">
{rules.map(rule => (
<Card key={rule.id} className="dg-card">
<CardContent className="py-3 px-4 flex flex-wrap items-center gap-3">
<div className="flex-1 min-w-[200px]">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-[var(--dg-text-primary)]">{rule.name}</span>
<Badge variant="outline" className="text-[10px] border-[var(--dg-border)]">{rule.source || 'log'}</Badge>
<Badge className={`text-[10px] ${severityColor[rule.severity] || ''}`}>{rule.severity}</Badge>
{!rule.enabled && rule.enabled !== undefined && (
<Badge variant="outline" className="text-[10px] text-[var(--dg-text-muted)]"> {i18n.t('aiops.llmDisabled')} </Badge>
)}
</div>
<p className="text-[11px] font-mono text-[var(--dg-text-muted)] mt-1 truncate" title={rule.expression}>{rule.expression}</p>
</div>
<div className="flex gap-1 shrink-0">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => { setEditing(rule); setDialogOpen(true); }}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 dg-text-red" onClick={() => handleDelete(rule)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
<AlertRuleDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
editingRule={editing}
notifyChannels={channels}
onSaved={load}
defaultType="log"
/>
</div>
);
}