feat(i18n): 增量国际化迁移 — deploy/kubernetes/monitor/task 全模块 + 补全 28 缺失翻译键

迁移范围:
- deploy 模块 (PipelineEditor/PipelineDetailDialog/PipelineFormDialog/PipelineRunDialog/ReleaseCreateDialog/ReleaseDetailDialog/ReleaseManager/ToolConfigTab/JobRoleCreateDialog 等 14 文件)
- kubernetes 模块 (YamlEditorToolbar/DeploymentHpaTab/DeploymentLogsTab/DeploymentPodsTab/DnsHostSection/SchedulingSection/EventsTab/HPACronHPATab/HistoryTab/PodsTab/ServicesTab/ActionModals 等 12 文件)
- monitor 模块 (AlertEventsTab/AlertCenterDetail/DatasourcePage/LogAlertBell/LogCenter/LogCenterArchBanner/StatChartPanel/LogRoleCreateDialog 等 8 文件)
- task 模块 (CronJobStatsPanel/CronJobTable/CronJobViewDialog/ExecutionHistoryDialog 4 文件)
- layout (PageEmpty/PageLoading 2 文件)
- site-monitor/snmp-monitor 模块 (6 文件)

翻译键: 3975 → 4003 (+28 缺失键补全), 22 命名空间, zh-CN/en-US 完全对齐
新增命名空间: agent/aiops/cloud/kubernetes/monitor/pipeline/rbac/system/task/ui

验证: go build✅ tsc -b=0 npm build✅ (20.78s) 17/17 集成测试通过
This commit is contained in:
attacker committed 2026-07-02 12:39:36 +08:00
1 parent eddf01dd79
commit 63e4c220d4
59 files changed
+6713 -641

No files matched your search

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -25,6 +26,7 @@ export function AlertEventsTab({
loading: boolean;
onReload: () => void;
}) {
const { t } = useTranslation();
const _confirm = useConfirm();
const [searchQuery, setSearchQuery] = useState('');
const [severityFilter, setSeverityFilter] = useState<string>('all');
@@ -45,21 +47,21 @@ export function AlertEventsTab({
return (
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10">
<AlertTriangle className="h-3 w-3 mr-1" />
严重
{t('monitor.alertEvents.severity.critical')}
</Badge>
);
case 'warning':
return (
<Badge variant="outline" className="border-amber-500/30 dg-text-yellow bg-amber-500/10">
<AlertTriangle className="h-3 w-3 mr-1" />
警告
{t('monitor.alertEvents.severity.warning')}
</Badge>
);
case 'info':
return (
<Badge variant="outline" className="border-blue-500/30 dg-text-blue bg-blue-500/10">
<Bell className="h-3 w-3 mr-1" />
提示
{t('monitor.alertEvents.severity.info')}
</Badge>
);
default:
@@ -72,13 +74,13 @@ export function AlertEventsTab({
case 'firing':
return (
<Badge variant="outline" className="border-red-500/30 dg-text-red bg-red-500/10 animate-pulse">
触发中
{t('monitor.alertEvents.status.firing')}
</Badge>
);
case 'resolved':
return (
<Badge variant="outline" className="border-emerald-500/30 dg-text-green bg-emerald-500/10">
已恢复
{t('monitor.alertEvents.status.resolved')}
</Badge>
);
default:
@@ -102,7 +104,7 @@ export function AlertEventsTab({
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 dg-text-muted" />
<Input
placeholder="搜索告警名称或目标..."
placeholder={t('monitor.alertEvents.searchPlaceholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 dg-input"
@@ -113,19 +115,19 @@ export function AlertEventsTab({
onChange={(e) => setSeverityFilter(e.target.value)}
className="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="all">全部级别</option>
<option value="critical">严重</option>
<option value="warning">警告</option>
<option value="info">提示</option>
<option value="all">{t('monitor.alertEvents.filter.allSeverity')}</option>
<option value="critical">{t('monitor.alertEvents.severity.critical')}</option>
<option value="warning">{t('monitor.alertEvents.severity.warning')}</option>
<option value="info">{t('monitor.alertEvents.severity.info')}</option>
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="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="all">全部状态</option>
<option value="firing">触发中</option>
<option value="resolved">已恢复</option>
<option value="all">{t('monitor.alertEvents.filter.allStatus')}</option>
<option value="firing">{t('monitor.alertEvents.status.firing')}</option>
<option value="resolved">{t('monitor.alertEvents.status.resolved')}</option>
</select>
</div>
@@ -136,14 +138,14 @@ export function AlertEventsTab({
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<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">{t('monitor.alertEvents.col.severity')}</TableHead>
<TableHead className="dg-text-secondary">{t('monitor.alertEvents.col.alertName')}</TableHead>
<TableHead className="dg-text-secondary">{t('monitor.alertEvents.col.status')}</TableHead>
<TableHead className="dg-text-secondary">{t('monitor.alertEvents.col.target')}</TableHead>
<TableHead className="dg-text-secondary">{t('monitor.alertEvents.col.currentValue')}</TableHead>
<TableHead className="dg-text-secondary">{t('monitor.alertEvents.col.threshold')}</TableHead>
<TableHead className="dg-text-secondary">{t('monitor.alertEvents.col.startTime')}</TableHead>
<TableHead className="dg-text-secondary text-right">{t('monitor.alertEvents.col.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -151,7 +153,7 @@ export function AlertEventsTab({
<TableRow className="border-[var(--dg-border)]">
<TableCell colSpan={8} className="text-center dg-text-muted py-8">
<Loader2 className="h-5 w-5 animate-spin inline mr-2" />
加载中...
{t('monitor.alertEvents.loading')}
</TableCell>
</TableRow>
) : filteredEvents.map((event) => (
@@ -187,7 +189,7 @@ export function AlertEventsTab({
onClick={() => handleAcknowledge(event.id)}
>
<CheckCircle className="h-4 w-4 mr-1" />
确认
{t('monitor.alertEvents.acknowledge')}
</Button>
)}
</div>
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Check, ChevronRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -18,6 +19,7 @@ export function AlertDetailDialog({
onOpenChange: (open: boolean) => void;
onAcknowledge: (id: string) => void;
}) {
const { t } = useTranslation();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-3xl">
@@ -26,44 +28,44 @@ export function AlertDetailDialog({
{alert?.title}
{alert && getSeverityBadge(alert.severity)}
</DialogTitle>
<DialogDescription className="sr-only">查看告警详情</DialogDescription>
<DialogDescription className="sr-only">{t('monitor.alertDetail.viewDetail')}</DialogDescription>
</DialogHeader>
{alert && (
<div className="space-y-4">
<div>
<Label className="text-muted-foreground">描述</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.description')}</Label>
<p className="mt-1">{alert.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-muted-foreground">来源</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.source')}</Label>
<p className="font-medium">{alert.source}</p>
</div>
<div>
<Label className="text-muted-foreground">服务</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.service')}</Label>
<p className="font-medium">{alert.service}</p>
</div>
<div>
<Label className="text-muted-foreground">指标</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.metric')}</Label>
<p className="font-medium">{alert.metric}</p>
</div>
<div>
<Label className="text-muted-foreground">当前值 / 阈值</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.currentValueThreshold')}</Label>
<p className="font-medium">{alert.value} / {alert.threshold}</p>
</div>
<div>
<Label className="text-muted-foreground">开始时间</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.startTime')}</Label>
<p className="font-medium">{alert.startedAt}</p>
</div>
{alert.resolvedAt && (
<div>
<Label className="text-muted-foreground">解决时间</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.resolvedTime')}</Label>
<p className="font-medium">{alert.resolvedAt}</p>
</div>
)}
</div>
<div>
<Label className="text-muted-foreground">标签</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.labels')}</Label>
<div className="flex flex-wrap gap-2 mt-1">
{Object.entries(alert.labels).map(([k, v]) => (
<Badge key={k} variant="outline">{k}={v}</Badge>
@@ -72,20 +74,20 @@ export function AlertDetailDialog({
</div>
{alert.runbookUrl && (
<div>
<Label className="text-muted-foreground">处理手册</Label>
<Label className="text-muted-foreground">{t('monitor.alertDetail.runbook')}</Label>
<a href={alert.runbookUrl} target="_blank" rel="noopener noreferrer" className="dg-text-blue hover:underline block mt-1">
查看处理手册 →
{t('monitor.alertDetail.viewRunbook')} →
</a>
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>关闭</Button>
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('common.close')}</Button>
{alert?.status === 'firing' && (
<Button onClick={() => { onAcknowledge(alert.id); onOpenChange(false); }}>
<Check className="w-4 h-4 mr-2" />
确认告警
{t('monitor.acknowledge')}
</Button>
)}
</DialogFooter>
@@ -105,12 +107,13 @@ export function AlertSilenceDialog({
onOpenChange: (open: boolean) => void;
onSilence: (id: string, duration: string) => void;
}) {
const { t } = useTranslation();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>静默告警</DialogTitle>
<DialogDescription>选择静默持续时间</DialogDescription>
<DialogTitle>{t('monitor.silenceAlert')}</DialogTitle>
<DialogDescription>{t('monitor.alertDetail.selectSilenceDuration')}</DialogDescription>
</DialogHeader>
<div className="space-y-2">
{['1h', '4h', '8h', '24h', '7d'].map((duration) => (
@@ -120,7 +123,7 @@ export function AlertSilenceDialog({
className="w-full justify-between"
onClick={() => alert && onSilence(alert.id, duration)}
>
<span>静默 {duration}</span>
<span>{t('monitor.alertDetail.silenceDuration', { duration })}</span>
<ChevronRight className="w-4 h-4" />
</Button>
))}
@@ -1,5 +1,6 @@
// 数据源管理页面 — Prometheus + Zabbix 双 Tab // Logan.Li
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Database } from 'lucide-react';
import { PageShell } from '@/components/layout/PageShell';
import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager';
@@ -7,6 +8,7 @@ import ZabbixDatasourceManager from '@/components/monitor/datasource/ZabbixDatas
export default function DatasourcePage() {
const [activeTab, setActiveTab] = useState('prometheus');
const { t } = useTranslation();
const pageTabs = [
{ id: 'prometheus', label: 'Prometheus' },
@@ -16,8 +18,8 @@ export default function DatasourcePage() {
return (
<PageShell
variant="list"
title="数据源管理"
subtitle="管理 Prometheus 与 Zabbix 监控数据源"
title={t('monitor.datasource.manageTitle')}
subtitle={t('monitor.datasource.manageSubtitle')}
icon={<Database className="h-5 w-5" />}
tabs={pageTabs}
activeTab={activeTab}
@@ -1,4 +1,5 @@
// LogAlertBell.tsx — 小铃铛告警关联组件 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Bell } from 'lucide-react';
interface Props {
@@ -14,13 +15,14 @@ const SIZES = {
};
export function LogAlertBell({ hasAlertRule, onClick, size = 'md' }: Props) {
const { t } = useTranslation();
const s = SIZES[size];
return (
<button
onClick={onClick}
className={`relative ${s.btn} flex items-center justify-center rounded-md border transition-colors hover:opacity-80`}
style={{ borderColor: 'var(--dg-border)', background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-muted)' }}
title={hasAlertRule ? '已关联告警策略' : '关联告警策略'}
title={hasAlertRule ? t('monitor.logAlertBell.linked') : t('monitor.logAlertBell.link')}
>
<Bell style={{ width: s.icon, height: s.icon }} />
{hasAlertRule && (
@@ -1,5 +1,6 @@
// LogCenter — 日志中心 v4(侧边栏子菜单驱动:Loki查询 / ES查询 / 日志库 / 日志告警)// Logan.Li
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Navigate, useParams } from 'react-router-dom';
import { ScrollText } from 'lucide-react';
import { PageShell } from '@/components/layout/PageShell';
@@ -10,17 +11,18 @@ import { LogAlertsPanel } from './LogAlertsPanel';
import LogRBAC from './LogRBAC';
import type { LogEngine } from './types-v3';
const SECTION_META: Record<string, { title: string; description: string; engine?: LogEngine }> = {
loki: { title: 'Loki 查询', description: '对接自建 Loki · LogQL 实时检索', engine: 'loki' },
es: { title: 'ES 查询', description: '对接自建 Elasticsearch · 索引日志检索', engine: 'elasticsearch' },
datasources: { title: '日志库', description: '管理日志数据源连接(Loki / Elasticsearch)' },
alerts: { title: '日志告警', description: '基于查询条件的日志告警策略' },
rbac: { title: '日志权限', description: '按 ES 索引 / Loki Label 正则限制查询范围' },
const SECTION_KEYS: Record<string, { titleKey: string; descriptionKey: string; engine?: LogEngine }> = {
loki: { titleKey: 'monitor.logCenter.loki', descriptionKey: 'monitor.logCenter.lokiDesc', engine: 'loki' },
es: { titleKey: 'monitor.logCenter.es', descriptionKey: 'monitor.logCenter.esDesc', engine: 'elasticsearch' },
datasources: { titleKey: 'monitor.logCenter.datasources', descriptionKey: 'monitor.logCenter.datasourcesDesc' },
alerts: { titleKey: 'monitor.logCenter.alerts', descriptionKey: 'monitor.logCenter.alertsDesc' },
rbac: { titleKey: 'monitor.logCenter.rbac', descriptionKey: 'monitor.logCenter.rbacDesc' },
};
const VALID_TABS = ['loki', 'es', 'datasources', 'alerts', 'rbac'];
export default function LogCenter() {
const { t } = useTranslation();
const { section } = useParams<{ section: string }>();
const tab = section && VALID_TABS.includes(section) ? section : null;
const [alertReloadKey, setAlertReloadKey] = useState(0);
@@ -29,14 +31,14 @@ export default function LogCenter() {
if (tab === 'rbac') return <LogRBAC />;
const meta = SECTION_META[tab];
const meta = SECTION_KEYS[tab];
return (
<LogQueryProvider>
<PageShell
variant="workspace"
title={meta.title}
subtitle={meta.description}
title={t(meta.titleKey)}
subtitle={t(meta.descriptionKey)}
icon={<ScrollText className="h-5 w-5" />}
contentClassName="flex-1 min-h-0 pb-20"
>
@@ -48,4 +50,4 @@ export default function LogCenter() {
</PageShell>
</LogQueryProvider>
);
}
}
@@ -1,15 +1,17 @@
// LogCenterArchBanner — 架构说明:平台不落库海量日志 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Info } from 'lucide-react';
export function LogCenterArchBanner() {
const { t } = useTranslation();
return (
<div className="flex gap-3 rounded-lg border border-cyan-500/20 bg-cyan-500/5 px-4 py-3 text-xs text-[var(--dg-text-secondary)] leading-relaxed">
<Info className="h-4 w-4 shrink-0 dg-text-cyan mt-0.5" />
<div>
<p className="text-[var(--dg-text-primary)] font-medium mb-1">无状态查询网关</p>
<p className="text-[var(--dg-text-primary)] font-medium mb-1">{t('monitor.logCenterArch.title')}</p>
<p>
AIOps 不存储 Loki / Elasticsearch 中的海量日志,仅保存<strong className="text-[var(--dg-text-primary)] font-normal">数据源连接</strong>与
<strong className="text-[var(--dg-text-primary)] font-normal">告警策略</strong>。查询时实时转发至外部集群,结果仅在当前页面展示,刷新即清空。
{t('monitor.logCenterArch.bodyPrefix')}<strong className="text-[var(--dg-text-primary)] font-normal">{t('monitor.logCenterArch.datasourceConn')}</strong>{t('monitor.logCenterArch.connector')}
<strong className="text-[var(--dg-text-primary)] font-normal">{t('monitor.logCenterArch.alertPolicy')}</strong>{t('monitor.logCenterArch.bodySuffix')}
</p>
</div>
</div>
@@ -1,5 +1,6 @@
// StatChartPanel.tsx — 统计图表面板(SLS风格)// Logan.Li
import { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { Save, Play, AlertCircle } from 'lucide-react';
import type { StatTable, ChartType, LogEngine } from './types-v3';
@@ -13,17 +14,10 @@ interface Props {
onSaveAsQuery?: () => void;
}
const CHART_TYPE_LABELS: Record<ChartType, string> = {
table: '表格',
line: '线图',
bar: '柱状图',
pie: '饼图',
area: '面积图',
};
const PIE_COLORS = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#a855f7', '#06b6d4', '#f97316', '#84cc16'];
export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSaveAsQuery }: Props) {
const { t } = useTranslation();
const [chartType, setChartType] = useState<ChartType>('table');
const [xAxisField, setXAxisField] = useState('');
const [yAxisField, setYAxisField] = useState('');
@@ -31,6 +25,14 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
const meta = ENGINE_META[engine];
const chartTypeLabels: Record<ChartType, string> = {
table: t('monitor.statChart.chartType.table'),
line: t('monitor.statChart.chartType.line'),
bar: t('monitor.statChart.chartType.bar'),
pie: t('monitor.statChart.chartType.pie'),
area: t('monitor.statChart.chartType.area'),
};
// 自动检测字段 // Logan.Li
const columns = statTable?.columns || [];
const rows = statTable?.rows || [];
@@ -80,20 +82,20 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<AlertCircle className="h-4 w-4" style={{ color: '#f59e0b' }} />
<span className="text-xs" style={{ color: '#f59e0b' }}>当前暂无统计信息,您可以按照样例进行操作</span>
<span className="text-xs" style={{ color: '#f59e0b' }}>{t('monitor.statChart.emptyHint')}</span>
</div>
<button
onClick={onSaveAsQuery}
className="h-7 px-3 rounded text-[11px] flex items-center gap-1"
style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-secondary)', background: 'var(--dg-bg-tertiary)' }}
>
<Save className="h-3 w-3" /> 另存为查询
<Save className="h-3 w-3" /> {t('monitor.statChart.saveAsQuery')}
</button>
</div>
{/* 查询样例 */}
<div className="mb-4">
<p className="text-[10px] mb-2" style={{ color: 'var(--dg-text-muted)' }}>查询样例:</p>
<p className="text-[10px] mb-2" style={{ color: 'var(--dg-text-muted)' }}>{t('monitor.statChart.querySamples')}</p>
<div className="space-y-1">
{querySamples.map((q, i) => (
<button
@@ -111,7 +113,7 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
{sampleQuery && (
<div className="flex gap-2">
<button className="h-8 px-4 rounded text-xs font-medium text-white flex items-center gap-1" style={{ background: meta.color }}>
<Play className="h-3.5 w-3.5" /> 执行分析
<Play className="h-3.5 w-3.5" /> {t('monitor.statChart.runAnalysis')}
</button>
</div>
)}
@@ -125,21 +127,21 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
<div className="flex-1 min-w-0">
{/* 图表类型切换 */}
<div className="flex items-center gap-1 mb-3">
{(Object.keys(CHART_TYPE_LABELS) as ChartType[]).map(t => (
{(Object.keys(chartTypeLabels) as ChartType[]).map(t2 => (
<button
key={t}
onClick={() => setChartType(t)}
key={t2}
onClick={() => setChartType(t2)}
className="px-2.5 py-1 rounded text-[11px] transition-colors"
style={{
background: chartType === t ? meta.color : 'var(--dg-bg-tertiary)',
color: chartType === t ? '#fff' : 'var(--dg-text-muted)',
background: chartType === t2 ? meta.color : 'var(--dg-bg-tertiary)',
color: chartType === t2 ? '#fff' : 'var(--dg-text-muted)',
}}
>
{CHART_TYPE_LABELS[t]}
{chartTypeLabels[t2]}
</button>
))}
<button onClick={onSaveAsAlert} className="ml-auto h-7 px-2.5 rounded text-[11px] flex items-center gap-1" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}>
<Save className="h-3 w-3" /> 另存为告警
<Save className="h-3 w-3" /> {t('monitor.statChart.saveAsAlert')}
</button>
</div>
@@ -168,7 +170,7 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
</tbody>
</table>
</div>
<div className="px-3 py-2 text-[10px]" style={{ color: 'var(--dg-text-muted)' }}>总数:{rows.length}</div>
<div className="px-3 py-2 text-[10px]" style={{ color: 'var(--dg-text-muted)' }}>{t('monitor.statChart.totalCount')}{rows.length}</div>
</div>
)}
@@ -223,25 +225,25 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
{/* 右侧:通用配置面板 */}
<div className="w-[260px] flex-shrink-0 border rounded-lg p-3" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
<div className="flex border-b mb-3" style={{ borderColor: 'var(--dg-border)' }}>
<button className="pb-2 text-xs font-medium" style={{ color: 'var(--dg-blue)', borderBottom: '2px solid var(--dg-blue)' }}>通用配置</button>
<button className="pb-2 text-xs font-medium" style={{ color: 'var(--dg-blue)', borderBottom: '2px solid var(--dg-blue)' }}>{t('monitor.statChart.generalConfig')}</button>
</div>
{/* 图表类型 */}
<div className="mb-3">
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>图表类型</p>
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>{t('monitor.statChart.chartTypeLabel')}</p>
<div className="grid grid-cols-5 gap-1">
{(Object.keys(CHART_TYPE_LABELS) as ChartType[]).map(t => (
{(Object.keys(chartTypeLabels) as ChartType[]).map(t2 => (
<button
key={t}
onClick={() => setChartType(t)}
key={t2}
onClick={() => setChartType(t2)}
className="py-1 rounded text-[9px] text-center transition-colors"
style={{
background: chartType === t ? 'var(--dg-bg-tertiary)' : 'transparent',
color: chartType === t ? 'var(--dg-text-primary)' : 'var(--dg-text-muted)',
border: chartType === t ? '1px solid var(--dg-border)' : '1px solid transparent',
background: chartType === t2 ? 'var(--dg-bg-tertiary)' : 'transparent',
color: chartType === t2 ? 'var(--dg-text-primary)' : 'var(--dg-text-muted)',
border: chartType === t2 ? '1px solid var(--dg-border)' : '1px solid transparent',
}}
>
{CHART_TYPE_LABELS[t].slice(0, 2)}
{chartTypeLabels[t2].slice(0, 2)}
</button>
))}
</div>
@@ -249,7 +251,7 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
{/* 字段配置 */}
<div className="mb-3">
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>X 轴字段</p>
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>{t('monitor.statChart.xAxisField')}</p>
<select
value={defaultX}
onChange={e => setXAxisField(e.target.value)}
@@ -259,7 +261,7 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
{stringCols.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>Y 轴字段</p>
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>{t('monitor.statChart.yAxisField')}</p>
<select
value={defaultY}
onChange={e => setYAxisField(e.target.value)}
@@ -272,9 +274,9 @@ export function StatChartPanel({ statTable, engine, query, onSaveAsAlert, onSave
{/* 当前查询 */}
<div>
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>当前查询</p>
<p className="text-[10px] mb-1.5" style={{ color: 'var(--dg-text-muted)' }}>{t('monitor.statChart.currentQuery')}</p>
<div className="p-2 rounded text-[10px] font-mono break-all" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-secondary)' }}>
{query || '暂无查询'}
{query || t('monitor.statChart.noQuery')}
</div>
</div>
</div>
@@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -63,6 +64,7 @@ export function LogRoleCreateDialog({
onDescriptionChange,
onSubmit,
}: LogRoleCreateDialogProps) {
const { t } = useTranslation();
const preview = useMemo(
() => matchSampleScopes(pattern, matchField, engine, SAMPLE_SCOPES),
[pattern, matchField, engine],
@@ -70,16 +72,20 @@ export function LogRoleCreateDialog({
const patternHint = useMemo(() => {
if (!pattern.trim()) {
return matchField === 'index' ? '输入正则,匹配 ES 索引名' : `输入正则,匹配 Loki ${matchField} Label`;
return matchField === 'index'
? t('rbac.logRole.patternHint.indexRegex')
: t('rbac.logRole.patternHint.labelRegex', { field: matchField });
}
try {
// eslint-disable-next-line no-new
new RegExp(pattern);
return preview.length > 0 ? `命中 ${preview.length} 个示例范围` : '无匹配示例';
return preview.length > 0
? t('rbac.logRole.patternHint.matched', { count: preview.length })
: t('rbac.logRole.patternHint.noMatch');
} catch {
return '正则语法错误';
return t('rbac.logRole.patternHint.syntaxError');
}
}, [pattern, matchField, preview.length]);
}, [pattern, matchField, preview.length, t]);
const filteredDS = datasources.filter((ds) => {
if (engine === '*') return true;
@@ -90,40 +96,40 @@ export function LogRoleCreateDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
<DialogHeader>
<DialogTitle>新建日志权限</DialogTitle>
<DialogTitle>{t('rbac.logRole.newTitle')}</DialogTitle>
<DialogDescription className="text-[var(--dg-text-muted)]">
按正则匹配 ES 索引或 Loki Label,限制用户可查询的日志范围
{t('rbac.logRole.desc')}
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">名称</Label>
<Input value={name} onChange={(e) => onNameChange(e.target.value)} placeholder="后端日志" className="h-8 text-sm dg-input" />
<Label className="text-xs">{t('rbac.logRole.label.name')}</Label>
<Input value={name} onChange={(e) => onNameChange(e.target.value)} placeholder={t('rbac.logRole.placeholder.name')} className="h-8 text-sm dg-input" />
</div>
<div className="space-y-1">
<Label className="text-xs">标识</Label>
<Input value={code} onChange={(e) => onCodeChange(e.target.value)} placeholder="backend-logs" className="h-8 text-sm font-mono dg-input" />
<Label className="text-xs">{t('rbac.logRole.label.code')}</Label>
<Input value={code} onChange={(e) => onCodeChange(e.target.value)} placeholder={t('rbac.logRole.placeholder.code')} className="h-8 text-sm font-mono dg-input" />
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">引擎</Label>
<Label className="text-xs">{t('rbac.logRole.label.engine')}</Label>
<Select value={engine} onValueChange={onEngineChange}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="*">全部引擎</SelectItem>
<SelectItem value="*">{t('rbac.logRole.engine.all')}</SelectItem>
<SelectItem value="loki">Loki</SelectItem>
<SelectItem value="elasticsearch">Elasticsearch</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">匹配维度</Label>
<Label className="text-xs">{t('rbac.logRole.label.matchField')}</Label>
<Select value={matchField} onValueChange={onMatchFieldChange}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="index">ES 索引</SelectItem>
<SelectItem value="index">{t('rbac.logRole.matchField.index')}</SelectItem>
<SelectItem value="app">Loki app</SelectItem>
<SelectItem value="namespace">Loki namespace</SelectItem>
<SelectItem value="job">Loki job</SelectItem>
@@ -132,7 +138,7 @@ export function LogRoleCreateDialog({
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">匹配正则</Label>
<Label className="text-xs">{t('rbac.logRole.label.pattern')}</Label>
<Input
value={pattern}
onChange={(e) => onPatternChange(e.target.value)}
@@ -143,11 +149,11 @@ export function LogRoleCreateDialog({
</div>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs">数据源(可选)</Label>
<Label className="text-xs">{t('rbac.logRole.label.datasource')}</Label>
<Select value={datasourceId || '__all'} onValueChange={(v) => onDatasourceIdChange(v === '__all' ? '' : v)}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder="全部数据源" /></SelectTrigger>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue placeholder={t('rbac.logRole.datasource.all')} /></SelectTrigger>
<SelectContent>
<SelectItem value="__all">全部数据源</SelectItem>
<SelectItem value="__all">{t('rbac.logRole.datasource.all')}</SelectItem>
{filteredDS.map((ds) => (
<SelectItem key={ds.id} value={String(ds.id)}>{ds.name} ({ds.type})</SelectItem>
))}
@@ -155,19 +161,19 @@ export function LogRoleCreateDialog({
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">权限级别</Label>
<Label className="text-xs">{t('rbac.logRole.label.roleLevel')}</Label>
<Select value={role} onValueChange={onRoleChange}>
<SelectTrigger className="h-8 text-sm dg-input"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="viewer">查看者 — 只读查询</SelectItem>
<SelectItem value="analyst">分析者 — 查询 + 告警</SelectItem>
<SelectItem value="viewer">{t('rbac.logRole.role.viewer')}</SelectItem>
<SelectItem value="analyst">{t('rbac.logRole.role.analyst')}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">说明</Label>
<Input value={description} onChange={(e) => onDescriptionChange(e.target.value)} placeholder="可选" className="h-8 text-sm dg-input" />
<Label className="text-xs">{t('rbac.logRole.label.description')}</Label>
<Input value={description} onChange={(e) => onDescriptionChange(e.target.value)} placeholder={t('rbac.logRole.placeholder.description')} className="h-8 text-sm dg-input" />
</div>
{preview.length > 0 && (
<div className="flex flex-wrap gap-1">
@@ -180,13 +186,13 @@ export function LogRoleCreateDialog({
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('rbac.logRole.cancel')}</Button>
<Button onClick={onSubmit} disabled={saving}>
{saving ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Save className="h-4 w-4 mr-1" />}
创建
{t('rbac.logRole.create')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
}