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