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 个含代码示例/数据字符串的文件安全回退
This commit is contained in:
attacker committed 2026-07-03 02:14:20 +08:00
1 parent 72bfb0e12d
commit 55f216cfbb
131 files changed
+4049 -1908

No files matched your search

@@ -18,6 +18,7 @@ import { Plus, Trash2, Loader2 } from 'lucide-react';
import { alertApi } from '@/api';
import { CHANNEL_TYPES, getChannelTypeLabel } from './alertShared';
import type { AlertChannel } from '@/types';
import i18n from '@/i18n';
const defaultChannelForm = { name: '', type: 'dingtalk', config: '', enabled: true };
@@ -85,7 +86,7 @@ export function AlertChannelDialog({
};
const handleDeleteChannel = async ( id: number) => {
{ const ok = await _confirm({ title: '删除通知渠道', description: '确定删除此通知渠道?', confirmText: '删除', variant: 'destructive' }); if (!ok) return; }
{ const ok = await _confirm({ title: i18n.t('monitor.alertChannels.deleteTitle'), description: i18n.t('monitor.alertChannels.deleteConfirm'), confirmText: i18n.t('kubernetes.storage.delete2'), variant: 'destructive' }); if (!ok) return; }
try {
await alertApi.deleteNotifyChannel(id);
if (editingChannel && editingChannel.id === id) resetChannelForm();
@@ -120,7 +121,7 @@ export function AlertChannelDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="dg-input max-w-2xl">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">通知渠道配置</DialogTitle>
<DialogTitle className="text-[var(--dg-text-primary)]"> {i18n.t('monitor.alertchanneldia.s976ad48f')} </DialogTitle>
<DialogDescription className="dg-text-muted">
管理告警通知渠道(钉钉/企业微信/邮件/短信/Webhook),配置后可在告警规则中引用
</DialogDescription>
@@ -130,7 +131,7 @@ export function AlertChannelDialog({
{/* 渠道列表 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-[var(--dg-text-secondary)] text-sm">已配置渠道</Label>
<Label className="text-[var(--dg-text-secondary)] text-sm"> {i18n.t('monitor.alertchanneldia.se6ad00af')} </Label>
<Button
size="sm"
variant="outline"
@@ -173,9 +174,9 @@ export function AlertChannelDialog({
</div>
<div className="flex items-center justify-between mt-2">
{ch.test_status === 'success' ? (
<span className="text-[11px] dg-text-green">测试通过</span>
<span className="text-[11px] dg-text-green"> {i18n.t('monitor.alertChannels.testPassed')} </span>
) : (
<span className="text-[11px] dg-text-muted">未测试</span>
<span className="text-[11px] dg-text-muted"> {i18n.t('monitor.alertchanneldia.s8c852a4c')} </span>
)}
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<Button
@@ -185,7 +186,7 @@ export function AlertChannelDialog({
disabled={channelTesting === ch.id}
onClick={() => handleTestChannel(ch.id)}
>
{channelTesting === ch.id ? <Loader2 className="h-3 w-3 animate-spin" /> : '测试'}
{channelTesting === ch.id ? <Loader2 className="h-3 w-3 animate-spin" /> : i18n.t('pipeline.testTool')}
</Button>
<Button
size="sm"
@@ -206,19 +207,19 @@ export function AlertChannelDialog({
{/* 渠道表单 */}
<div className="space-y-4">
<Label className="text-[var(--dg-text-secondary)] text-sm">
{editingChannel ? '编辑渠道' : '新建渠道'}
{editingChannel ? i18n.t('monitor.alertchanneldia.sc4714508') : i18n.t('monitor.alertchanneldia.s82bd472e')}
</Label>
<div className="space-y-1.5">
<Label className="dg-text-secondary text-xs">渠道名称 <span className="dg-text-red">*</span></Label>
<Label className="dg-text-secondary text-xs"> {i18n.t('monitor.alertchanneldia.sb8cbd86d')} <span className="dg-text-red">*</span></Label>
<Input
value={channelForm.name}
onChange={(e) => setChannelForm({ ...channelForm, name: e.target.value })}
placeholder="如:运维告警群"
placeholder={i18n.t('monitor.alertChannel.namePlaceholder')}
className="dg-input h-9"
/>
</div>
<div className="space-y-1.5">
<Label className="dg-text-secondary text-xs">渠道类型</Label>
<Label className="dg-text-secondary text-xs"> {i18n.t('monitor.alertChannel.typeLabel')} </Label>
<select
value={channelForm.type}
onChange={(e) => setChannelForm({ ...channelForm, type: e.target.value })}
@@ -230,7 +231,7 @@ export function AlertChannelDialog({
</select>
</div>
<div className="space-y-1.5">
<Label className="dg-text-secondary text-xs">配置(JSON)</Label>
<Label className="dg-text-secondary text-xs"> {i18n.t('monitor.alertChannel.configJson')} </Label>
<Textarea
value={channelForm.config}
onChange={(e) => setChannelForm({ ...channelForm, config: e.target.value })}
@@ -246,7 +247,7 @@ export function AlertChannelDialog({
onCheckedChange={(v) => setChannelForm({ ...channelForm, enabled: v })}
className="data-[state=checked]:bg-emerald-600"
/>
<Label className="dg-text-secondary text-xs">启用此渠道</Label>
<Label className="dg-text-secondary text-xs"> {i18n.t('monitor.alertchanneldia.sbdab1c59')} </Label>
</div>
)}
</div>
@@ -266,7 +267,7 @@ export function AlertChannelDialog({
onClick={handleSaveChannel}
>
{channelSaving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{editingChannel ? '保存修改' : '添加渠道'}
{editingChannel ? i18n.t('cloud.saveChanges') : i18n.t('monitor.alertChannels.addChannel')}
</Button>
</DialogFooter>
</DialogContent>
@@ -20,6 +20,7 @@ import { PageShell } from '@/components/layout/PageShell';
import { StatStrip } from '@/components/ui/stat-strip';
import type { JsonData } from '@/types/common';
import type { AlertEventRecord, AlertRuleRecord, AlertChannel } from '@/types';
import i18n from '@/i18n';
export function AlertManager() {
const [activeTab, setActiveTab] = useState('events');
@@ -75,15 +76,15 @@ export function AlertManager() {
];
const pageTabs = [
{ id: 'events', label: '告警事件' },
{ id: 'rules', label: '告警规则' },
{ id: 'events', label: i18n.t('kubernetes.eventspagetsx.s0e5b83cc') },
{ id: 'rules', label: i18n.t('monitor.alertRules') },
];
return (
<PageShell
variant="list"
title="监控告警"
subtitle="告警规则管理与告警事件处理"
title={i18n.t('monitor.alertmanagertsx.sa57c10a3')}
subtitle={i18n.t('monitor.alertmanagertsx.s1a70ca90')}
icon={<Bell className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
@@ -91,10 +92,10 @@ export function AlertManager() {
metrics={(
<StatStrip
items={[
{ label: '告警规则', value: rules.length, color: 'blue' },
{ label: '触发中', value: alerts.filter((e) => e.status === 'firing').length, color: 'red' },
{ label: '今日告警', value: alerts.length, color: 'amber' },
{ label: '已恢复', value: alerts.filter((e) => e.status === 'resolved').length, color: 'emerald' },
{ label: i18n.t('monitor.alertRules'), value: rules.length, color: 'blue' },
{ label: i18n.t('monitor.alertHelper.statusFiring'), value: alerts.filter((e) => e.status === 'firing').length, color: 'red' },
{ label: i18n.t('monitor.alertmanagertsx.s240597ec'), value: alerts.length, color: 'amber' },
{ label: i18n.t('kubernetes.msg.uncordonSuccess'), value: alerts.filter((e) => e.status === 'resolved').length, color: 'emerald' },
]}
/>
)}
@@ -117,8 +118,8 @@ export function AlertManager() {
>
<Card className="dg-card">
<CardHeader className="pb-2">
<CardTitle className="text-[var(--dg-text-primary)]">告警趋势</CardTitle>
<CardDescription className="dg-text-muted">过去24小时告警统计</CardDescription>
<CardTitle className="text-[var(--dg-text-primary)]"> {i18n.t('monitor.alertmanagertsx.s358b05d0')} </CardTitle>
<CardDescription className="dg-text-muted"> {i18n.t('monitor.alertmanagertsx.s615fa6cb')} </CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px]">
@@ -151,7 +152,7 @@ export function AlertManager() {
stroke="#ef4444"
fillOpacity={1}
fill="url(#colorCritical)"
name="严重"
name={i18n.t('monitor.alertHelper.sevCritical')}
/>
<Area
type="monotone"
@@ -159,7 +160,7 @@ export function AlertManager() {
stroke="#f59e0b"
fillOpacity={1}
fill="url(#colorWarning)"
name="警告"
name={i18n.t('kubernetes.event.warning')}
/>
</AreaChart>
</ResponsiveContainer>
@@ -16,6 +16,7 @@ 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: '',
@@ -127,30 +128,30 @@ export function AlertRuleDialog({
<DialogContent className="dg-input max-w-lg">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingRule ? '编辑告警规则' : '新建告警规则'}
{editingRule ? i18n.t('monitor.alertruledialog.sb62c8bc1') : i18n.t('monitor.alertruledialog.s3564fa12')}
</DialogTitle>
<DialogDescription className="dg-text-muted">
{editingRule ? '修改告警规则的触发条件和通知方式' : '配置监控指标的告警触发条件'}
{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)]">规则名称 <span className="dg-text-red">*</span></Label>
<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="如:CPU 使用率告警"
placeholder={i18n.t('monitor.alertruledialog.sf111cd25')}
className="dg-input"
/>
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<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="规则说明"
placeholder={i18n.t('monitor.alertruledialog.scc807bdb')}
rows={2}
className="dg-input resize-none"
/>
@@ -158,7 +159,7 @@ export function AlertRuleDialog({
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">规则类型</Label>
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.alertruledialog.s36582565')} </Label>
<select
value={ruleForm.type}
onChange={(e) => {
@@ -168,13 +169,13 @@ export function AlertRuleDialog({
}}
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">指标监控</option>
<option value="log">日志告警</option>
<option value="event">事件告警</option>
<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)]">数据源</Label>
<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 })}
@@ -189,7 +190,7 @@ export function AlertRuleDialog({
<>
<option value="prometheus">Prometheus</option>
<option value="zabbix">Zabbix</option>
<option value="custom">自定义</option>
<option value="custom"> {i18n.t('aiops.providerCustom')} </option>
</>
)}
</select>
@@ -198,15 +199,15 @@ export function AlertRuleDialog({
{ruleForm.type === 'log' && (
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">日志数据源 <span className="dg-text-red">*</span></Label>
<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="">请选择数据源</option>
<option value=""> {i18n.t('monitor.alertruledialog.s9acb966d')} </option>
{logSources.map((s) => (
<option key={s.id} value={s.id}>{s.name}{s.is_default ? ' (默认)' : ''}</option>
<option key={s.id} value={s.id}>{s.name}{s.is_default ? i18n.t('monitor.logPanel.defaultSuffix') : ''}</option>
))}
</select>
{logSources.length === 0 && (
@@ -216,7 +217,7 @@ export function AlertRuleDialog({
)}
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">触发表达式 <span className="dg-text-red">*</span></Label>
<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 })}
@@ -225,26 +226,26 @@ export function AlertRuleDialog({
/>
<p className="text-xs dg-text-muted">
{ruleForm.type === 'log'
? (ruleForm.source === 'loki' ? 'LogQL 查询或 count_over_time 表达式' : 'Elasticsearch Lucene 查询')
: 'PromQL 或指标表达式,例如 cpu_usage > 80'}
? (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)]">告警级别</Label>
<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">提示</option>
<option value="warning">警告</option>
<option value="critical">严重</option>
<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)]">持续时间(秒)</Label>
<Label className="text-[var(--dg-text-secondary)]"> {i18n.t('monitor.alertruledialog.scce774b3')} </Label>
<Input
type="number"
value={ruleForm.duration}
@@ -255,7 +256,7 @@ export function AlertRuleDialog({
</div>
<div className="space-y-1.5">
<Label className="text-[var(--dg-text-secondary)]">通知渠道</Label>
<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">
暂无已配置的通知渠道,请先在「通知渠道配置」中添加
@@ -302,7 +303,7 @@ export function AlertRuleDialog({
onClick={handleSaveRule}
>
{ruleSaving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{editingRule ? '保存修改' : '创建规则'}
{editingRule ? i18n.t('cloud.saveChanges') : i18n.t('monitor.alertruledialog.s8e1e3dea')}
</Button>
</DialogFooter>
</DialogContent>
@@ -18,6 +18,7 @@ import { toast } from 'sonner';
import { alertApi } from '@/api';
import { renderRuleChannels } from './alertShared';
import type { AlertChannel, AlertRuleRecord } from '@/types';
import i18n from '@/i18n';
const COL_SPAN = 9; // 复选框 + 7 数据列 + 操作列 // Logan.Li
@@ -53,19 +54,19 @@ export function AlertRulesTab({
const handleDeleteRule = async ( id: number) => {
const ok = await confirm({
title: '删除告警规则',
description: '确定删除此告警规则?',
confirmText: '删除',
title: i18n.t('monitor.alertrulestabts.sfa5bfdc3'),
description: i18n.t('monitor.alertrulestabts.sd301558d'),
confirmText: i18n.t('kubernetes.storage.delete2'),
variant: 'destructive',
});
if (!ok) return;
try {
await alertApi.deleteRule(id);
toast.success('告警规则已删除');
toast.success(i18n.t('monitor.alertrulestabts.s20b3fdcd'));
onReload();
} catch (error) {
console.error('Failed to delete rule:', error);
toast.error('删除失败');
toast.error(i18n.t('kubernetes.msg.deleteFailed2'));
}
};
@@ -73,9 +74,9 @@ export function AlertRulesTab({
const handleBatchDelete = async () => {
if (selectedIds.length === 0) return;
const ok = await confirm({
title: '批量删除告警规则',
title: i18n.t('monitor.alertrulestabts.sbb457b70'),
description: `确定删除选中的 ${selectedIds.length} 条告警规则吗?此操作不可撤销。`,
confirmText: '批量删除',
confirmText: i18n.t('system.terminalBatchDelete'),
variant: 'destructive',
});
if (!ok) return;
@@ -86,7 +87,7 @@ export function AlertRulesTab({
onReload();
} catch (error) {
console.error('Failed to batch delete rules:', error);
toast.error('批量删除失败');
toast.error(i18n.t('system.batchDeleteFailed'));
}
};
@@ -132,14 +133,14 @@ export function AlertRulesTab({
className="border-[var(--dg-border-hover)]"
/>
</TableHead>
<TableHead className="dg-text-secondary">规则名称</TableHead>
<TableHead className="dg-text-secondary">数据源</TableHead>
<TableHead className="dg-text-secondary">触发条件</TableHead>
<TableHead className="dg-text-secondary">级别</TableHead>
<TableHead className="dg-text-secondary">持续时间</TableHead>
<TableHead className="dg-text-secondary">通知渠道</TableHead>
<TableHead className="dg-text-secondary">状态</TableHead>
<TableHead className="dg-text-secondary text-right">操作</TableHead>
<TableHead className="dg-text-secondary"> {i18n.t('monitor.ruleName')} </TableHead>
<TableHead className="dg-text-secondary"> {i18n.t('monitor.dashboard.custom.datasource')} </TableHead>
<TableHead className="dg-text-secondary"> {i18n.t('monitor.alertrulestabts.s65939ccd')} </TableHead>
<TableHead className="dg-text-secondary"> {i18n.t('kubernetes.event.eventLevel')} </TableHead>
<TableHead className="dg-text-secondary"> {i18n.t('monitor.alertDuration')} </TableHead>
<TableHead className="dg-text-secondary"> {i18n.t('monitor.notificationChannel')} </TableHead>
<TableHead className="dg-text-secondary"> {i18n.t('system.terminalStatus')} </TableHead>
<TableHead className="dg-text-secondary text-right"> {i18n.t('system.auditAction')} </TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -5,6 +5,7 @@ import { TrendingUp, Loader2, AlertCircle } from 'lucide-react';
import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts';
import type { Panel } from './GrafanaDashboard';
import { prometheusProxyApi, promDatasourceApi } from '@/api';
import i18n from '@/i18n';
// 默认 Prometheus URL 缓存
let _defaultPromURL: string | null = null;
@@ -82,7 +83,7 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
try {
const url = await getDefaultPromURL(datasourceUrl);
if (!url) {
setError('未配置 Prometheus 数据源');
setError(i18n.t('monitor.dashboard.prometheus.emptyTitle'));
setLoading(false);
return;
}
@@ -100,16 +101,16 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
});
const parsed = parseRangeResult(res);
setChartData(parsed.length > 0 ? parsed : []);
if (parsed.length === 0) setError('无数据');
if (parsed.length === 0) setError(i18n.t('kubernetes.configmap.noData'));
} else {
// stat / gauge / table 用即时查询
const res = await prometheusProxyApi.query(url, panel.query);
const parsed = parseInstantResult(res);
setInstantData(parsed);
if (parsed.length === 0) setError('无数据');
if (parsed.length === 0) setError(i18n.t('kubernetes.configmap.noData'));
}
} catch (e: any) {
setError(e?.message ? String(e.message).slice(0, 80) : '查询失败');
setError(e?.message ? String(e.message).slice(0, 80) : i18n.t('monitor.logPanel.queryFailed'));
} finally {
setLoading(false);
}
@@ -153,7 +154,7 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
{unit && <span className="text-sm text-[var(--dg-text-muted)] mt-2">{unit}</span>}
<div className="flex items-center gap-1 mt-2 dg-text-green text-sm">
<TrendingUp className="w-4 h-4" />
<span>实时</span>
<span> {i18n.t('monitor.hostDashboard.realtime')} </span>
</div>
</div>
);
@@ -187,7 +188,7 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
);
}
case 'table': {
if (instantData.length === 0) return <div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">无数据</div>;
if (instantData.length === 0) return <div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.configmap.noData')} </div>;
return (
<div className="overflow-auto max-h-[200px]">
<table className="w-full text-[11px]">
@@ -216,7 +217,7 @@ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; da
case 'graph':
default: {
if (chartData.length === 0) {
return <div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">无数据</div>;
return <div className="text-center py-4 text-xs text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.configmap.noData')} </div>;
}
return (
<div className="h-full min-h-[120px]">
@@ -8,6 +8,7 @@ import { StatStrip } from '@/components/ui/stat-strip';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { prometheusProxyApi } from '@/api';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface Props { dsUrl?: string; }
@@ -58,7 +59,7 @@ const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
]}
/>
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3 max-h-60 overflow-y-auto">
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 dg-text-purple" />节点 CPU / 内存利用率</div>
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 dg-text-purple" /> {i18n.t('monitor.prometheusdashb.se7fb2b41')} </div>
{(d.nodeCPU || []).map((n: JsonData, i: number) => {
const mem = (d.nodeMem || []).find((m: JsonData) => m.node === n.node);
return <div key={i} className="flex items-center gap-3 text-[11px] mb-1.5"><span className="w-28 truncate" title={n.node}>{n.node}</span><MiniBar val={Number(n.value||0)} label={`${Number(n.value||0).toFixed(1)}%`} /><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></div>;
@@ -70,7 +71,7 @@ const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => (
<div className="overflow-x-auto rounded-lg border border-[var(--dg-border)]">
<table className="w-full text-xs">
<thead className="bg-[var(--dg-bg-tertiary)]"><tr><th className="px-3 py-2 text-left">节点</th><th className="px-3 py-2 text-left">状态</th><th className="px-3 py-2 text-left">CPU</th><th className="px-3 py-2 text-left">内存</th><th className="px-3 py-2 text-left">磁盘</th><th className="px-3 py-2 text-left">流入</th><th className="px-3 py-2 text-left">流出</th></tr></thead>
<thead className="bg-[var(--dg-bg-tertiary)]"><tr><th className="px-3 py-2 text-left"> {i18n.t('kubernetes.node.title')} </th><th className="px-3 py-2 text-left"> {i18n.t('system.terminalStatus')} </th><th className="px-3 py-2 text-left">CPU</th><th className="px-3 py-2 text-left"> {i18n.t('kubernetes.pod.memory')} </th><th className="px-3 py-2 text-left"> {i18n.t('monitor.dashboard.col.disk')} </th><th className="px-3 py-2 text-left"> {i18n.t('monitor.prometheusdashb.s361ffa7f')} </th><th className="px-3 py-2 text-left"> {i18n.t('monitor.prometheusdashb.s37cc0a9a')} </th></tr></thead>
<tbody>
{(d.nodes || []).map((n: JsonData, i: number) => {
const inst = n.instance || n.exported_instance || '';
@@ -104,7 +105,7 @@ const PodPanel: React.FC<{ d: JsonData}> = ({ d }) => (
]}
/>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">CPU Top 10</div>{(d.cpuTop||[]).map((p: JsonData, i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><MiniBar val={Number(p.value||0)*100} label={`${Number(p.value||0).toFixed(2)}c`} color="bg-purple-500" /></div>)}</div>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">内存 Top 10</div>{(d.memTop||[]).map((p: JsonData, i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2"> {i18n.t('monitor.prometheusdashb.sb0f653eb')} </div>{(d.memTop||[]).map((p: JsonData, i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
</div>
);
@@ -143,7 +144,7 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
const data: JsonData = await prometheusProxyApi.connect(u);
setConnected(true); setTargets(data.targets||0);
loadAll(u);
} catch (e) { setConnected(false); setError((e as JsonData).message||'连接失败'); }
} catch (e) { setConnected(false); setError((e as JsonData).message||i18n.t('aiops.llmTestFailed')); }
finally { setLoading(false); }
};
@@ -171,11 +172,11 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
<Activity className="h-4 w-4 dg-text-purple" />
<CardTitle className="text-sm font-medium">Prometheus 监控</CardTitle>
{connected && <Badge className="text-[10px] bg-purple-500/10 dg-text-purple">{targets} targets</Badge>}
{connected && <div className="ml-auto"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />刷新</Button></div>}
{connected && <div className="ml-auto"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" /> {i18n.t('monitor.dashboard.refresh')} </Button></div>}
</div>
</CardHeader>
<CardContent className="px-4 pb-3 pt-0">
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-purple" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-purple" /><p className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.prometheusdashb.s5e8eb212')} </p></div>)
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs dg-text-red"><AlertTriangle className="h-4 w-4 inline mr-1" />{error}</div>)
: (
<Tabs value={tab} onValueChange={setTab}>
@@ -18,6 +18,7 @@ import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { zabbixProxyApi } from '@/api';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface ZabbixHost {
hostid: string; hostname: string; name: string; ip: string; available: string;
@@ -50,7 +51,7 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
setHosts(data.hosts || []);
setVersion(data.version || '');
setConnected(true);
} catch (e) { setConnected(false); setError((e as JsonData).message || '连接失败'); }
} catch (e) { setConnected(false); setError((e as JsonData).message || i18n.t('aiops.llmTestFailed')); }
finally { setLoading(false); }
};
@@ -97,21 +98,21 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
<CardTitle className="text-sm font-medium">Zabbix 主机监控</CardTitle>
{version && <Badge className="text-[10px] bg-blue-500/10 dg-text-blue">v{version}</Badge>}
{connected && <span className="text-[10px] text-[var(--dg-text-muted)] ml-2">{hosts.length} 台</span>}
{connected && <div className="ml-auto flex gap-1"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" />刷新</Button></div>}
{connected && <div className="ml-auto flex gap-1"><Button variant="ghost" size="sm" className="h-6 px-2 text-[10px]" onClick={refresh}><RefreshCw className="w-3 h-3 mr-1" /> {i18n.t('monitor.dashboard.refresh')} </Button></div>}
</div>
</CardHeader>
<CardContent className="px-4 pb-3 pt-0">
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-blue" /><p className="text-xs text-[var(--dg-text-muted)]">连接中…</p></div>)
{loading ? (<div className="text-center py-12"><Loader2 className="h-6 w-6 mx-auto mb-2 animate-spin dg-text-blue" /><p className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.prometheusdashb.s5e8eb212')} </p></div>)
: error ? (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-xs dg-text-red flex items-start gap-2"><AlertTriangle className="h-4 w-4 mt-0.5" /><span>{error}</span></div>)
: hosts.length === 0 ? (<div className="text-center py-8 text-[var(--dg-text-muted)]"><Server className="h-8 w-8 mx-auto mb-2 dg-text-muted" /><p className="text-sm">未找到被监控的 Zabbix 主机</p></div>)
: hosts.length === 0 ? (<div className="text-center py-8 text-[var(--dg-text-muted)]"><Server className="h-8 w-8 mx-auto mb-2 dg-text-muted" /><p className="text-sm"> {i18n.t('monitor.zabbixdashboard.sdb131ab4')} </p></div>)
: (
<div className="overflow-x-auto rounded-lg border border-[var(--dg-border)]">
<table className="w-full text-xs">
<thead className="bg-[var(--dg-bg-tertiary)] border-b border-[var(--dg-border)]"><tr>
{[
{ f: 'hostname' as SortField, l: '主机名' },{ f: 'cpu_usage' as SortField, l: 'CPU' },{ f: 'memory_usage' as SortField, l: '内存' },
{ f: 'disk_usage' as SortField, l: '磁盘' },{ f: 'load5' as SortField, l: 'Load5' },{ f: 'net_in_bps' as SortField, l: '流入' },
{ f: 'net_out_bps' as SortField, l: '流出' },{ f: 'problem_count' as SortField, l: '问题' },{ f: 'uptime_days' as SortField, l: '运行' },
{ f: 'hostname' as SortField, l: i18n.t('kubernetes.workload.hostname') },{ f: 'cpu_usage' as SortField, l: 'CPU' },{ f: 'memory_usage' as SortField, l: i18n.t('kubernetes.pod.memory') },
{ f: 'disk_usage' as SortField, l: i18n.t('monitor.dashboard.col.disk') },{ f: 'load5' as SortField, l: 'Load5' },{ f: 'net_in_bps' as SortField, l: i18n.t('monitor.prometheusdashb.s361ffa7f') },
{ f: 'net_out_bps' as SortField, l: i18n.t('monitor.prometheusdashb.s37cc0a9a') },{ f: 'problem_count' as SortField, l: i18n.t('monitor.zabbixdashboard.s5dc99f6e') },{ f: 'uptime_days' as SortField, l: i18n.t('automation.scriptRun') },
].map(({ f, l }) => (
<th key={f} className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)] cursor-pointer hover:text-[var(--dg-text-primary)] select-none whitespace-nowrap" onClick={() => toggleSort(f)}>
<div className="flex items-center gap-1">{l}<SI f={f} /></div></th>
@@ -23,6 +23,7 @@ import PromDsFormDialog from './prometheus-ds/PromDsFormDialog';
import { PageLoading } from '@/components/layout/PageLoading';
import { PageEmpty } from '@/components/layout/PageEmpty';
import { ContentPanel } from '@/components/layout/ContentPanel';
import i18n from '@/i18n';
export default function PrometheusDatasourceManager() {
const _confirm = useConfirm();
@@ -33,7 +34,7 @@ export default function PrometheusDatasourceManager() {
const [editingDs, setEditingDs] = useState<PromDS | null>(null);
useEffect(() => {
if (isError) toast.error('获取数据源列表失败');
if (isError) toast.error(i18n.t('monitor.zabbixdatasourc.s45f81749'));
}, [isError]);
const openCreate = () => {
@@ -48,16 +49,16 @@ export default function PrometheusDatasourceManager() {
const handleDelete = async (ds: PromDS) => {
const ok = await _confirm({
title: '删除数据源',
title: i18n.t('monitor.deleteDatasource'),
description: `确定删除数据源「${ds.name}」吗?此操作不可撤销。`,
variant: 'destructive',
});
if (!ok) return;
try {
await deleteMutation.mutateAsync(ds.id);
toast.success('已删除');
toast.success(i18n.t('monitor.silenceRules.deleted'));
} catch {
toast.error('删除失败');
toast.error(i18n.t('kubernetes.msg.deleteFailed2'));
}
};
@@ -66,7 +67,7 @@ export default function PrometheusDatasourceManager() {
await setDefaultMutation.mutateAsync(ds.id);
toast.success(`已将「${ds.name}」设为默认数据源`);
} catch {
toast.error('设置失败');
toast.error(i18n.t('monitor.zabbixdatasourc.s9f96034c'));
}
};
@@ -94,8 +95,8 @@ export default function PrometheusDatasourceManager() {
{sources.length === 0 ? (
<PageEmpty
icon={<Database className="h-12 w-12" />}
title="暂无 Prometheus 数据源"
description="添加数据源后可用于指标监控与监控大屏"
title={i18n.t('monitor.prometheusdatas.s99ed81c8')}
description={i18n.t('monitor.prometheusdatas.sd9a356b8')}
action={(
<Button onClick={openCreate} size="sm" className="dg-btn-primary">
<Plus className="w-4 h-4 mr-1" />添加数据源
@@ -107,11 +108,11 @@ export default function PrometheusDatasourceManager() {
<table className="w-full">
<thead className="border-b border-[var(--border-default)]">
<tr className="text-xs dg-text-secondary">
<th className="text-left py-3 px-4 font-medium">名称</th>
<th className="text-left py-3 px-4 font-medium"> {i18n.t('kubernetes.secret.secretName')} </th>
<th className="text-left py-3 px-4 font-medium">URL</th>
<th className="text-left py-3 px-4 font-medium w-20">默认</th>
<th className="text-left py-3 px-4 font-medium w-16">状态</th>
<th className="text-right py-3 px-4 font-medium w-36">操作</th>
<th className="text-left py-3 px-4 font-medium w-20"> {i18n.t('k8s.storage.default')} </th>
<th className="text-left py-3 px-4 font-medium w-16"> {i18n.t('system.terminalStatus')} </th>
<th className="text-right py-3 px-4 font-medium w-36"> {i18n.t('system.auditAction')} </th>
</tr>
</thead>
<tbody>
@@ -142,16 +143,16 @@ export default function PrometheusDatasourceManager() {
</td>
<td className="py-3 px-4">
{ds.is_default ? (
<span className="dg-badge-yellow"><Star className="w-3 h-3 mr-1 inline" />默认</span>
<span className="dg-badge-yellow"><Star className="w-3 h-3 mr-1 inline" /> {i18n.t('k8s.storage.default')} </span>
) : (
<span className="text-xs dg-text-muted">--</span>
)}
</td>
<td className="py-3 px-4">
{ds.status === 1 ? (
<span className="dg-status-running">启用</span>
<span className="dg-status-running"> {i18n.t('system.userActive')} </span>
) : (
<span className="dg-badge-muted">禁用</span>
<span className="dg-badge-muted"> {i18n.t('system.userInactive')} </span>
)}
</td>
<td className="py-3 px-4">
@@ -160,7 +161,7 @@ export default function PrometheusDatasourceManager() {
variant="ghost" size="sm"
className="h-8 px-2"
onClick={() => handleTestQuick(ds)}
title="测试连接"
title={i18n.t('aiops.llmTest')}
>
<Plug className="w-3.5 h-3.5" />
</Button>
@@ -169,7 +170,7 @@ export default function PrometheusDatasourceManager() {
variant="ghost" size="sm"
className="h-8 px-2 dg-text-yellow"
onClick={() => handleSetDefault(ds)}
title="设为默认"
title={i18n.t('aiops.llmSetDefault')}
>
<Star className="w-3.5 h-3.5" />
</Button>
@@ -32,6 +32,7 @@ import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { zabbixDatasourceApi } from '@/api';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface ZabbixDS {
id: number;
@@ -78,7 +79,7 @@ export default function ZabbixDatasourceManager() {
const res: JsonData = await zabbixDatasourceApi.list();
setSources(res?.data || res || []);
} catch {
toast.error('获取数据源列表失败');
toast.error(i18n.t('monitor.zabbixdatasourc.s45f81749'));
} finally {
setLoading(false);
}
@@ -115,38 +116,38 @@ export default function ZabbixDatasourceManager() {
};
const handleSave = async () => {
if (!form.name.trim()) { toast.error('请输入数据源名称'); return; }
if (!form.url.trim()) { toast.error('请输入 Zabbix API URL'); return; }
if (!form.username.trim()) { toast.error('请输入用户名'); return; }
if (!form.password.trim()) { toast.error('请输入密码'); return; }
if (!form.name.trim()) { toast.error(i18n.t('monitor.promDs.nameRequired')); return; }
if (!form.url.trim()) { toast.error(i18n.t('monitor.zabbixdatasourc.s2ba385d6')); return; }
if (!form.username.trim()) { toast.error(i18n.t('pipeline.usernameRequired')); return; }
if (!form.password.trim()) { toast.error(i18n.t('login.passwordRequired')); return; }
try {
if (editingId) {
await zabbixDatasourceApi.update(editingId, form);
toast.success('数据源已更新');
toast.success(i18n.t('monitor.zabbixdatasourc.s8dfdd2f4'));
} else {
await zabbixDatasourceApi.create(form);
toast.success('数据源已创建');
toast.success(i18n.t('monitor.zabbixdatasourc.sa6def01f'));
}
setDialogOpen(false);
fetchList();
} catch (err) {
toast.error(err?.message || '操作失败');
toast.error(err?.message || i18n.t('cloud.operationFailed'));
}
};
const handleDelete = async (ds: ZabbixDS) => {
const ok = await _confirm({
title: '删除数据源',
title: i18n.t('monitor.deleteDatasource'),
description: `确定删除「${ds.name}」?`,
variant: 'destructive',
});
if (!ok) return;
try {
await zabbixDatasourceApi.delete(ds.id);
toast.success('已删除');
toast.success(i18n.t('monitor.silenceRules.deleted'));
fetchList();
} catch { toast.error('删除失败'); }
} catch { toast.error(i18n.t('kubernetes.msg.deleteFailed2')); }
};
const handleSetDefault = async (ds: ZabbixDS) => {
@@ -154,12 +155,12 @@ export default function ZabbixDatasourceManager() {
await zabbixDatasourceApi.setDefault(ds.id);
toast.success(`已将「${ds.name}」设为默认`);
fetchList();
} catch { toast.error('设置失败'); }
} catch { toast.error(i18n.t('monitor.zabbixdatasourc.s9f96034c')); }
};
const handleTest = async () => {
if (!form.url.trim() || !form.username.trim()) {
toast.error('请填写 URL 和用户名');
toast.error(i18n.t('monitor.zabbixdatasourc.s1d3562b7'));
return;
}
setTesting(true);
@@ -173,7 +174,7 @@ export default function ZabbixDatasourceManager() {
const data = res?.data || res;
setTestResult(data);
} catch (err) {
setTestResult({ success: false, message: err?.message || '测试失败' });
setTestResult({ success: false, message: err?.message || i18n.t('monitor.alertChannels.testFailed') });
} finally {
setTesting(false);
}
@@ -199,7 +200,7 @@ export default function ZabbixDatasourceManager() {
/>
Zabbix 数据源
</h2>
<p className="text-xs text-[var(--dg-text-muted)]">配置 Zabbix API 连接,获取主机监控数据</p>
<p className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.zabbixdatasourc.s79e929d8')} </p>
</div>
<Button onClick={openCreate} size="sm">
<Plus className="w-4 h-4 mr-1" />添加
@@ -210,7 +211,7 @@ export default function ZabbixDatasourceManager() {
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="py-12 text-center text-[var(--dg-text-muted)]">
<Database className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p className="text-sm">暂无 Zabbix 数据源</p>
<p className="text-sm"> {i18n.t('monitor.zabbixdatasourc.sc9c9bd18')} </p>
<Button size="sm" className="mt-4" onClick={openCreate}>
<Plus className="w-4 h-4 mr-1" />添加数据源
</Button>
@@ -226,15 +227,15 @@ export default function ZabbixDatasourceManager() {
<div className="flex items-center gap-2 mb-1">
<div className={`w-2 h-2 rounded-full ${ds.status === 1 ? 'bg-emerald-400' : 'bg-[var(--dg-bg-active)]'}`} />
<span className="font-medium text-[var(--dg-text-primary)]">{ds.name}</span>
{ds.is_default && <Badge className="bg-amber-500/20 dg-text-yellow text-[10px]"><Star className="w-3 h-3 mr-0.5" />默认</Badge>}
{ds.is_default && <Badge className="bg-amber-500/20 dg-text-yellow text-[10px]"><Star className="w-3 h-3 mr-0.5" /> {i18n.t('k8s.storage.default')} </Badge>}
{ds.status === 1
? <Badge className="bg-emerald-500/20 dg-text-green text-[10px]">启用</Badge>
: <Badge className="bg-[var(--dg-bg-active)]/20 dg-text-secondary text-[10px]">禁用</Badge>}
? <Badge className="bg-emerald-500/20 dg-text-green text-[10px]"> {i18n.t('system.userActive')} </Badge>
: <Badge className="bg-[var(--dg-bg-active)]/20 dg-text-secondary text-[10px]"> {i18n.t('system.userInactive')} </Badge>}
</div>
<div className="text-xs text-[var(--dg-text-muted)] space-x-3">
<code className="text-orange-300">{ds.url}</code>
<span>用户: {ds.username}</span>
{ds.direct_db && <Badge variant="outline" className="text-[10px]">直连DB</Badge>}
{ds.direct_db && <Badge variant="outline" className="text-[10px]"> {i18n.t('monitor.zabbixdatasourc.s85260abd')} </Badge>}
</div>
</div>
<div className="flex items-center gap-1">
@@ -263,7 +264,7 @@ export default function ZabbixDatasourceManager() {
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Database className="h-5 w-5 dg-text-orange" />
{editingId ? '编辑 Zabbix 数据源' : '新建 Zabbix 数据源'}
{editingId ? i18n.t('monitor.zabbixdatasourc.sc1c87ed6') : i18n.t('monitor.zabbixdatasourc.sa77d384f')}
</DialogTitle>
<DialogDescription>
配置 Zabbix API 连接,用于主机监控与告警数据获取
@@ -274,14 +275,14 @@ export default function ZabbixDatasourceManager() {
{/* Name + Default */}
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs text-[var(--dg-text-secondary)]">名称 *</Label>
<Input className="mt-1 h-9 text-sm bg-[var(--dg-bg-primary)]" placeholder="如:生产 Zabbix"
<Label className="text-xs text-[var(--dg-text-secondary)]"> {i18n.t('deploy.form.nameStar')} </Label>
<Input className="mt-1 h-9 text-sm bg-[var(--dg-bg-primary)]" placeholder={i18n.t('monitor.zabbixdatasourc.s9f1e6a92')}
value={form.name} onChange={e => updateForm('name', e.target.value)} />
</div>
<div className="flex items-end pb-1">
<div className="flex items-center gap-2">
<Switch checked={form.is_default} onCheckedChange={v => updateForm('is_default', v)} />
<Label className="text-xs cursor-pointer">设为默认</Label>
<Label className="text-xs cursor-pointer"> {i18n.t('aiops.llmSetDefault')} </Label>
</div>
</div>
</div>
@@ -311,9 +312,9 @@ export default function ZabbixDatasourceManager() {
</button>
{showAuth && (
<div className="px-3 pb-3 space-y-3">
<Input className="h-9 text-sm bg-[var(--dg-bg-primary)]" placeholder="用户名 *"
<Input className="h-9 text-sm bg-[var(--dg-bg-primary)]" placeholder={i18n.t('system.userNameRequired')}
value={form.username} onChange={e => updateForm('username', e.target.value)} />
<Input className="h-9 text-sm bg-[var(--dg-bg-primary)]" type="password" placeholder="密码 *"
<Input className="h-9 text-sm bg-[var(--dg-bg-primary)]" type="password" placeholder={i18n.t('system.passwordRequired')}
value={form.password} onChange={e => updateForm('password', e.target.value)} />
</div>
)}
@@ -365,7 +366,7 @@ export default function ZabbixDatasourceManager() {
onClick={() => setShowOther(!showOther)}>
<div className="flex items-center gap-2">
<Settings className="w-4 h-4" />
<span>其他设置</span>
<span> {i18n.t('monitor.zabbixdatasourc.sdda36edb')} </span>
</div>
{showOther ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
@@ -376,11 +377,11 @@ export default function ZabbixDatasourceManager() {
<Switch checked={form.direct_db} onCheckedChange={v => updateForm('direct_db', v)} />
</div>
<div className="flex items-center justify-between">
<Label className="text-xs">只读用户禁用确认</Label>
<Label className="text-xs"> {i18n.t('monitor.zabbixdatasourc.s413ec771')} </Label>
<Switch checked={form.disable_acknowledges} onCheckedChange={v => updateForm('disable_acknowledges', v)} />
</div>
<div className="flex items-center justify-between">
<Label className="text-xs">禁用数据对齐</Label>
<Label className="text-xs"> {i18n.t('monitor.zabbixdatasourc.s6d4d6f81')} </Label>
<Switch checked={form.disable_data_alignment} onCheckedChange={v => updateForm('disable_data_alignment', v)} />
</div>
</div>
@@ -404,8 +405,8 @@ export default function ZabbixDatasourceManager() {
测试连接
</Button>
<div className="flex-1" />
<Button variant="ghost" size="sm" onClick={() => setDialogOpen(false)}>取消</Button>
<Button size="sm" onClick={handleSave}>保存</Button>
<Button variant="ghost" size="sm" onClick={() => setDialogOpen(false)}> {i18n.t('rbac.jobRole.cancel')} </Button>
<Button size="sm" onClick={handleSave}> {i18n.t('task.script.save')} </Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -3,6 +3,7 @@
import { toast } from 'sonner';
import { promDatasourceApi } from '@/api';
import type { PromDS, HeaderEntry, PromDsForm } from './types';
import i18n from '@/i18n';
export type TestConnectionParams = {
url: string;
@@ -106,10 +107,10 @@ export async function handleTestQuick(ds: PromDS): Promise<void> {
if (data?.success) {
toast.success(`连接成功 · ${data.target_count || 0} targets`);
} else {
toast.error(data?.message || '连接失败');
toast.error(data?.message || i18n.t('aiops.llmTestFailed'));
}
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '测试失败';
const message = err instanceof Error ? err.message : i18n.t('monitor.alertChannels.testFailed');
toast.error(message);
}
}
@@ -1,6 +1,7 @@
import { useState, useEffect, useMemo } from 'react';
import { ChevronDown, Loader2, Search } from 'lucide-react';
import { logCenterApi } from '@/api/log/log-center';
import i18n from '@/i18n';
interface Props {
datasourceId?: number;
@@ -40,9 +41,9 @@ export function EsIndexPicker({ datasourceId, value, onChange }: Props) {
onClick={() => setOpen(!open)}
className="h-10 px-2 text-xs rounded flex items-center gap-1 font-mono min-w-[150px] max-w-[220px]"
style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}
title="选择或搜索 ES 索引"
title={i18n.t('monitor.esindexpickerts.sa0eff8ea')}
>
<span className="truncate flex-1 text-left">{value || '选择索引'}</span>
<span className="truncate flex-1 text-left">{value || i18n.t('monitor.selectIndex')}</span>
{loading ? <Loader2 className="h-3 w-3 animate-spin shrink-0" /> : <ChevronDown className="h-3 w-3 shrink-0" />}
</button>
{open && (
@@ -58,7 +59,7 @@ export function EsIndexPicker({ datasourceId, value, onChange }: Props) {
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="搜索索引..."
placeholder={i18n.t('monitor.esindexpickerts.s9ae0c004')}
className="flex-1 bg-transparent text-xs outline-none font-mono"
style={{ color: 'var(--dg-text-primary)' }}
/>
@@ -88,7 +89,7 @@ export function EsIndexPicker({ datasourceId, value, onChange }: Props) {
</button>
))}
{!loading && filtered.length === 0 && (
<p className="text-xs px-2 py-3 text-center" style={{ color: 'var(--dg-text-muted)' }}>无匹配索引</p>
<p className="text-xs px-2 py-3 text-center" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.esindexpickerts.saf5cc132')} </p>
)}
</div>
</div>
@@ -4,6 +4,7 @@ import { X, BellRing, Loader2, Flame, Database, Cloud } from 'lucide-react';
import { alertApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import type { LogEngine } from './types-v3';
import i18n from '@/i18n';
interface LogQueryContext {
engine: LogEngine;
@@ -73,11 +74,11 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
notify_channels: [],
});
setSaved(true);
toast({ title: '告警规则已创建' });
toast({ title: i18n.t('monitor.savequerydialog.sd3dbea92') });
onSaved?.();
setTimeout(() => onOpenChange(false), 800);
} catch (e: any) {
toast({ title: '创建失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('monitor.dashboard.custom.toastCreateFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally {
setSaving(false);
}
@@ -94,7 +95,7 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: 'var(--dg-border)' }}>
<div className="flex items-center gap-2">
<BellRing className="h-5 w-5" style={{ color: '#f59e0b' }} />
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>创建告警规则</span>
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}> {i18n.t('monitor.logalertfromque.s5f470749')} </span>
</div>
<button
onClick={() => onOpenChange(false)}
@@ -112,12 +113,12 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded font-medium" style={{ background: `${color}15`, color }}>
<Icon className="h-3 w-3" />{meta.name}
</span>
<span className="text-[11px] truncate" style={{ color: 'var(--dg-text-secondary)' }}>{ctx.datasourceName || '默认数据源'}</span>
<span className="text-[11px] truncate" style={{ color: 'var(--dg-text-secondary)' }}>{ctx.datasourceName || i18n.t('monitor.savequerydialog.sddb4ca92')}</span>
<span className="text-[10px] ml-auto" style={{ color: 'var(--dg-text-muted)' }}>{ctx.timeRange || '1h'}</span>
</div>
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>规则名称 *</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.silenceDialog.ruleNameRequired')} </label>
<input
value={name}
onChange={e => setName(e.target.value)}
@@ -127,7 +128,7 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
</div>
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>描述</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('aiops.llmModelDesc')} </label>
<textarea
value={description}
onChange={e => setDescription(e.target.value)}
@@ -138,7 +139,7 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
</div>
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>触发条件 *</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.logalertfromque.sa6c48ab1')} </label>
<textarea
value={expression}
onChange={e => setExpression(e.target.value)}
@@ -147,26 +148,26 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-primary)' }}
/>
<p className="text-[10px] mt-1" style={{ color: 'var(--dg-text-muted)' }}>
{ctx.engine === 'loki' ? 'LogQL 告警表达式' : ctx.engine === 'sls' ? 'SLS SQL/SPL 查询' : 'ES Lucene 查询'}
{ctx.engine === 'loki' ? i18n.t('monitor.logalertfromque.s0d669ee6') : ctx.engine === 'sls' ? i18n.t('monitor.logalertfromque.sf769e374') : i18n.t('monitor.logalertfromque.sa079c76e')}
</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>级别</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('kubernetes.event.eventLevel')} </label>
<select
value={severity}
onChange={e => setSeverity(e.target.value)}
className="w-full h-9 px-2 text-xs rounded dg-input"
style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-primary)' }}
>
<option value="info">提示</option>
<option value="warning">警告</option>
<option value="critical">严重</option>
<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>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>持续(秒)</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.logalertfromque.s5ec177f4')} </label>
<input
type="number"
value={duration}
@@ -199,7 +200,7 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
style={{ background: 'var(--dg-blue)' }}
>
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
{saved ? '已创建' : '创建规则'}
{saved ? i18n.t('kubernetes.event.created') : i18n.t('monitor.alertruledialog.s8e1e3dea')}
</button>
</div>
</div>
@@ -9,6 +9,7 @@ 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();
@@ -37,14 +38,14 @@ export function LogAlertsPanel() {
useEffect(() => { load(); }, [load]);
const handleDelete = async (rule: any) => {
const ok = await confirm({ title: '删除规则', description: `删除「${rule.name}」?`, variant: 'destructive', confirmText: '删除' });
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: '已删除' });
toast({ title: i18n.t('monitor.silenceRules.deleted') });
load();
} catch (e: any) {
toast({ title: '删除失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('kubernetes.msg.deleteFailed2'), description: e?.message || String(e), variant: 'destructive' });
}
};
@@ -57,7 +58,7 @@ export function LogAlertsPanel() {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-[var(--dg-text-muted)]">仅保存告警策略(LogQL / Lucene),日志数据仍在外部 Loki / ES</p>
<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' : ''}`} />刷新
@@ -87,7 +88,7 @@ export function LogAlertsPanel() {
<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)]">已禁用</Badge>
<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>
@@ -2,6 +2,7 @@
import { useState } from 'react';
import { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react';
import type { LogCluster } from './types-v3';
import i18n from '@/i18n';
interface Props {
clusters: LogCluster[];
@@ -41,7 +42,7 @@ export function LogClusterPanel({ clusters }: Props) {
{/* 头部 */}
<div className="flex items-center justify-between px-4 py-2" style={{ background: 'var(--dg-bg-tertiary)', borderBottom: '1px solid var(--dg-border)' }}>
<span className="text-xs" style={{ color: 'var(--dg-text-muted)' }}>共 {clusters.length} 个聚类模式,总计 {total.toLocaleString()} 条日志</span>
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}>基于日志相似度自动聚类</span>
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.logclusterpanel.s38112792')} </span>
</div>
{/* 聚类列表 */}
@@ -90,7 +91,7 @@ export function LogClusterPanel({ clusters }: Props) {
{/* 展开详情 */}
{expanded[i] && (
<div className="px-4 py-3 animate-fade-in" style={{ background: 'var(--dg-bg-primary)', paddingLeft: '48px' }}>
<p className="text-[10px] mb-1" style={{ color: 'var(--dg-text-muted)' }}>样例日志:</p>
<p className="text-[10px] mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.logclusterpanel.se909e426')} </p>
<div className="p-2 rounded text-[11px] font-mono break-all" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-secondary)' }}>
{cluster.sample}
</div>
@@ -14,6 +14,7 @@ import { Loader2, Plug, RefreshCw } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { logCenterApi, type LogDataSource, type TestConnectionResult } from '@/api';
import type { LogEngine } from './log-center-types';
import i18n from '@/i18n';
export function LogDatasourceDialog({
open, onOpenChange, engine, editing, onSaved,
@@ -127,12 +128,12 @@ export function LogDatasourceDialog({
: payload();
const result: TestConnectionResult = await logCenterApi.testDatasource(data);
if (result.status === 'ok') {
toast({ title: '连接成功' });
toast({ title: i18n.t('aiops.llmTestSuccess') });
} else {
toast({ title: '连接失败', description: result.error || '未知错误', variant: 'destructive' });
toast({ title: i18n.t('aiops.llmTestFailed'), description: result.error || i18n.t('monitor.dashboard.agent.toast.unknownError'), variant: 'destructive' });
}
} catch (e: any) {
toast({ title: '连接失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('aiops.llmTestFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally {
setTesting(false);
}
@@ -147,11 +148,11 @@ export function LogDatasourceDialog({
} else {
await logCenterApi.createDatasource(payload());
}
toast({ title: '已保存' });
toast({ title: i18n.t('deploy.toast.saved') });
onOpenChange(false);
onSaved();
} catch (e: any) {
toast({ title: '保存失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('kubernetes.msg.saveFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally {
setSaving(false);
}
@@ -162,7 +163,7 @@ export function LogDatasourceDialog({
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-md max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editing ? '编辑' : '添加'}{engine === 'loki' ? ' Loki' : ' Elasticsearch'} 数据源
{editing ? i18n.t('common.edit') : i18n.t('monitor.dashboard.custom.addBtn')}{engine === 'loki' ? ' Loki' : ' Elasticsearch'} 数据源
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
仅保存连接信息,日志本体存储在您的 Loki / ES 集群
@@ -170,17 +171,17 @@ export function LogDatasourceDialog({
</DialogHeader>
<div className="space-y-3 py-1">
<div>
<Label className="text-xs text-[var(--dg-text-muted)]">名称</Label>
<Label className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('kubernetes.secret.secretName')} </Label>
<Input value={name} onChange={e => setName(e.target.value)} className="mt-1 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
</div>
<div>
<Label className="text-xs text-[var(--dg-text-muted)]">地址</Label>
<Label className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.datasourceUrl')} </Label>
<Input value={url} onChange={e => setUrl(e.target.value)} placeholder="http://host:port" className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
</div>
{engine === 'elasticsearch' && (
<div>
<div className="flex items-center justify-between">
<Label className="text-xs text-[var(--dg-text-muted)]">默认索引</Label>
<Label className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.logdatasourcedi.s6621b501')} </Label>
<Button
variant="ghost"
size="sm"
@@ -210,7 +211,7 @@ export function LogDatasourceDialog({
获取索引
</Button>
</div>
<Input value={defaultIndex} onChange={e => setDefaultIndex(e.target.value)} placeholder="索引名或通配符,如 .monitoring-es-7-*" className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input value={defaultIndex} onChange={e => setDefaultIndex(e.target.value)} placeholder={i18n.t('monitor.logdatasourcedi.s6064025e')} className="mt-1 h-9 font-mono text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
{/* 已获取索引快捷选择(支持通配) // Logan.Li */}
{indices.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1 max-h-24 overflow-auto">
@@ -245,23 +246,23 @@ export function LogDatasourceDialog({
</div>
)}
<div>
<Label className="text-xs text-[var(--dg-text-muted)]">认证方式</Label>
<Label className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.dashboard.creds.authType')} </Label>
<Select value={authType} onValueChange={(v) => setAuthType(v as LogDataSource['auth_type'])}>
<SelectTrigger className="mt-1 h-9 w-full bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<SelectItem value="none" className="text-xs">无认证</SelectItem>
<SelectItem value="none" className="text-xs"> {i18n.t('monitor.logdatasourcedi.s7870c3f1')} </SelectItem>
<SelectItem value="basic" className="text-xs">Basic Auth</SelectItem>
<SelectItem value="bearer" className="text-xs">Bearer Token</SelectItem>
<SelectItem value="custom" className="text-xs">自定义 Headers</SelectItem>
<SelectItem value="custom" className="text-xs"> {i18n.t('monitor.logdatasourcedi.s2e722014')} </SelectItem>
</SelectContent>
</Select>
</div>
{authType === 'basic' && (
<div className="grid grid-cols-2 gap-2">
<Input value={user} onChange={e => setUser(e.target.value)} placeholder="用户名" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder="密码" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input value={user} onChange={e => setUser(e.target.value)} placeholder={i18n.t('system.userName')} className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder={i18n.t('system.passwordLabel')} className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
</div>
)}
{authType === 'bearer' && (
@@ -277,23 +278,23 @@ export function LogDatasourceDialog({
const next = [...headers];
next[i].name = e.target.value;
setHeaders(next);
}} placeholder="Header 名,如 X-Scope-OrgID" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
}} placeholder={i18n.t('monitor.logdatasourcedi.sc7f8ebda')} className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
<Input value={h.value} onChange={e => {
const next = [...headers];
next[i].value = e.target.value;
setHeaders(next);
}} placeholder="Header 值" className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
}} placeholder={i18n.t('monitor.logdatasourcedi.scf7126fb')} className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
</div>
))}
<Button variant="outline" size="sm" className="h-7 text-xs border-[var(--dg-border)]" onClick={() => setHeaders([...headers, { name: '', value: '' }])}>添加 Header</Button>
<Button variant="outline" size="sm" className="h-7 text-xs border-[var(--dg-border)]" onClick={() => setHeaders([...headers, { name: '', value: '' }])}> {i18n.t('monitor.logdatasourcedi.s86a7a65e')} </Button>
</div>
)}
<div className="flex items-center justify-between">
<Label className="text-xs text-[var(--dg-text-muted)]">跳过 TLS 校验</Label>
<Label className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.logdatasourcedi.sa441a86f')} </Label>
<Switch checked={skipTLS} onCheckedChange={setSkipTLS} />
</div>
<div className="flex items-center justify-between">
<Label className="text-xs text-[var(--dg-text-muted)]">设为默认</Label>
<Label className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('aiops.llmSetDefault')} </Label>
<Switch checked={isDefault} onCheckedChange={setIsDefault} />
</div>
</div>
@@ -11,6 +11,7 @@ import { useConfirm } from '@/components/ui/confirm-dialog';
import { logCenterApi, type LogDataSource, type TestConnectionResult } from '@/api';
import { LogDatasourceDialog } from './LogDatasourceDialog';
import type { LogEngine } from './log-center-types';
import i18n from '@/i18n';
export function LogDatasourcePage() {
const { toast } = useToast();
@@ -46,18 +47,18 @@ export function LogDatasourcePage() {
const handleDelete = async (ds: LogDataSource) => {
const ok = await confirm({
title: '删除数据源',
title: i18n.t('monitor.deleteDatasource'),
description: `确认删除「${ds.name}」?`,
variant: 'destructive',
confirmText: '删除',
confirmText: i18n.t('kubernetes.storage.delete2'),
});
if (!ok) return;
try {
await logCenterApi.deleteDatasource(ds.id);
toast({ title: '已删除' });
toast({ title: i18n.t('monitor.silenceRules.deleted') });
load();
} catch (e) {
toast({ title: '删除失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('kubernetes.msg.deleteFailed2'), description: e?.message || String(e), variant: 'destructive' });
}
};
@@ -66,22 +67,22 @@ export function LogDatasourcePage() {
// 已保存数据源通过 datasource_id 测试,避免密码丢失或前端暴露密码。// Logan.Li
const result: TestConnectionResult = await logCenterApi.testDatasource({ datasource_id: ds.id });
if (result.status === 'ok') {
toast({ title: '连接成功', description: `${ds.name} 可达` });
toast({ title: i18n.t('aiops.llmTestSuccess'), description: `${ds.name} 可达` });
} else {
toast({ title: '连接失败', description: result.error || '未知错误', variant: 'destructive' });
toast({ title: i18n.t('aiops.llmTestFailed'), description: result.error || i18n.t('monitor.dashboard.agent.toast.unknownError'), variant: 'destructive' });
}
} catch (e) {
toast({ title: '连接失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('aiops.llmTestFailed'), description: e?.message || String(e), variant: 'destructive' });
}
};
const handleSetDefault = async (ds: LogDataSource) => {
try {
await logCenterApi.setDefaultDatasource(ds.id);
toast({ title: '已设为默认' });
toast({ title: i18n.t('approval.msg.setDefaultSuccess') });
load();
} catch (e) {
toast({ title: '操作失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('cloud.operationFailed'), description: e?.message || String(e), variant: 'destructive' });
}
};
@@ -92,39 +93,39 @@ export function LogDatasourcePage() {
// DataTable 列定义
const columns: DataTableColumn<LogDataSource>[] = [
{ key: 'name', header: '名称', sortable: true, render: (ds) => <span className="font-medium text-[var(--dg-text-primary)]">{ds.name}</span> },
{ key: 'name', header: i18n.t('kubernetes.secret.secretName'), sortable: true, render: (ds) => <span className="font-medium text-[var(--dg-text-primary)]">{ds.name}</span> },
{
key: 'type', header: '类型', sortable: true, width: 120,
key: 'type', header: i18n.t('system.credentialType'), sortable: true, width: 120,
render: (ds) => <Badge className={engineBadge(ds.type)}>{engineLabel(ds.type)}</Badge>,
},
{
key: 'url', header: '地址',
key: 'url', header: i18n.t('monitor.datasourceUrl'),
render: (ds) => <span className="text-xs font-mono text-[var(--dg-text-muted)]">{ds.url}</span>,
},
{
key: 'is_default', header: '默认', width: 60, align: 'center', sortable: true,
key: 'is_default', header: i18n.t('k8s.storage.default'), width: 60, align: 'center', sortable: true,
render: (ds) => ds.is_default ? <Star className="h-4 w-4 dg-text-yellow mx-auto" /> : <span className="text-xs text-[var(--dg-text-muted)]">—</span>,
},
{
key: 'actions', header: '操作', align: 'right', hideable: false,
key: 'actions', header: i18n.t('system.auditAction'), align: 'right', hideable: false,
render: (ds) => (
<div className="flex items-center justify-end gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
title="测试连接" onClick={() => handleTest(ds)}>
title={i18n.t('aiops.llmTest')} onClick={() => handleTest(ds)}>
<Plug className="h-3.5 w-3.5" />
</Button>
{!ds.is_default && (
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:dg-text-yellow"
title="设为默认" onClick={() => handleSetDefault(ds)}>
title={i18n.t('aiops.llmSetDefault')} onClick={() => handleSetDefault(ds)}>
<Star className="h-3.5 w-3.5" />
</Button>
)}
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
title="编辑" onClick={() => handleEdit(ds)}>
title={i18n.t('common.edit')} onClick={() => handleEdit(ds)}>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:dg-text-red"
title="删除" onClick={() => handleDelete(ds)}>
title={i18n.t('kubernetes.storage.delete2')} onClick={() => handleDelete(ds)}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
@@ -146,10 +147,10 @@ export function LogDatasourcePage() {
rowKey={(ds) => ds.id}
loading={loading}
searchable
searchPlaceholder="搜索数据源名称或地址..."
searchPlaceholder={i18n.t('monitor.logdatasourcepa.seb5e4d30')}
searchKeys={['name', 'url']}
sortable
emptyText="暂无数据源,点击上方按钮添加"
emptyText={i18n.t('monitor.logdatasourcepa.sc8a96073')}
toolbarExtra={
<>
<Button variant="outline" size="sm" className="h-8 border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]" onClick={load}>
@@ -4,6 +4,7 @@ import { Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useLogQueryContext } from './LogQueryContext';
import { LogAlertFromQueryDialog } from './LogAlertFromQueryDialog';
import i18n from '@/i18n';
export function LogDingAssistant({ onRuleCreated }: { onRuleCreated?: () => void }) {
const { context } = useLogQueryContext();
@@ -19,10 +20,10 @@ export function LogDingAssistant({ onRuleCreated }: { onRuleCreated?: () => void
size="lg"
className="h-12 rounded-full shadow-lg bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-400 hover:to-orange-400 text-white gap-2 px-5"
onClick={() => setOpen(true)}
title="叮当 · 添加告警"
title={i18n.t('monitor.logdingassistan.s26b79b6b')}
>
<Sparkles className="h-5 w-5" />
<span className="font-medium">叮当</span>
<span className="font-medium"> {i18n.t('monitor.logdingassistan.s1a663141')} </span>
</Button>
</div>
<LogAlertFromQueryDialog
@@ -2,6 +2,7 @@
import { useState, useMemo } from 'react';
import { Search, ChevronDown, ChevronRight } from 'lucide-react';
import type { LogField } from './types-v3';
import i18n from '@/i18n';
interface LogFieldSidebarProps {
fields: LogField[];
@@ -64,7 +65,7 @@ export function LogFieldSidebar({
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="搜索字段"
placeholder={i18n.t('monitor.logfieldsidebar.s2d3013c3')}
className="w-full h-7 pl-7 pr-2 text-xs rounded-md dg-input"
style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}
/>
@@ -1,13 +1,14 @@
import { NavLink } from 'react-router-dom';
import { FileSearch, Database, HardDrive, AlertTriangle, Shield } from 'lucide-react';
import { cn } from '@/lib/utils';
import i18n from '@/i18n';
const NAV_ITEMS = [
{ path: '/log-center/loki', label: 'Loki 查询', icon: FileSearch },
{ path: '/log-center/es', label: 'ES 查询', icon: Database },
{ path: '/log-center/datasources', label: '日志库', icon: HardDrive },
{ path: '/log-center/alerts', label: '日志告警', icon: AlertTriangle },
{ path: '/log-center/rbac', label: '日志权限', icon: Shield },
{ path: '/log-center/loki', label: i18n.t('monitor.lokiQuery'), icon: FileSearch },
{ path: '/log-center/es', label: i18n.t('monitor.esQuery'), icon: Database },
{ path: '/log-center/datasources', label: i18n.t('monitor.logLibrary'), icon: HardDrive },
{ path: '/log-center/alerts', label: i18n.t('monitor.alertHelper.srcLog'), icon: AlertTriangle },
{ path: '/log-center/rbac', label: i18n.t('monitor.logCenter.rbac'), icon: Shield },
];
// 日志中心二级导航 // Logan.Li
@@ -1,6 +1,7 @@
// LogQueryToolbar.tsx — 查询工具栏(统计条 + 分页 + 操作)// Logan.Li
import { ArrowUpDown, Bell, Download, Copy, ChevronLeft, ChevronRight, ChevronsDown } from 'lucide-react';
import { useState } from 'react';
import i18n from '@/i18n';
interface Props {
total: number;
@@ -23,9 +24,9 @@ interface Props {
}
const STATUS_LABEL: Record<string, { text: string; color: string }> = {
precise: { text: '结果精确', color: '#22c55e' },
partial: { text: '部分命中', color: '#f59e0b' },
failed: { text: '查询失败', color: '#ef4444' },
precise: { text: i18n.t('monitor.logquerytoolbar.sd0368470'), color: '#22c55e' },
partial: { text: i18n.t('monitor.logquerytoolbar.s8b09f4fe'), color: '#f59e0b' },
failed: { text: i18n.t('monitor.logPanel.queryFailed'), color: '#ef4444' },
};
export function LogQueryToolbar({
@@ -121,7 +122,7 @@ export function LogQueryToolbar({
</button>
<div className="flex items-center gap-1 ml-1">
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}>到第</span>
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.logquerytoolbar.s8b76adb2')} </span>
<input
type="number"
value={jumpPage}
@@ -152,7 +153,7 @@ export function LogQueryToolbar({
onClick={onSortToggle}
className="h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title={sortDirection === 'desc' ? '降序' : '升序'}
title={sortDirection === 'desc' ? i18n.t('common.descending') : i18n.t('common.ascending')}
>
<ArrowUpDown className="h-3.5 w-3.5" />
</button>
@@ -160,7 +161,7 @@ export function LogQueryToolbar({
onClick={onExport}
className="h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title="导出"
title={i18n.t('common.export2')}
>
<Download className="h-3.5 w-3.5" />
</button>
@@ -168,7 +169,7 @@ export function LogQueryToolbar({
onClick={onCopy}
className="h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title="复制"
title={i18n.t('aiops.copyMessage')}
>
<Copy className="h-3.5 w-3.5" />
</button>
@@ -176,7 +177,7 @@ export function LogQueryToolbar({
onClick={onBellClick}
className="relative h-7 w-7 flex items-center justify-center rounded border transition-colors"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}
title="关联告警"
title={i18n.t('aiops.alertAnalysisRelated')}
>
<Bell className="h-3.5 w-3.5" />
{hasAlertRule && (
@@ -18,20 +18,21 @@ import { LogRoleCreateDialog } from './log-rbac/LogRoleCreateDialog';
import { matchSampleScopes, SAMPLE_SCOPES, scopeDisplayLabel } from './log-rbac/matchLogScope';
import type { LogDataSource } from '@/api/log/log-center';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
const ROLE_LABEL: Record<string, string> = {
viewer: '查看者',
analyst: '分析者',
viewer: i18n.t('pipeline.viewer'),
analyst: i18n.t('monitor.logrbactsx.sa72dec16'),
};
const ENGINE_LABEL: Record<string, string> = {
'*': '全部',
'*': i18n.t('system.auditAll'),
loki: 'Loki',
elasticsearch: 'ES',
};
const FIELD_LABEL: Record<string, string> = {
index: '索引',
index: i18n.t('monitor.logIndex'),
app: 'app',
namespace: 'namespace',
job: 'job',
@@ -106,7 +107,7 @@ export default function LogRBAC() {
const handleCreate = async () => {
if (!newName.trim() || !newCode.trim() || !newPattern.trim()) {
toast.error('请填写名称、标识与正则'); return;
toast.error(i18n.t('monitor.logrbactsx.sf6e80bf0')); return;
}
setSaving(true);
try {
@@ -120,83 +121,83 @@ export default function LogRBAC() {
description: newDesc,
...(newDatasourceId ? { datasource_id: Number(newDatasourceId) } : {}),
});
toast.success('已创建');
toast.success(i18n.t('kubernetes.event.created'));
setCreateOpen(false);
setNewName(''); setNewCode(''); setNewEngine('*'); setNewMatchField('app');
setNewPattern(''); setNewRole('viewer'); setNewDatasourceId(''); setNewDesc('');
loadData();
} catch (e: unknown) {
toast.error((e as Error)?.message || '创建失败');
toast.error((e as Error)?.message || i18n.t('monitor.dashboard.custom.toastCreateFailed'));
} finally { setSaving(false); }
};
const handleUpdate = async (id: number) => {
if (!editPattern.trim()) { toast.error('正则不能为空'); return; }
if (!editPattern.trim()) { toast.error(i18n.t('pipeline.regexEmpty')); return; }
setSaving(true);
try {
await logRbacApi.updateRole(id, { pattern: editPattern, role: editRole });
toast.success('已更新');
toast.success(i18n.t('deploy.toast.updated'));
setEditing(null);
loadData();
} catch (e: unknown) {
toast.error((e as Error)?.message || '更新失败');
toast.error((e as Error)?.message || i18n.t('kubernetes.deployment.imageUpdateFailed'));
} finally { setSaving(false); }
};
const handleDelete = async (role: LogScopeRole) => {
const ok = await _confirm({
title: '删除权限',
title: i18n.t('monitor.logrbactsx.s5d79e436'),
description: `确定删除「${role.name}」?将影响 ${matchCount[role.id]?.length || 0} 个示例范围`,
variant: 'destructive',
});
if (!ok) return;
try { await logRbacApi.deleteRole(role.id); toast.success('已删除'); loadData(); }
catch { toast.error('删除失败'); }
try { await logRbacApi.deleteRole(role.id); toast.success(i18n.t('monitor.silenceRules.deleted')); loadData(); }
catch { toast.error(i18n.t('kubernetes.msg.deleteFailed2')); }
};
const handleToggleStatus = async (role: LogScopeRole) => {
try {
await logRbacApi.updateRole(role.id, { status: role.status ? 0 : 1 });
toast.success(role.status ? '已禁用' : '已启用');
toast.success(role.status ? i18n.t('aiops.llmDisabled') : i18n.t('aiops.llmEnabled'));
loadData();
} catch { toast.error('操作失败'); }
} catch { toast.error(i18n.t('cloud.operationFailed')); }
};
const handleAssign = async () => {
if (!selectedUserId || !assignRoleId) { toast.error('请选择用户和权限'); return; }
if (!selectedUserId || !assignRoleId) { toast.error(i18n.t('monitor.logrbactsx.s0fc53985')); return; }
setAssignLoading(true);
try {
await logRbacApi.assignRole(Number(selectedUserId), {
log_scope_role_id: Number(assignRoleId),
});
toast.success('已分配');
toast.success(i18n.t('deploy.toast.assigned'));
setAssignRoleId('');
loadAssignments();
} catch (e: unknown) {
toast.error((e as Error)?.message || '分配失败');
toast.error((e as Error)?.message || i18n.t('deploy.toast.assignFailed'));
} finally { setAssignLoading(false); }
};
const handleRevoke = async (id: number) => {
try {
await logRbacApi.removeRole(Number(selectedUserId), id);
toast.success('已撤销');
toast.success(i18n.t('deploy.toast.revoked'));
loadAssignments();
} catch { toast.error('撤销失败'); }
} catch { toast.error(i18n.t('deploy.toast.revokeFailed')); }
};
const selectedUser = users.find((u) => String(u.id) === selectedUserId);
const pageTabs = [
{ id: 'roles', label: '权限策略', badge: roles.length || undefined },
{ id: 'users', label: '用户授权' },
{ id: 'preview', label: '命中预览' },
{ id: 'roles', label: i18n.t('monitor.logrbactsx.s93ef07cd'), badge: roles.length || undefined },
{ id: 'users', label: i18n.t('monitor.logrbactsx.s2de89571') },
{ id: 'preview', label: i18n.t('pipeline.matchPreview') },
];
return (
<PageShell
variant="list"
title="日志权限"
subtitle="按 ES 索引 / Loki Label 正则限制查询范围"
title={i18n.t('monitor.logCenter.rbac')}
subtitle={i18n.t('monitor.logCenter.rbacDesc')}
icon={<ScrollText className="h-5 w-5" />}
loading={loading}
tabs={pageTabs}
@@ -213,9 +214,9 @@ export default function LogRBAC() {
roles.length === 0 ? (
<PageEmpty
icon={<Shield className="h-10 w-10" />}
title="暂无日志权限策略"
description="创建策略并分配给用户后,将按正则限制可查询的索引与 Label 范围"
action={<Button onClick={() => setCreateOpen(true)}><Plus className="h-4 w-4 mr-1" />新建权限</Button>}
title={i18n.t('monitor.logrbactsx.s4a00d253')}
description={i18n.t('monitor.logrbactsx.sd11b7407')}
action={<Button onClick={() => setCreateOpen(true)}><Plus className="h-4 w-4 mr-1" /> {i18n.t('monitor.logrbactsx.s6058845c')} </Button>}
/>
) : (
<div className="space-y-2">
@@ -235,7 +236,7 @@ export default function LogRBAC() {
<Badge variant="outline" className="text-[10px] border-[var(--dg-border)]">
{FIELD_LABEL[role.match_field] || role.match_field}
</Badge>
{role.status === 0 && <Badge variant="outline" className="text-[10px]">已禁用</Badge>}
{role.status === 0 && <Badge variant="outline" className="text-[10px]"> {i18n.t('aiops.llmDisabled')} </Badge>}
<Badge
variant="outline"
className="text-[10px] cursor-pointer border-[var(--dg-border)]"
@@ -256,8 +257,8 @@ export default function LogRBAC() {
<Select value={editRole} onValueChange={setEditRole}>
<SelectTrigger className="h-7 w-20 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="viewer">查看者</SelectItem>
<SelectItem value="analyst">分析者</SelectItem>
<SelectItem value="viewer"> {i18n.t('pipeline.viewer')} </SelectItem>
<SelectItem value="analyst"> {i18n.t('monitor.logrbactsx.sa72dec16')} </SelectItem>
</SelectContent>
</Select>
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => handleUpdate(role.id)}><Check className="w-3.5 h-3.5" /></Button>
@@ -265,8 +266,8 @@ export default function LogRBAC() {
</>
) : (
<>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => { setEditing(role.id); setEditPattern(role.pattern); setEditRole(role.role); }}>编辑</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleToggleStatus(role)}>{role.status ? '禁用' : '启用'}</Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => { setEditing(role.id); setEditPattern(role.pattern); setEditRole(role.role); }}> {i18n.t('common.edit')} </Button>
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleToggleStatus(role)}>{role.status ? i18n.t('system.userInactive') : i18n.t('system.userActive')}</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-red)]" onClick={() => handleDelete(role)}><Trash2 className="w-3.5 h-3.5" /></Button>
</>
)}
@@ -287,9 +288,9 @@ export default function LogRBAC() {
</p>
<div className="flex flex-wrap gap-3 items-end">
<div className="space-y-1 flex-1 min-w-[200px]">
<Label className="text-xs">用户</Label>
<Label className="text-xs"> {i18n.t('system.terminalUser')} </Label>
<Select value={selectedUserId} onValueChange={setSelectedUserId}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder="选择用户" /></SelectTrigger>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder={i18n.t('deploy.rbac.selectUser')} /></SelectTrigger>
<SelectContent className="max-h-60">
{users.map((u) => (
<SelectItem key={u.id as number} value={String(u.id)}>
@@ -300,9 +301,9 @@ export default function LogRBAC() {
</Select>
</div>
<div className="space-y-1 w-48">
<Label className="text-xs">权限策略</Label>
<Label className="text-xs"> {i18n.t('monitor.logrbactsx.s93ef07cd')} </Label>
<Select value={assignRoleId} onValueChange={setAssignRoleId}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder="选择策略" /></SelectTrigger>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder={i18n.t('kubernetes.hpa.selectPolicy')} /></SelectTrigger>
<SelectContent>
{roles.filter((r) => r.status === 1).map((r) => (
<SelectItem key={r.id} value={String(r.id)}>{r.name} ({ROLE_LABEL[r.role]})</SelectItem>
@@ -317,7 +318,7 @@ export default function LogRBAC() {
</div>
{selectedUser ? (
assignments.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)]">该用户尚未分配日志权限</p>
<p className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.logrbactsx.sa120fc78')} </p>
) : (
<div className="space-y-1.5">
{assignments.map((a) => {
@@ -338,7 +339,7 @@ export default function LogRBAC() {
</div>
)
) : (
<p className="text-xs text-[var(--dg-text-muted)] flex items-center gap-1"><Users className="w-3.5 h-3.5" />选择用户后查看已分配权限</p>
<p className="text-xs text-[var(--dg-text-muted)] flex items-center gap-1"><Users className="w-3.5 h-3.5" /> {i18n.t('monitor.logrbactsx.s9be00c95')} </p>
)}
</CardContent>
</Card>
@@ -371,7 +372,7 @@ export default function LogRBAC() {
</Badge>
))}
{previewRoleId !== null && previewMatches.length === 0 && (
<span className="text-xs text-[var(--dg-text-muted)]">无匹配 — 请调整正则</span>
<span className="text-xs text-[var(--dg-text-muted)]"> {i18n.t('monitor.logrbactsx.s64096f4d')} </span>
)}
</div>
</CardContent>
@@ -3,6 +3,7 @@ import { ChevronDown, ChevronRight, Loader2, Tags } from 'lucide-react';
import { logCenterApi } from '@/api/log/log-center';
import { extractLokiSelector, mergeLokiSelector } from './query-builder';
import type { TimeRange } from './types-v3';
import i18n from '@/i18n';
interface Props {
datasourceId?: number;
@@ -95,7 +96,7 @@ export function LokiLabelBrowser({ datasourceId, query, timeRange, onQueryChange
))}
</div>
<div className="flex-1 overflow-y-auto max-h-48 p-1">
{!activeLabel && <p className="text-[11px] px-2 py-4 text-center" style={{ color: 'var(--dg-text-muted)' }}>选择左侧标签</p>}
{!activeLabel && <p className="text-[11px] px-2 py-4 text-center" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.lokilabelbrowse.s088b9477')} </p>}
{loadingValues && <div className="flex justify-center py-6"><Loader2 className="h-4 w-4 animate-spin" /></div>}
{!loadingValues && activeLabel && values.map((v) => (
<button
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import type { QueryHistoryItem } from './types-v3';
import { historyStorage } from './types-v3';
import { LogAlertBell } from './LogAlertBell';
import i18n from '@/i18n';
interface Props {
visible: boolean;
@@ -20,7 +21,7 @@ const ENGINE_ICONS: Record<string, { Icon: typeof Flame; color: string }> = {
function relativeTime(ts: number): string {
const diff = Date.now() - ts;
if (diff < 60000) return '刚刚';
if (diff < 60000) return i18n.t('task.cronJob.time.justNow');
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
return `${Math.floor(diff / 86400000)}天前`;
@@ -59,7 +60,7 @@ export function QueryHistoryPanel({ visible, onClose, onSelect, onCreateAlert }:
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: 'var(--dg-border)' }}>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" style={{ color: 'var(--dg-text-muted)' }} />
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>查询历史</span>
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}> {i18n.t('monitor.logPanel.history')} </span>
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-muted)' }}>
{history.length} 条
</span>
@@ -69,7 +70,7 @@ export function QueryHistoryPanel({ visible, onClose, onSelect, onCreateAlert }:
onClick={handleClear}
className="h-7 w-7 flex items-center justify-center rounded transition-colors"
style={{ color: 'var(--dg-text-muted)' }}
title="清空"
title={i18n.t('common.clear')}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
@@ -88,8 +89,8 @@ export function QueryHistoryPanel({ visible, onClose, onSelect, onCreateAlert }:
{history.length === 0 ? (
<div className="flex flex-col items-center justify-center h-48 gap-2">
<Clock className="h-8 w-8" style={{ color: 'var(--dg-border)' }} />
<span className="text-xs" style={{ color: 'var(--dg-text-muted)' }}>暂无查询记录</span>
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}>执行查询后将自动记录</span>
<span className="text-xs" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.queryhistorypan.s1e60bc17')} </span>
<span className="text-[10px]" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.queryhistorypan.s3895021f')} </span>
</div>
) : (
<div className="divide-y" style={{ borderColor: 'var(--dg-border)' }}>
@@ -137,7 +138,7 @@ export function QueryHistoryPanel({ visible, onClose, onSelect, onCreateAlert }:
onClick={() => onSelect(item)}
className="h-6 w-6 flex items-center justify-center rounded transition-colors"
style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-green)' }}
title="执行查询"
title={i18n.t('monitor.queryhistorypan.s4fb36a61')}
>
<Play className="h-3 w-3" />
</button>
@@ -1,38 +1,39 @@
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import i18n from '@/i18n';
const ALL_TEMPLATES: Record<'loki' | 'elasticsearch', { label: string; query: string }[]> = {
loki: [
{ label: 'Nginx 错误', query: '{job="nginx"} |= "error"' },
{ label: 'JSON 过滤', query: '{namespace="prod"} | json | level="warn"' },
{ label: '超时搜索', query: '{app="api"} |= "timeout"' },
{ label: '级别统计', query: 'sum by(level) (count_over_time({job="app"}[5m]))' },
{ label: '正则多级别', query: '{container="frontend"} |~ "ERROR|WARN|FATAL"' },
{ label: '慢请求>1s', query: '{job="api"} | json | response_time > 1000' },
{ label: '错误率统计', query: 'sum by(level) (count_over_time({app="cex-admin-api"} |= "error" [5m]))' },
{ label: '追踪ID查询', query: '{app="cex-admin-api"} |= "trace_id" | json' },
{ label: '服务聚合', query: 'sum by(service_name) (count_over_time({namespace="astralx"}[5m]))' },
{ label: '高延迟筛选', query: '{app="cex-admin-api"} | json | response_time > 500' },
{ label: '异常堆栈', query: '{app="cex-admin-api"} |= "Exception"' },
{ label: '日志趋势', query: 'sum(count_over_time({app="cex-admin-api"}[1h]))' },
{ label: '正则提取', query: '{app="cex-admin-api"} | regexp "(?P<method>[A-Za-z0-9_]+) (?P<path>[^ ]+)"' },
{ label: '格式输出', query: '{app="cex-admin-api"} | json | line_format "{{.level}} {{.message}}"' },
{ label: i18n.t('monitor.quickquerybarts.s64559524'), query: '{job="nginx"} |= "error"' },
{ label: i18n.t('monitor.quickquerybarts.s315990ac'), query: '{namespace="prod"} | json | level="warn"' },
{ label: i18n.t('monitor.quickquerybarts.sfc6d4276'), query: '{app="api"} |= "timeout"' },
{ label: i18n.t('monitor.quickquerybarts.s0ce3d219'), query: 'sum by(level) (count_over_time({job="app"}[5m]))' },
{ label: i18n.t('monitor.quickquerybarts.s6209b154'), query: '{container="frontend"} |~ "ERROR|WARN|FATAL"' },
{ label: i18n.t('monitor.quickquerybarts.sf839efd6'), query: '{job="api"} | json | response_time > 1000' },
{ label: i18n.t('monitor.quickquerybarts.s386c7580'), query: 'sum by(level) (count_over_time({app="cex-admin-api"} |= "error" [5m]))' },
{ label: i18n.t('monitor.quickquerybarts.sf94f3fdb'), query: '{app="cex-admin-api"} |= "trace_id" | json' },
{ label: i18n.t('monitor.quickquerybarts.s7f0fa5fa'), query: 'sum by(service_name) (count_over_time({namespace="astralx"}[5m]))' },
{ label: i18n.t('monitor.quickquerybarts.s49c7620d'), query: '{app="cex-admin-api"} | json | response_time > 500' },
{ label: i18n.t('monitor.quickquerybarts.sb8edf48d'), query: '{app="cex-admin-api"} |= "Exception"' },
{ label: i18n.t('monitor.logTrend'), query: 'sum(count_over_time({app="cex-admin-api"}[1h]))' },
{ label: i18n.t('monitor.quickquerybarts.s1ace0c03'), query: '{app="cex-admin-api"} | regexp "(?P<method>[A-Za-z0-9_]+) (?P<path>[^ ]+)"' },
{ label: i18n.t('monitor.quickquerybarts.s20b34227'), query: '{app="cex-admin-api"} | json | line_format "{{.level}} {{.message}}"' },
],
elasticsearch: [
{ label: 'API 5xx', query: 'status:500 AND path:"/api/*"' },
{ label: '错误超时', query: '(level:error OR log.level:error) AND message:*timeout*' },
{ label: '排除调试', query: 'host.name:server-* AND NOT level:debug' },
{ label: '慢请求', query: 'response_time:>1000 AND method:POST' },
{ label: '严重错误', query: '@timestamp:[now-1h TO now] AND (level:critical OR log.level:critical)' },
{ label: '服务错误', query: 'service:nginx AND (level:(error OR fatal) OR log.level:(error OR fatal))' },
{ label: '组合查询', query: 'app:cex-admin-api AND (level:(error OR warn) OR log.level:(error OR warn))' },
{ label: '模糊匹配', query: 'message:*timeout* AND app:cex-admin-api' },
{ label: '范围查询', query: 'response_time:[1000 TO *] AND method:POST' },
{ label: '排除路径', query: 'app:cex-admin-api AND NOT path:/health' },
{ label: '时间过滤', query: '@timestamp:[now-1h TO now] AND level:error' },
{ label: 'IP段过滤', query: 'client_ip:172.36.* AND app:cex-admin-api' },
{ label: '多级别筛选', query: 'app:cex-admin-api AND (level:(error OR warn OR info) OR log.level:(error OR warn OR info))' },
{ label: '嵌套字段', query: 'labels.orgId:10001 AND app:cex-admin-api' },
{ label: i18n.t('monitor.quickquerybarts.sa1eb8196'), query: '(level:error OR log.level:error) AND message:*timeout*' },
{ label: i18n.t('monitor.quickquerybarts.s0235c6eb'), query: 'host.name:server-* AND NOT level:debug' },
{ label: i18n.t('monitor.quickquerybarts.s0876c43d'), query: 'response_time:>1000 AND method:POST' },
{ label: i18n.t('monitor.quickquerybarts.se31850bf'), query: '@timestamp:[now-1h TO now] AND (level:critical OR log.level:critical)' },
{ label: i18n.t('monitor.quickquerybarts.s6241b884'), query: 'service:nginx AND (level:(error OR fatal) OR log.level:(error OR fatal))' },
{ label: i18n.t('monitor.quickquerybarts.s04adb0fd'), query: 'app:cex-admin-api AND (level:(error OR warn) OR log.level:(error OR warn))' },
{ label: i18n.t('monitor.quickquerybarts.sc579cfbb'), query: 'message:*timeout* AND app:cex-admin-api' },
{ label: i18n.t('monitor.quickquerybarts.s0c9d7886'), query: 'response_time:[1000 TO *] AND method:POST' },
{ label: i18n.t('monitor.quickquerybarts.se11dceb4'), query: 'app:cex-admin-api AND NOT path:/health' },
{ label: i18n.t('monitor.quickquerybarts.s00f6f739'), query: '@timestamp:[now-1h TO now] AND level:error' },
{ label: i18n.t('monitor.quickquerybarts.s8a1879ae'), query: 'client_ip:172.36.* AND app:cex-admin-api' },
{ label: i18n.t('monitor.quickquerybarts.s6f401985'), query: 'app:cex-admin-api AND (level:(error OR warn OR info) OR log.level:(error OR warn OR info))' },
{ label: i18n.t('monitor.quickquerybarts.sf09e007f'), query: 'labels.orgId:10001 AND app:cex-admin-api' },
],
};
@@ -5,6 +5,7 @@ import { alertApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import type { LogEngine, TimeRange } from './types-v3';
import { ENGINE_META, savedQueryStorage } from './types-v3';
import i18n from '@/i18n';
interface SaveQueryProps {
open: boolean;
@@ -46,24 +47,24 @@ export function SaveQueryDialog({ open, onClose, engine, query, timeRange, datas
<div className="fixed inset-0 z-50" style={{ background: 'rgba(0,0,0,0.5)' }} onClick={onClose} />
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-lg border shadow-xl overflow-hidden" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: 'var(--dg-border)' }}>
<div className="flex items-center gap-2"><Save className="h-4 w-4" style={{ color: 'var(--dg-blue)' }} /><span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>另存为查询</span></div>
<div className="flex items-center gap-2"><Save className="h-4 w-4" style={{ color: 'var(--dg-blue)' }} /><span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}> {i18n.t('monitor.statChart.saveAsQuery')} </span></div>
<button onClick={onClose} className="h-7 w-7 flex items-center justify-center" style={{ color: 'var(--dg-text-muted)' }}><X className="h-4 w-4" /></button>
</div>
<div className="p-4 space-y-3">
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>查询名称 *</label>
<input value={name} onChange={e => setName(e.target.value)} placeholder="输入查询名称" className="w-full h-9 px-3 text-xs rounded" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }} />
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.savequerydialog.s54c06867')} </label>
<input value={name} onChange={e => setName(e.target.value)} placeholder={i18n.t('monitor.savequerydialog.s30168e0e')} className="w-full h-9 px-3 text-xs rounded" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }} />
</div>
<div className="p-2 rounded text-[11px]" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-secondary)' }}>
<span style={{ color: ENGINE_META[engine].color }}>{ENGINE_META[engine].name}</span> · {datasourceName || '默认'}<br />
<span style={{ color: ENGINE_META[engine].color }}>{ENGINE_META[engine].name}</span> · {datasourceName || i18n.t('k8s.storage.default')}<br />
<span className="font-mono">{query}</span><br />
<span style={{ color: 'var(--dg-text-muted)' }}>{timeRange.label} · {timeRange.start} ~ {timeRange.end}</span>
</div>
</div>
<div className="flex items-center justify-end gap-2 px-4 py-3 border-t" style={{ borderColor: 'var(--dg-border)' }}>
<button onClick={onClose} className="h-8 px-3 rounded text-xs" style={{ color: 'var(--dg-text-secondary)', border: '1px solid var(--dg-border)', background: 'var(--dg-bg-tertiary)' }}>取消</button>
<button onClick={onClose} className="h-8 px-3 rounded text-xs" style={{ color: 'var(--dg-text-secondary)', border: '1px solid var(--dg-border)', background: 'var(--dg-bg-tertiary)' }}> {i18n.t('rbac.jobRole.cancel')} </button>
<button onClick={handleSave} disabled={saving || !name.trim() || saved} className="h-8 px-4 rounded text-xs font-medium text-white flex items-center gap-1" style={{ background: 'var(--dg-blue)' }}>
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}{saved ? '已保存' : '保存'}
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}{saved ? i18n.t('deploy.toast.saved') : i18n.t('task.script.save')}
</button>
</div>
</div>
@@ -100,7 +101,7 @@ export function SaveAlertDialog({ open, onClose, engine, query, datasourceId, da
try {
await alertApi.createRule({
name: name.trim(),
description: `从查询页创建: ${datasourceName || '默认数据源'}`,
description: `从查询页创建: ${datasourceName || i18n.t('monitor.savequerydialog.sddb4ca92')}`,
type: 'log',
source: engine === 'sls' ? 'loki' : engine,
expression: query.trim(),
@@ -110,11 +111,11 @@ export function SaveAlertDialog({ open, onClose, engine, query, datasourceId, da
notify_channels: [],
});
setSaved(true);
toast({ title: '告警规则已创建' });
toast({ title: i18n.t('monitor.savequerydialog.sd3dbea92') });
onSave?.();
setTimeout(() => { setSaved(false); onClose(); }, 800);
} catch (e: any) {
toast({ title: '创建失败', description: e?.message || String(e), variant: 'destructive' });
toast({ title: i18n.t('monitor.dashboard.custom.toastCreateFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally {
setSaving(false);
}
@@ -125,35 +126,35 @@ export function SaveAlertDialog({ open, onClose, engine, query, datasourceId, da
<div className="fixed inset-0 z-50" style={{ background: 'rgba(0,0,0,0.5)' }} onClick={onClose} />
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-lg border shadow-xl overflow-hidden" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: 'var(--dg-border)' }}>
<div className="flex items-center gap-2"><BellRing className="h-4 w-4" style={{ color: '#f59e0b' }} /><span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>另存为告警</span></div>
<div className="flex items-center gap-2"><BellRing className="h-4 w-4" style={{ color: '#f59e0b' }} /><span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}> {i18n.t('monitor.logSaveAsAlert')} </span></div>
<button onClick={onClose} className="h-7 w-7 flex items-center justify-center" style={{ color: 'var(--dg-text-muted)' }}><X className="h-4 w-4" /></button>
</div>
<div className="p-4 space-y-3">
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>告警名称 *</label>
<input value={name} onChange={e => setName(e.target.value)} placeholder="输入告警规则名称" className="w-full h-9 px-3 text-xs rounded" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }} />
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.savequerydialog.s78e82a11')} </label>
<input value={name} onChange={e => setName(e.target.value)} placeholder={i18n.t('monitor.savequerydialog.sa3a8d765')} className="w-full h-9 px-3 text-xs rounded" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }} />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>级别</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('kubernetes.event.eventLevel')} </label>
<select value={severity} onChange={e => setSeverity(e.target.value)} className="w-full h-9 px-2 text-xs rounded" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}>
<option value="info">提示</option><option value="warning">警告</option><option value="critical">严重</option>
<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>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>触发条件</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.alertrulestabts.s65939ccd')} </label>
<input value={condition} onChange={e => setCondition(e.target.value)} className="w-full h-9 px-2 text-xs rounded" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }} />
</div>
</div>
<div className="p-2 rounded text-[11px]" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-secondary)' }}>
<span style={{ color: meta.color }}>{meta.name}</span> · {datasourceName || '默认'}<br />
<span style={{ color: meta.color }}>{meta.name}</span> · {datasourceName || i18n.t('k8s.storage.default')}<br />
<span className="font-mono">{query}</span>
</div>
</div>
<div className="flex items-center justify-end gap-2 px-4 py-3 border-t" style={{ borderColor: 'var(--dg-border)' }}>
<button onClick={onClose} className="h-8 px-3 rounded text-xs" style={{ color: 'var(--dg-text-secondary)', border: '1px solid var(--dg-border)', background: 'var(--dg-bg-tertiary)' }}>取消</button>
<button onClick={onClose} className="h-8 px-3 rounded text-xs" style={{ color: 'var(--dg-text-secondary)', border: '1px solid var(--dg-border)', background: 'var(--dg-bg-tertiary)' }}> {i18n.t('rbac.jobRole.cancel')} </button>
<button onClick={handleSave} disabled={saving || !name.trim() || saved} className="h-8 px-4 rounded text-xs font-medium text-white flex items-center gap-1" style={{ background: '#f59e0b' }}>
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}{saved ? '已创建' : '创建告警'}
{saving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}{saved ? i18n.t('kubernetes.event.created') : i18n.t('monitor.savequerydialog.sbc1d9937')}
</button>
</div>
</div>
@@ -3,6 +3,7 @@ import { X, Trash2, Play, Bookmark, Clock } from 'lucide-react';
import type { SavedQuery } from './types-v3';
import { savedQueryStorage } from './types-v3';
import { ENGINE_META } from './types-v3';
import i18n from '@/i18n';
interface Props {
visible: boolean;
@@ -30,7 +31,7 @@ export function SavedQueriesPanel({ visible, onClose, onSelect }: Props) {
<div className="flex items-center justify-between px-4 py-3 border-b" style={{ borderColor: 'var(--dg-border)' }}>
<div className="flex items-center gap-2">
<Bookmark className="h-4 w-4" style={{ color: 'var(--dg-blue)' }} />
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>已保存查询</span>
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}> {i18n.t('monitor.logPanel.savedQueries')} </span>
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-muted)' }}>
{items.length}
</span>
@@ -41,7 +42,7 @@ export function SavedQueriesPanel({ visible, onClose, onSelect }: Props) {
</div>
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{items.length === 0 ? (
<p className="text-xs text-center py-8" style={{ color: 'var(--dg-text-muted)' }}>暂无保存的查询</p>
<p className="text-xs text-center py-8" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('monitor.savedqueriespan.s9c8dc62b')} </p>
) : items.map((item) => (
<div
key={item.id}
@@ -65,7 +66,7 @@ export function SavedQueriesPanel({ visible, onClose, onSelect }: Props) {
onClick={() => { onSelect(item); onClose(); }}
className="h-7 w-7 flex items-center justify-center rounded border"
style={{ borderColor: 'var(--dg-border)', color: '#22c55e' }}
title="应用"
title={i18n.t('kubernetes.yaml.apply')}
>
<Play className="h-3.5 w-3.5" />
</button>
@@ -73,7 +74,7 @@ export function SavedQueriesPanel({ visible, onClose, onSelect }: Props) {
onClick={() => { savedQueryStorage.remove(item.id); setItems(savedQueryStorage.getAll()); }}
className="h-7 w-7 flex items-center justify-center rounded border"
style={{ borderColor: 'var(--dg-border)', color: 'var(--dg-text-muted)' }}
title="删除"
title={i18n.t('kubernetes.storage.delete2')}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
@@ -2,30 +2,31 @@
import { useState, useRef, useEffect } from 'react';
import { ChevronDown, ChevronUp, Calendar } from 'lucide-react';
import type { TimeRange } from './types-v3';
import i18n from '@/i18n';
// 相对时间预设:最近N... 每次点击时计算 start = now - N // Logan.Li
const RELATIVE_PRESETS = [
{ label: '最近1分钟', value: '1m' },
{ label: '最近5分钟', value: '5m' },
{ label: '最近15分钟', value: '15m' },
{ label: '最近1小时', value: '1h' },
{ label: '最近4小时', value: '4h' },
{ label: '最近1天', value: '24h' },
{ label: '最近1周', value: '7d' },
{ label: '最近30天', value: '30d' },
{ label: i18n.t('monitor.timerangepicker.s6b3f2ecb'), value: '1m' },
{ label: i18n.t('monitor.logPanel.last5min'), value: '5m' },
{ label: i18n.t('monitor.timerangepicker.sd4962f4b'), value: '15m' },
{ label: i18n.t('monitor.timerangepicker.sd8a81545'), value: '1h' },
{ label: i18n.t('monitor.timerangepicker.s0823ad83'), value: '4h' },
{ label: i18n.t('monitor.timerangepicker.s77be0dba'), value: '24h' },
{ label: i18n.t('monitor.timerangepicker.s2ba41ef4'), value: '7d' },
{ label: i18n.t('monitor.timerangepicker.s63f66f1f'), value: '30d' },
];
// 绝对时间预设:固定的自然日/周/月/季/年 // Logan.Li
const ABSOLUTE_PRESETS = [
{ label: '今天', value: 'today' },
{ label: '昨天', value: 'yesterday' },
{ label: '前天', value: 'day_before_yesterday' },
{ label: '本周', value: 'this_week' },
{ label: '上周', value: 'last_week' },
{ label: '本月', value: 'this_month' },
{ label: '上月', value: 'last_month' },
{ label: '本季度', value: 'this_quarter' },
{ label: '本年度', value: 'this_year' },
{ label: i18n.t('audit.terminal.presetToday'), value: 'today' },
{ label: i18n.t('common.yesterday'), value: 'yesterday' },
{ label: i18n.t('monitor.timerangepicker.s94995c31'), value: 'day_before_yesterday' },
{ label: i18n.t('common.thisWeek'), value: 'this_week' },
{ label: i18n.t('common.lastWeek'), value: 'last_week' },
{ label: i18n.t('cloud.thisMonth'), value: 'this_month' },
{ label: i18n.t('common.lastMonth'), value: 'last_month' },
{ label: i18n.t('monitor.timerangepicker.s0477ce74'), value: 'this_quarter' },
{ label: i18n.t('monitor.timerangepicker.s77df6c8e'), value: 'this_year' },
];
function getPresetRange(value: string): { start: Date; end: Date; label: string } {
@@ -94,7 +95,7 @@ export function TimeRangePicker({ value, onChange }: Props) {
onChange({
start: customStart,
end: customEnd,
label: '自定义',
label: i18n.t('aiops.providerCustom'),
type: 'absolute',
});
setActivePreset('custom');
@@ -135,7 +136,7 @@ export function TimeRangePicker({ value, onChange }: Props) {
{/* 头部 */}
<div className="flex items-center gap-2 mb-3 pb-3" style={{ borderBottom: '1px solid var(--dg-border)' }}>
<Calendar className="h-4 w-4" style={{ color: 'var(--dg-blue)' }} />
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>时间选择</span>
<span className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}> {i18n.t('monitor.timerangepicker.sc2c8fb23')} </span>
<button
onClick={openCustom}
className="text-xs ml-2 px-2 py-0.5 rounded transition-colors"
@@ -151,7 +152,7 @@ export function TimeRangePicker({ value, onChange }: Props) {
{mode === 'preset' ? (
<>
{/* 相对时间 — 最近... */}
<p className="text-[10px] mb-2 font-medium uppercase tracking-wide" style={{ color: '#22c55e' }}>相对时间(滚动窗口)</p>
<p className="text-[10px] mb-2 font-medium uppercase tracking-wide" style={{ color: '#22c55e' }}> {i18n.t('monitor.timerangepicker.s8ead0af2')} </p>
<div className="grid grid-cols-4 gap-1.5 mb-3">
{RELATIVE_PRESETS.map(p => (
<button
@@ -170,7 +171,7 @@ export function TimeRangePicker({ value, onChange }: Props) {
</div>
{/* 绝对时间 — 固定日历范围 */}
<p className="text-[10px] mb-2 font-medium uppercase tracking-wide" style={{ color: 'var(--dg-blue)' }}>绝对时间(日历范围)</p>
<p className="text-[10px] mb-2 font-medium uppercase tracking-wide" style={{ color: 'var(--dg-blue)' }}> {i18n.t('monitor.timerangepicker.s714f6cdd')} </p>
<div className="grid grid-cols-5 gap-1.5">
{ABSOLUTE_PRESETS.map(p => (
<button
@@ -191,10 +192,10 @@ export function TimeRangePicker({ value, onChange }: Props) {
) : (
/* 自定义日历模式 */
<div className="space-y-3">
<p className="text-[10px] font-medium uppercase" style={{ color: 'var(--dg-blue)' }}>自定义时间范围</p>
<p className="text-[10px] font-medium uppercase" style={{ color: 'var(--dg-blue)' }}> {i18n.t('monitor.hosttimerangepi.s0122e022')} </p>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>开始时间</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('system.terminalStartTime')} </label>
<input
type="datetime-local"
value={customStart.substring(0, 16)}
@@ -204,7 +205,7 @@ export function TimeRangePicker({ value, onChange }: Props) {
/>
</div>
<div>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}>结束时间</label>
<label className="text-[10px] block mb-1" style={{ color: 'var(--dg-text-muted)' }}> {i18n.t('system.terminalEndTime')} </label>
<input
type="datetime-local"
value={customEnd.substring(0, 16)}
@@ -215,8 +216,8 @@ export function TimeRangePicker({ value, onChange }: Props) {
</div>
</div>
<div className="flex justify-end gap-2 pt-2">
<button onClick={() => setMode('preset')} className="h-8 px-3 rounded text-xs" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-secondary)', background: 'var(--dg-bg-tertiary)' }}>取消</button>
<button onClick={handleCustomApply} className="h-8 px-4 rounded text-xs font-medium text-white" style={{ background: 'var(--dg-blue)' }}>应用</button>
<button onClick={() => setMode('preset')} className="h-8 px-3 rounded text-xs" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-secondary)', background: 'var(--dg-bg-tertiary)' }}> {i18n.t('rbac.jobRole.cancel')} </button>
<button onClick={handleCustomApply} className="h-8 px-4 rounded text-xs font-medium text-white" style={{ background: 'var(--dg-blue)' }}> {i18n.t('kubernetes.yaml.apply')} </button>
</div>
</div>
)}
@@ -1,6 +1,7 @@
// 日志查询 URL 状态编解码 — 可分享链接 // Logan.Li
import type { TimeRange } from './types-v3';
import i18n from '@/i18n';
export interface LogQueryUrlState {
query?: string;
@@ -16,7 +17,7 @@ export function parseLogQueryUrl(params: URLSearchParams): LogQueryUrlState {
const index = params.get('index') || undefined;
const start = params.get('from') || undefined;
const end = params.get('to') || undefined;
const timeLabel = params.get('tr') || '自定义';
const timeLabel = params.get('tr') || i18n.t('aiops.providerCustom');
const autoRun = params.get('run') === '1';
const state: LogQueryUrlState = { query, datasourceId, index, autoRun };
if (start && end) {
@@ -1,3 +1,4 @@
import i18n from '@/i18n';
// types-v3.ts — v3 统一日志中心类型定义 // Logan.Li
export type LogEngine = 'loki' | 'elasticsearch' | 'sls';
@@ -132,7 +133,7 @@ export const ENGINE_META: Record<LogEngine, { name: string; color: string; query
color: '#f97316',
queryPlaceholder: 'keyword | SELECT * WHERE level = "info"',
statPlaceholder: '* | SELECT level, COUNT(*) as cnt GROUP BY level',
externalLink: 'SLS控制台',
externalLink: i18n.t('monitor.typesv3ts.sfe4ff0b4'),
},
};
@@ -248,27 +249,27 @@ export const savedQueryStorage = {
// 快捷查询模板 // Logan.Li
export const QUICK_TEMPLATES: Record<LogEngine, { label: string; query: string }[]> = {
loki: [
{ label: 'Nginx 错误日志', query: '{job="nginx"} |= "error"' },
{ label: 'JSON 解析过滤', query: '{namespace="prod"} | json | level="warn"' },
{ label: '超时搜索', query: '{app="api"} |= "timeout"' },
{ label: '按级别统计', query: 'sum by (level) (count_over_time({job="app"}[5m]))' },
{ label: '正则匹配多级别', query: '{container="frontend"} |~ "ERROR|WARN|FATAL"' },
{ label: '慢请求 >1s', query: '{job="api"} | json | response_time > 1000' },
{ label: i18n.t('monitor.typesv3ts.s70d6c24a'), query: '{job="nginx"} |= "error"' },
{ label: i18n.t('monitor.typesv3ts.saf1cb054'), query: '{namespace="prod"} | json | level="warn"' },
{ label: i18n.t('monitor.quickquerybarts.sfc6d4276'), query: '{app="api"} |= "timeout"' },
{ label: i18n.t('monitor.typesv3ts.s475a42fe'), query: 'sum by (level) (count_over_time({job="app"}[5m]))' },
{ label: i18n.t('monitor.typesv3ts.sac8a5c83'), query: '{container="frontend"} |~ "ERROR|WARN|FATAL"' },
{ label: i18n.t('monitor.typesv3ts.s4dc793a9'), query: '{job="api"} | json | response_time > 1000' },
],
elasticsearch: [
{ label: 'API 5xx 错误', query: 'status:500 AND path:"/api/*"' },
{ label: '错误超时日志', query: '(level:error OR log.level:error) AND message:*timeout*' },
{ label: '排除调试日志', query: 'host.name:server-* AND NOT level:debug AND NOT log.level:debug' },
{ label: '慢请求检测', query: 'response_time:>1000 AND method:POST' },
{ label: '最近1小时严重错误', query: '@timestamp:[now-1h TO now] AND (level:critical OR log.level:critical)' },
{ label: '特定服务错误', query: 'service:nginx AND (level:(error OR fatal) OR log.level:(error OR fatal))' },
{ label: i18n.t('monitor.typesv3ts.sd81428f4'), query: 'status:500 AND path:"/api/*"' },
{ label: i18n.t('monitor.typesv3ts.s6624c787'), query: '(level:error OR log.level:error) AND message:*timeout*' },
{ label: i18n.t('monitor.typesv3ts.s98b327ac'), query: 'host.name:server-* AND NOT level:debug AND NOT log.level:debug' },
{ label: i18n.t('monitor.typesv3ts.s95d72fa1'), query: 'response_time:>1000 AND method:POST' },
{ label: i18n.t('monitor.typesv3ts.s9a0d4c8c'), query: '@timestamp:[now-1h TO now] AND (level:critical OR log.level:critical)' },
{ label: i18n.t('monitor.typesv3ts.s68f58cc0'), query: 'service:nginx AND (level:(error OR fatal) OR log.level:(error OR fatal))' },
],
sls: [
{ label: '错误日志筛选', query: "* | select * where level = 'error'" },
{ label: '按级别统计', query: "* | select count(*) as cnt, level group by level" },
{ label: '时间聚合', query: "* | select date_trunc('minute', __time__) as t, count(*) group by t order by t" },
{ label: 'Top 方法统计', query: "* | select method, count(*) as cnt, avg(response_time) group by method order by cnt desc" },
{ label: '慢请求分析', query: "* | select * where response_time > 1000 order by response_time desc limit 100" },
{ label: '状态码分布', query: "* | select status_code, count(*) as cnt, count(*) * 100.0 / sum(count(*)) over() as pct group by status_code" },
{ label: i18n.t('monitor.typesv3ts.sf8889aa4'), query: "* | select * where level = 'error'" },
{ label: i18n.t('monitor.typesv3ts.s475a42fe'), query: "* | select count(*) as cnt, level group by level" },
{ label: i18n.t('monitor.typesv3ts.s27ee0975'), query: "* | select date_trunc('minute', __time__) as t, count(*) group by t order by t" },
{ label: i18n.t('monitor.typesv3ts.sa5ad7487'), query: "* | select method, count(*) as cnt, avg(response_time) group by method order by cnt desc" },
{ label: i18n.t('monitor.typesv3ts.s80428bd5'), query: "* | select * where response_time > 1000 order by response_time desc limit 100" },
{ label: i18n.t('monitor.typesv3ts.s63c5c72e'), query: "* | select status_code, count(*) as cnt, count(*) * 100.0 / sum(count(*)) over() as pct group by status_code" },
],
};