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:
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user