feat(i18n): 全量中英文国际化改造
- 翻译 en-US.json 中 1006 条未翻译的中文条目为英文 - 提取 30+ 组件文件的硬编码中文到 locale JSON - 新增 900+ 翻译键覆盖所有业务模块: - Layout (Sidebar, Header, Breadcrumb) - Server (ServerForm, CloudServerImport, WebShell, RdpTools) - Deploy (ReleaseManager, PipelineCreate, AppRelease) - Kubernetes (DeploymentEdit, PodExec, PodLogs, ResourceEditor) - Monitor (UnifiedQueryPanel, AlertCenter, Prometheus/Grafana Dashboard) - AIOps (predictUtils, PredictiveCostTab, AIPredictive) - System (useLdapManagement, AgentMonitor, AgentWizard) - tsc --noEmit 类型检查通过
This commit is contained in:
1 parent
76ec9387f0
commit
a9513c2056
44 files changed
+4342
-1792
No files matched your search
@@ -122,13 +122,13 @@ const AlertCenter: React.FC = () => {
|
||||
const hours = unit === 'd' ? num * 24 : num;
|
||||
const endTime = new Date(now.getTime() + hours * 3600_000);
|
||||
await alertCenterApi.createSilence({
|
||||
name: `静默: ${alert?.title || id} (${duration})`,
|
||||
name: `${t('monitor.alertCenterPanel.silencePrefix')} ${alert?.title || id} (${duration})`,
|
||||
description: `${t('alert.silenceDesc')} — ${alert?.source || ''}`,
|
||||
silence_type: 'onetime',
|
||||
start_time: now.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
});
|
||||
toast.success(`告警已静默 ${duration}`);
|
||||
toast.success(t('monitor.alertCenterPanel.silencedFor', { duration }));
|
||||
setSilenceOpen(false);
|
||||
refetchAlerts();
|
||||
} catch {
|
||||
@@ -173,10 +173,10 @@ const AlertCenter: React.FC = () => {
|
||||
const pageTabs = [
|
||||
{ id: 'alerts', label: t('alert.tab.alerts') },
|
||||
{ id: 'rules', label: t('alert.tab.rules') },
|
||||
{ id: 'metrics', label: '指标目录' },
|
||||
{ id: 'metrics', label: t('monitor.alertCenterPanel.metricCatalog') },
|
||||
{ id: 'contacts', label: t('alert.tab.contacts') },
|
||||
{ id: 'groups', label: t('alert.tab.groups') },
|
||||
{ id: 'notify', label: '通知策略' },
|
||||
{ id: 'notify', label: t('monitor.alertCenterPanel.notifyPolicy') },
|
||||
];
|
||||
|
||||
const alertsToolbar = activeTab === 'alerts' ? (
|
||||
@@ -215,15 +215,15 @@ const AlertCenter: React.FC = () => {
|
||||
</Select>
|
||||
<Select value={sourceFilter} onValueChange={(v) => handleFilterChange('source', v)}>
|
||||
<SelectTrigger className="w-[120px] h-8 text-xs">
|
||||
<SelectValue placeholder="来源" />
|
||||
<SelectValue placeholder={t('monitor.alertCenterPanel.sourcePlaceholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部来源</SelectItem>
|
||||
<SelectItem value="host">主机监控</SelectItem>
|
||||
<SelectItem value="middleware">中间件监控</SelectItem>
|
||||
<SelectItem value="snmp">SNMP 监控</SelectItem>
|
||||
<SelectItem value="site">站点拨测</SelectItem>
|
||||
<SelectItem value="log">日志告警</SelectItem>
|
||||
<SelectItem value="all">{t('monitor.alertCenterPanel.allSources')}</SelectItem>
|
||||
<SelectItem value="host">{t('monitor.alertCenterPanel.hostMonitor')}</SelectItem>
|
||||
<SelectItem value="middleware">{t('monitor.alertCenterPanel.middlewareMonitor')}</SelectItem>
|
||||
<SelectItem value="snmp">{t('monitor.alertCenterPanel.snmpMonitor')}</SelectItem>
|
||||
<SelectItem value="site">{t('monitor.alertCenterPanel.siteProbe')}</SelectItem>
|
||||
<SelectItem value="log">{t('monitor.alertCenterPanel.logAlert')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -239,8 +239,8 @@ const AlertCenter: React.FC = () => {
|
||||
return (
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="告警中心"
|
||||
subtitle="告警事件处理、规则与通知配置"
|
||||
title={t('monitor.alertCenterPanel.alertCenterTitle')}
|
||||
subtitle={t('monitor.alertCenterPanel.alertCenterSubtitle')}
|
||||
icon={<AlertTriangle className="h-5 w-5" />}
|
||||
tabs={pageTabs}
|
||||
activeTab={activeTab}
|
||||
@@ -262,7 +262,7 @@ const AlertCenter: React.FC = () => {
|
||||
</Button>
|
||||
) : activeTab === 'contacts' ? (
|
||||
<Button size="sm" className="dg-btn-primary" onClick={() => { setEditingContact(null); setContactDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-1" />添加联系人
|
||||
<Plus className="w-4 h-4 mr-1" />{t('monitor.alertCenterPanel.addContact')}
|
||||
</Button>
|
||||
) : activeTab === 'groups' ? (
|
||||
<>
|
||||
@@ -270,7 +270,7 @@ const AlertCenter: React.FC = () => {
|
||||
<RefreshCw className="w-4 h-4 mr-2" />{t('common.refresh')}
|
||||
</Button>
|
||||
<Button className="dg-btn-primary" onClick={() => { setEditingGroup(null); setGroupDialogOpen(true); }}>
|
||||
<Plus className="w-4 h-4 mr-2" />添加联系组
|
||||
<Plus className="w-4 h-4 mr-2" />{t('monitor.alertCenterPanel.addContactGroup')}
|
||||
</Button>
|
||||
</>
|
||||
) : undefined}
|
||||
@@ -407,7 +407,7 @@ const AlertCenter: React.FC = () => {
|
||||
|
||||
{activeTab === 'contacts' && (
|
||||
contacts.length === 0 ? (
|
||||
<PageEmpty title="暂无报警联系人" description="点击「添加联系人」创建" />
|
||||
<PageEmpty title={t('monitor.alertCenterPanel.noContactsTitle')} description={t('monitor.alertCenterPanel.noContactsDesc')} />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{contacts.map((c) => (
|
||||
@@ -435,7 +435,7 @@ const AlertCenter: React.FC = () => {
|
||||
<div className="flex gap-1.5 pt-1.5">
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs flex-1"
|
||||
onClick={() => { setEditingContact(c); setContactDialogOpen(true); }}>
|
||||
编辑
|
||||
{t("monitor.alertCenterPanel.edit")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs"
|
||||
onClick={async () => {
|
||||
@@ -444,7 +444,7 @@ const AlertCenter: React.FC = () => {
|
||||
try { await alertApi.deleteContact(c.id); toast.success(t('alert.deleted')); fetchContacts(); }
|
||||
catch { toast.error(t('alert.deleteFailed')); }
|
||||
}}>
|
||||
删除
|
||||
{t("monitor.alertCenterPanel.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -458,7 +458,7 @@ const AlertCenter: React.FC = () => {
|
||||
|
||||
{activeTab === 'groups' && (
|
||||
contactGroups.length === 0 ? (
|
||||
<PageEmpty title="暂无报警联系组" description="点击「添加联系组」创建" />
|
||||
<PageEmpty title={t('monitor.alertCenterPanel.noGroupsTitle')} description={t('monitor.alertCenterPanel.noGroupsDesc')} />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{contactGroups.map((g) => {
|
||||
@@ -485,7 +485,7 @@ const AlertCenter: React.FC = () => {
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" className="flex-1"
|
||||
onClick={() => { setEditingGroup(g); setGroupDialogOpen(true); }}>
|
||||
编辑
|
||||
{t("monitor.alertCenterPanel.edit")}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"
|
||||
onClick={async () => {
|
||||
@@ -494,7 +494,7 @@ const AlertCenter: React.FC = () => {
|
||||
try { await alertApi.deleteContactGroup(g.id); toast.success(t('alert.deleted')); fetchContactGroups(); }
|
||||
catch { toast.error(t('alert.deleteFailed')); }
|
||||
}}>
|
||||
删除
|
||||
{t("monitor.alertCenterPanel.delete")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -507,20 +507,20 @@ const DatasourceToolbar: React.FC<{
|
||||
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<Select value={activeId} onValueChange={onChange}>
|
||||
<SelectTrigger className="w-[280px] h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择数据源" />
|
||||
<SelectValue placeholder={t('monitor.datasourceToolbar.selectDatasource')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{sources.map(ds => (
|
||||
<SelectItem key={String(ds.id)} value={String(ds.id)}>
|
||||
{ds.name || ds.url}{ds.is_default ? ' · 默认' : ''}
|
||||
{ds.name || ds.url}{ds.is_default ? t('monitor.datasourceToolbar.defaultSuffix') : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-8 text-xs gap-1" onClick={onRefresh} disabled={refreshing}>
|
||||
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />刷新数据源
|
||||
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />{t('monitor.datasourceToolbar.refreshDatasource')}
|
||||
</Button>
|
||||
<span className="text-[11px] text-[var(--dg-text-muted)]">共 {sources.length} 个数据源,当前仅查询选中项</span>
|
||||
<span className="text-[11px] text-[var(--dg-text-muted)]">{t('monitor.datasourceToolbar.datasourceSummary', { count: sources.length })}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { StatStrip } from '@/components/ui/stat-strip';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { prometheusProxyApi } from '@/api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface Props { dsUrl?: string; }
|
||||
@@ -37,7 +38,6 @@ const fmtBps = (b: number) => {
|
||||
if (b > 1e3) return `${(b / 1e3).toFixed(1)} KB/s`;
|
||||
return `${b.toFixed(1)} B/s`;
|
||||
};
|
||||
const fmtCores = (v: number) => v >= 1 ? `${v.toFixed(2)} 核` : `${(v * 1000).toFixed(0)} m`;
|
||||
const podName = (p: JsonData) => `${p.namespace ? `${p.namespace}/` : ''}${p.pod || 'unknown'}`;
|
||||
|
||||
const metricValue = (item?: JsonData): number => Number(item?.value ?? Number.NaN);
|
||||
@@ -65,7 +65,11 @@ function parsePrune( result: JsonData[]): JsonData[] {
|
||||
|
||||
const Box = (p: JsonData) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
|
||||
|
||||
const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
const fmtCores = (v: number, t: (k: string) => string) => v >= 1 ? `${v.toFixed(2)} ${t('monitor.prometheusPanel.cores')}` : `${(v * 1000).toFixed(0)} m`;
|
||||
|
||||
const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<StatStrip
|
||||
items={[
|
||||
@@ -76,18 +80,20 @@ 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" />{t('monitor.prometheusPanel.nodeCpuMemUtil')}</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>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
type NodeSortField = 'name' | 'cpu' | 'mem' | 'disk' | 'rx' | 'tx';
|
||||
|
||||
const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
const { t } = useTranslation();
|
||||
const [sortField, setSortField] = useState<NodeSortField>('cpu');
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||
const [page, setPage] = useState(1);
|
||||
@@ -130,8 +136,8 @@ const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-[var(--dg-bg-tertiary)]"><tr>
|
||||
{[
|
||||
{ f: 'name' as NodeSortField, l: '节点' }, { f: 'cpu' as NodeSortField, l: 'CPU' },
|
||||
{ f: 'mem' as NodeSortField, l: '内存' }, { f: 'disk' as NodeSortField, l: '磁盘' }, { f: 'rx' as NodeSortField, l: '流入' }, { f: 'tx' as NodeSortField, l: '流出' },
|
||||
{ f: 'name' as NodeSortField, l: t('monitor.prometheusPanel.node') }, { f: 'cpu' as NodeSortField, l: t('monitor.prometheusPanel.cpu') },
|
||||
{ f: 'mem' as NodeSortField, l: t('monitor.prometheusPanel.memory') }, { f: 'disk' as NodeSortField, l: t('monitor.prometheusPanel.disk') }, { f: 'rx' as NodeSortField, l: t('monitor.prometheusPanel.inbound') }, { f: 'tx' as NodeSortField, l: t('monitor.prometheusPanel.outbound') },
|
||||
].map(({ f, l }) => (
|
||||
<th key={f} className="px-3 py-2 text-left cursor-pointer select-none" onClick={() => toggleSort(f)}><div className="flex items-center gap-1">{l}<SI f={f} /></div></th>
|
||||
))}
|
||||
@@ -152,15 +158,15 @@ const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => {
|
||||
{sorted.length > pageSize && (
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-xs">
|
||||
<div className="flex items-center gap-2 text-[var(--dg-text-muted)]">
|
||||
<span>共 {sorted.length} 个节点</span>
|
||||
<span>{t('monitor.prometheusPanel.totalNodes', { count: sorted.length })}</span>
|
||||
<select className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded px-2 py-0.5 text-xs" value={pageSize} onChange={e => { setPageSize(Number(e.target.value)); setPage(1); }}>
|
||||
<option value={10}>10/页</option><option value={20}>20/页</option><option value={50}>50/页</option>
|
||||
<option value={10}>10/{t('monitor.prometheusPanel.perPage')}</option><option value={20}>20/{t('monitor.prometheusPanel.perPage')}</option><option value={50}>50/{t('monitor.prometheusPanel.perPage')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={safePage <= 1} onClick={() => setPage(Math.max(1, safePage - 1))}>上一页</button>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={safePage <= 1} onClick={() => setPage(Math.max(1, safePage - 1))}>{t('monitor.prometheusPanel.prevPage')}</button>
|
||||
<span className="px-2 text-[var(--dg-text-primary)]">{safePage} / {totalPages}</span>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={safePage >= totalPages} onClick={() => setPage(Math.min(totalPages, safePage + 1))}>下一页</button>
|
||||
<button className="px-2 py-0.5 rounded hover:bg-[var(--dg-bg-primary)] disabled:opacity-30" disabled={safePage >= totalPages} onClick={() => setPage(Math.min(totalPages, safePage + 1))}>{t('monitor.prometheusPanel.nextPage')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -183,7 +189,7 @@ const SimpleRank: React.FC<{
|
||||
<CardContent className="p-3 pt-0">
|
||||
<div className="space-y-2 max-h-[320px] overflow-y-auto pr-1">
|
||||
{rows.length === 0 ? (
|
||||
<div className="py-8 text-center text-xs text-[var(--dg-text-muted)]">暂无数据</div>
|
||||
<div className="py-8 text-center text-xs text-[var(--dg-text-muted)]">{t('monitor.prometheusPanel.noData')}</div>
|
||||
) : rows.slice(0, 10).map((p: JsonData, i: number) => {
|
||||
const value = Number(p.value || 0);
|
||||
return (
|
||||
@@ -226,7 +232,7 @@ const PodNetworkRank: React.FC<{ rx: JsonData[]; tx: JsonData[] }> = ({ rx, tx }
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-primary)] border-[var(--dg-border)]">
|
||||
<CardHeader className="py-2 px-3">
|
||||
<CardTitle className="text-xs font-medium">Pod 流量网卡 Top 10</CardTitle>
|
||||
<CardTitle className="text-xs font-medium">{t('monitor.prometheusPanel.podTrafficTop10')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
@@ -234,14 +240,14 @@ const PodNetworkRank: React.FC<{ rx: JsonData[]; tx: JsonData[] }> = ({ rx, tx }
|
||||
<thead className="bg-[var(--dg-bg-tertiary)] border-y border-[var(--dg-border)]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-[var(--dg-text-secondary)]">Pod</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">流入</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">流出</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">总流量</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('monitor.prometheusPanel.inbound')}</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('monitor.prometheusPanel.outbound')}</th>
|
||||
<th className="px-3 py-2 text-right font-medium text-[var(--dg-text-secondary)]">{t('monitor.prometheusPanel.totalTraffic')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 ? (
|
||||
<tr><td colSpan={4} className="px-3 py-8 text-center text-[var(--dg-text-muted)]">暂无数据</td></tr>
|
||||
<tr><td colSpan={4} className="px-3 py-8 text-center text-[var(--dg-text-muted)]">{t('monitor.prometheusPanel.noData')}</td></tr>
|
||||
) : rows.map((p, i) => (
|
||||
<tr key={p.name} className="border-b border-[var(--dg-border)] last:border-0 hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2">
|
||||
@@ -271,8 +277,8 @@ const PodPanel: React.FC<{ d: JsonData}> = ({ d }) => (
|
||||
/>
|
||||
<PodNetworkRank rx={d.netRxTop || []} tx={d.netTxTop || []} />
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<SimpleRank title="Pod CPU Top 10" rows={d.cpuTop || []} valueText={fmtCores} color="bg-purple-500" />
|
||||
<SimpleRank title="Pod 内存 Top 10" rows={d.memTop || []} valueText={fmtBytes} color="bg-blue-500" />
|
||||
<SimpleRank title="Pod CPU Top 10" rows={d.cpuTop || []} valueText={(v) => fmtCores(v, t)} color="bg-purple-500" />
|
||||
<SimpleRank title={t('monitor.prometheusPanel.podMemTop10')} rows={d.memTop || []} valueText={fmtBytes} color="bg-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -365,7 +371,7 @@ const JvmPanel: React.FC<{ dsUrl?: string; fallback: JsonData }> = ({ dsUrl, fal
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Select value={application} onValueChange={setApplication}>
|
||||
<SelectTrigger className="w-[260px] h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择 Application" />
|
||||
<SelectValue placeholder={t('monitor.prometheusPanel.selectApplication')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{applications.map(app => <SelectItem key={app} value={app}>{app}</SelectItem>)}
|
||||
@@ -373,13 +379,13 @@ const JvmPanel: React.FC<{ dsUrl?: string; fallback: JsonData }> = ({ dsUrl, fal
|
||||
</Select>
|
||||
<Select value={instance} onValueChange={setInstance}>
|
||||
<SelectTrigger className="w-[280px] h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择 Instance" />
|
||||
<SelectValue placeholder={t('monitor.prometheusPanel.selectInstance')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{instances.map(ins => <SelectItem key={ins} value={ins}>{ins}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{loading && <span className="text-[11px] text-[var(--dg-text-muted)] inline-flex items-center gap-1"><Loader2 className="w-3 h-3 animate-spin" />加载 JVM 指标</span>}
|
||||
{loading && <span className="text-[11px] text-[var(--dg-text-muted)] inline-flex items-center gap-1"><Loader2 className="w-3 h-3 animate-spin" />{t('monitor.prometheusPanel.loadingJvmMetrics')}</span>}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
|
||||
<Card className="bg-[var(--dg-bg-primary)]"><CardContent className="p-3 text-center"><div className="text-lg font-bold">{uptime>86400?`${(uptime/86400).toFixed(1)}d`:`${(uptime/3600).toFixed(1)}h`}</div><div className="text-[10px] text-[var(--dg-text-muted)]">Uptime</div></CardContent></Card>
|
||||
@@ -414,7 +420,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||t('monitor.prometheusPanel.connectionFailed')); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
@@ -442,13 +448,13 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
|
||||
<CardHeader className="py-3 px-4 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 dg-text-purple" />
|
||||
<CardTitle className="text-sm font-medium">Prometheus 监控</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">{t('monitor.prometheusPanel.prometheusMonitor')}</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" />{t('monitor.prometheusPanel.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)]">{t('monitor.prometheusPanel.connecting')}</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}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useState, useEffect } from 'react';
|
||||
import { X, BellRing, Loader2, Flame, Database, Cloud } from 'lucide-react';
|
||||
import { alertApi } from '@/api';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getErrorMessage } from '@/lib/utils';
|
||||
import type { LogEngine } from './types-v3';
|
||||
|
||||
@@ -30,6 +31,7 @@ const ENGINE_META: Record<LogEngine, { name: string; Icon: typeof Flame; color:
|
||||
|
||||
export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Props) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [expression, setExpression] = useState('');
|
||||
@@ -41,8 +43,8 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
|
||||
useEffect(() => {
|
||||
if (!open || !ctx) return;
|
||||
const meta = ENGINE_META[ctx.engine];
|
||||
setName(`${meta.name} 日志告警`);
|
||||
setDescription(`数据源: ${ctx.datasourceName || '—'} · 范围: ${ctx.timeRange || '—'}${ctx.index ? ` · 索引: ${ctx.index}` : ''}`);
|
||||
setName(`${meta.name} ${t('monitor.logAlertFromQuery.logAlert')}`);
|
||||
setDescription(`${t('monitor.logAlertFromQuery.datasourceLabel')}: ${ctx.datasourceName || '—'} · ${t('monitor.logAlertFromQuery.rangeLabel')}: ${ctx.timeRange || '—'}${ctx.index ? ` · ${t('monitor.logAlertFromQuery.indexLabel')}: ${ctx.index}` : ''}`);
|
||||
if (ctx.engine === 'loki') {
|
||||
setExpression(`count_over_time(${ctx.query}[5m]) > 0`);
|
||||
} else if (ctx.engine === 'sls') {
|
||||
@@ -74,11 +76,11 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
|
||||
notify_channels: [],
|
||||
});
|
||||
setSaved(true);
|
||||
toast({ title: '告警规则已创建' });
|
||||
toast({ title: t('monitor.logAlertFromQuery.ruleCreated') });
|
||||
onSaved?.();
|
||||
setTimeout(() => onOpenChange(false), 800);
|
||||
} catch (e: unknown) {
|
||||
toast({ title: '创建失败', description: getErrorMessage(e), variant: 'destructive' });
|
||||
toast({ title: t('monitor.logAlertFromQuery.createFailed'), description: getErrorMessage(e), variant: 'destructive' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -95,7 +97,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)' }}>{t('monitor.logAlertFromQuery.createRuleTitle')}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
@@ -113,12 +115,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 || t('monitor.logAlertFromQuery.defaultDatasource')}</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)' }}>{t('monitor.logAlertFromQuery.ruleName')} *</label>
|
||||
<input
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
@@ -128,7 +130,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)' }}>{t('monitor.logAlertFromQuery.description')}</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
@@ -139,7 +141,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)' }}>{t('monitor.logAlertFromQuery.triggerCondition')} *</label>
|
||||
<textarea
|
||||
value={expression}
|
||||
onChange={e => setExpression(e.target.value)}
|
||||
@@ -148,26 +150,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' ? t('monitor.logAlertFromQuery.logqlHint') : ctx.engine === 'sls' ? t('monitor.logAlertFromQuery.slsHint') : t('monitor.logAlertFromQuery.esHint')}
|
||||
</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)' }}>{t('monitor.logAlertFromQuery.level')}</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">{t('monitor.logAlertFromQuery.info')}</option>
|
||||
<option value="warning">{t('monitor.logAlertFromQuery.warning')}</option>
|
||||
<option value="critical">{t('monitor.logAlertFromQuery.critical')}</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)' }}>{t('monitor.logAlertFromQuery.durationSeconds')}</label>
|
||||
<input
|
||||
type="number"
|
||||
value={duration}
|
||||
@@ -180,7 +182,7 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
|
||||
|
||||
{/* 原始查询 */}
|
||||
<div className="p-2 rounded text-[10px] font-mono break-all" style={{ background: 'var(--dg-bg-tertiary)', color: 'var(--dg-text-secondary)' }}>
|
||||
原始查询: {ctx.query}
|
||||
{t('monitor.logAlertFromQuery.originalQuery')}: {ctx.query}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -191,7 +193,7 @@ export function LogAlertFromQueryDialog({ open, onOpenChange, ctx, onSaved }: Pr
|
||||
className="h-8 px-3 rounded text-xs transition-colors"
|
||||
style={{ color: 'var(--dg-text-secondary)', border: '1px solid var(--dg-border)', background: 'var(--dg-bg-tertiary)' }}
|
||||
>
|
||||
取消
|
||||
{t('monitor.logAlertFromQuery.cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
@@ -200,7 +202,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 ? t('monitor.logAlertFromQuery.created') : t('monitor.logAlertFromQuery.createRule')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { Loader2, Plug, RefreshCw } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getErrorMessage } from '@/lib/utils';
|
||||
import { logCenterApi, type LogDataSource, type TestConnectionResult } from '@/api';
|
||||
import type { LogEngine } from './log-center-types';
|
||||
@@ -26,6 +27,7 @@ export function LogDatasourceDialog({
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [defaultIndex, setDefaultIndex] = useState('logstash-*');
|
||||
@@ -128,12 +130,12 @@ export function LogDatasourceDialog({
|
||||
: payload();
|
||||
const result: TestConnectionResult = await logCenterApi.testDatasource(data);
|
||||
if (result.status === 'ok') {
|
||||
toast({ title: '连接成功' });
|
||||
toast({ title: t('monitor.logDatasource.connectionSuccess') });
|
||||
} else {
|
||||
toast({ title: '连接失败', description: result.error || '未知错误', variant: 'destructive' });
|
||||
toast({ title: t('monitor.logDatasource.connectionFailed'), description: result.error || t('monitor.logDatasource.unknownError'), variant: 'destructive' });
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
toast({ title: '连接失败', description: getErrorMessage(e), variant: 'destructive' });
|
||||
toast({ title: t('monitor.logDatasource.connectionFailed'), description: getErrorMessage(e), variant: 'destructive' });
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
@@ -148,11 +150,11 @@ export function LogDatasourceDialog({
|
||||
} else {
|
||||
await logCenterApi.createDatasource(payload());
|
||||
}
|
||||
toast({ title: '已保存' });
|
||||
toast({ title: t('monitor.logDatasource.saved') });
|
||||
onOpenChange(false);
|
||||
onSaved();
|
||||
} catch (e: unknown) {
|
||||
toast({ title: '保存失败', description: getErrorMessage(e), variant: 'destructive' });
|
||||
toast({ title: t('monitor.logDatasource.saveFailed'), description: getErrorMessage(e), variant: 'destructive' });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -163,25 +165,25 @@ 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 ? t('monitor.logDatasource.editPrefix') : t('monitor.logDatasource.addPrefix')}{engine === 'loki' ? ' Loki' : ' Elasticsearch'} {t('monitor.logDatasource.datasourceSuffix')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">
|
||||
仅保存连接信息,日志本体存储在您的 Loki / ES 集群
|
||||
{t('monitor.logDatasource.description')}
|
||||
</DialogDescription>
|
||||
</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)]">{t('monitor.logDatasource.name')}</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)]">{t('monitor.logDatasource.address')}</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)]">{t('monitor.logDatasource.defaultIndex')}</Label>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -208,10 +210,10 @@ export function LogDatasourceDialog({
|
||||
) : (
|
||||
<RefreshCw className="h-3 w-3 mr-0.5" />
|
||||
)}
|
||||
获取索引
|
||||
{t('monitor.logDatasource.fetchIndices')}
|
||||
</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={t('monitor.logDatasource.indexPlaceholder')} 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">
|
||||
@@ -246,23 +248,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)]">{t('monitor.logDatasource.authMethod')}</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">{t('monitor.logDatasource.noAuth')}</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">{t('monitor.logDatasource.customHeaders')}</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={t('monitor.logDatasource.usernamePlaceholder')} 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={t('monitor.logDatasource.passwordPlaceholder')} className="h-9 text-xs bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
|
||||
</div>
|
||||
)}
|
||||
{authType === 'bearer' && (
|
||||
@@ -278,34 +280,34 @@ 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={t('monitor.logDatasource.headerNamePlaceholder')} 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={t('monitor.logDatasource.headerValuePlaceholder')} 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: '' }])}>{t('monitor.logDatasource.addHeader')}</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)]">{t('monitor.logDatasource.skipTLS')}</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)]">{t('monitor.logDatasource.setDefault')}</Label>
|
||||
<Switch checked={isDefault} onCheckedChange={setIsDefault} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleTest} disabled={testing}>
|
||||
{testing ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Plug className="h-4 w-4 mr-1" />}
|
||||
测试
|
||||
{t('monitor.logDatasource.test')}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={saving}>
|
||||
{saving && <Loader2 className="h-4 w-4 animate-spin mr-1" />}
|
||||
保存
|
||||
{t('monitor.logDatasource.save')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -36,24 +36,24 @@ function getPresetRange(value: string): { start: Date; end: Date; label: string
|
||||
let label = '';
|
||||
|
||||
switch (value) {
|
||||
case '1m': start.setMinutes(now.getMinutes() - 1); label = '最近1分钟'; break;
|
||||
case '5m': start.setMinutes(now.getMinutes() - 5); label = '最近5分钟'; break;
|
||||
case '15m': start.setMinutes(now.getMinutes() - 15); label = '最近15分钟'; break;
|
||||
case '1h': start.setHours(now.getHours() - 1); label = '最近1小时'; break;
|
||||
case '4h': start.setHours(now.getHours() - 4); label = '最近4小时'; break;
|
||||
case '24h': start.setDate(now.getDate() - 1); label = '最近1天'; break;
|
||||
case '7d': start.setDate(now.getDate() - 7); label = '最近1周'; break;
|
||||
case '30d': start.setDate(now.getDate() - 30); label = '最近30天'; break;
|
||||
case 'today': start.setHours(0, 0, 0, 0); label = '今天'; break;
|
||||
case 'yesterday': start.setDate(now.getDate() - 1); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 1); end.setHours(23, 59, 59, 999); label = '昨天'; break;
|
||||
case 'day_before_yesterday': start.setDate(now.getDate() - 2); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 2); end.setHours(23, 59, 59, 999); label = '前天'; break;
|
||||
case 'this_week': { const d = now.getDay(); const sun = d === 0 ? 0 : d; start.setDate(now.getDate() - sun); start.setHours(0, 0, 0, 0); label = '本周'; break; }
|
||||
case 'last_week': { const d = now.getDay(); const sun = d === 0 ? 0 : d; start.setDate(now.getDate() - sun - 7); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - sun); end.setHours(0, 0, 0, 0); label = '上周'; break; }
|
||||
case 'this_month': start.setDate(1); start.setHours(0, 0, 0, 0); label = '本月'; break;
|
||||
case 'last_month': start.setMonth(now.getMonth() - 1); start.setDate(1); start.setHours(0, 0, 0, 0); end.setMonth(now.getMonth()); end.setDate(1); end.setHours(0, 0, 0, 0); label = '上月'; break;
|
||||
case 'this_quarter': { const q = Math.floor(now.getMonth() / 3); start.setMonth(q * 3); start.setDate(1); start.setHours(0, 0, 0, 0); label = '本季度'; break; }
|
||||
case 'this_year': start.setMonth(0); start.setDate(1); start.setHours(0, 0, 0, 0); label = '本年度'; break;
|
||||
default: start.setMinutes(now.getMinutes() - 5); label = '最近5分钟';
|
||||
case '1m': start.setMinutes(now.getMinutes() - 1); label = i18n.t('monitor.timeRangePicker.range1min'); break;
|
||||
case '5m': start.setMinutes(now.getMinutes() - 5); label = i18n.t('monitor.timeRangePicker.range5min'); break;
|
||||
case '15m': start.setMinutes(now.getMinutes() - 15); label = i18n.t('monitor.timeRangePicker.range15min'); break;
|
||||
case '1h': start.setHours(now.getHours() - 1); label = i18n.t('monitor.timeRangePicker.range1hour'); break;
|
||||
case '4h': start.setHours(now.getHours() - 4); label = i18n.t('monitor.timeRangePicker.range4hours'); break;
|
||||
case '24h': start.setDate(now.getDate() - 1); label = i18n.t('monitor.timeRangePicker.range1day'); break;
|
||||
case '7d': start.setDate(now.getDate() - 7); label = i18n.t('monitor.timeRangePicker.range1week'); break;
|
||||
case '30d': start.setDate(now.getDate() - 30); label = i18n.t('monitor.timeRangePicker.range30days'); break;
|
||||
case 'today': start.setHours(0, 0, 0, 0); label = i18n.t('monitor.timeRangePicker.today'); break;
|
||||
case 'yesterday': start.setDate(now.getDate() - 1); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 1); end.setHours(23, 59, 59, 999); label = i18n.t('monitor.timeRangePicker.yesterday'); break;
|
||||
case 'day_before_yesterday': start.setDate(now.getDate() - 2); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - 2); end.setHours(23, 59, 59, 999); label = i18n.t('monitor.timeRangePicker.dayBeforeYesterday'); break;
|
||||
case 'this_week': { const d = now.getDay(); const sun = d === 0 ? 0 : d; start.setDate(now.getDate() - sun); start.setHours(0, 0, 0, 0); label = i18n.t('monitor.timeRangePicker.thisWeek'); break; }
|
||||
case 'last_week': { const d = now.getDay(); const sun = d === 0 ? 0 : d; start.setDate(now.getDate() - sun - 7); start.setHours(0, 0, 0, 0); end.setDate(now.getDate() - sun); end.setHours(0, 0, 0, 0); label = i18n.t('monitor.timeRangePicker.lastWeek'); break; }
|
||||
case 'this_month': start.setDate(1); start.setHours(0, 0, 0, 0); label = i18n.t('monitor.timeRangePicker.thisMonth'); break;
|
||||
case 'last_month': start.setMonth(now.getMonth() - 1); start.setDate(1); start.setHours(0, 0, 0, 0); end.setMonth(now.getMonth()); end.setDate(1); end.setHours(0, 0, 0, 0); label = i18n.t('monitor.timeRangePicker.lastMonth'); break;
|
||||
case 'this_quarter': { const q = Math.floor(now.getMonth() / 3); start.setMonth(q * 3); start.setDate(1); start.setHours(0, 0, 0, 0); label = i18n.t('monitor.timeRangePicker.thisQuarter'); break; }
|
||||
case 'this_year': start.setMonth(0); start.setDate(1); start.setHours(0, 0, 0, 0); label = i18n.t('monitor.timeRangePicker.thisYear'); break;
|
||||
default: start.setMinutes(now.getMinutes() - 5); label = i18n.t('monitor.timeRangePicker.range5min');
|
||||
}
|
||||
return { start, end, label };
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export function TimeRangePicker({ value, onChange }: Props) {
|
||||
background: mode === 'custom' ? 'var(--dg-blue)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
日历中选择
|
||||
{i18n.t('monitor.timeRangePicker.calendarSelect')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,13 +25,14 @@ import { applyESFieldFilter, applyLokiLabelFilter, extractLokiSelector } from '.
|
||||
import type { LogEngine, TimeRange, ResultTab, ViewMode, LogEntry, LogField, StatTable, LogCluster, QueryHistoryItem, SavedQuery } from './types-v3';
|
||||
import { ENGINE_META, buildStatTableFromEntries, buildClustersFromEntries, extractFields, generateHistogramData, historyStorage } from './types-v3';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getErrorMessage } from '@/lib/utils';
|
||||
|
||||
// 默认最近5分钟 // Logan.Li
|
||||
const DEFAULT_TIME_RANGE: TimeRange = {
|
||||
start: new Date(Date.now() - 5 * 60000).toISOString().replace('T', ' ').substring(0, 19),
|
||||
end: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
label: '最近5分钟',
|
||||
label: 'Last 5 min',
|
||||
type: 'relative',
|
||||
};
|
||||
|
||||
@@ -52,6 +53,7 @@ interface Props { engine: LogEngine; onRuleCreated: () => void; }
|
||||
|
||||
export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const urlBootstrapped = useRef(false);
|
||||
@@ -265,10 +267,10 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
historyStorage.add({ engine: effEngine as LogEngine, datasourceName: currentSource.name, query, timeRange: timeRange.label, resultCount: mapped.length });
|
||||
} catch (e: unknown) {
|
||||
const msg = getErrorMessage(e);
|
||||
const title = msg.includes('401') || msg.includes('403') ? '认证失败'
|
||||
: msg.includes('404') ? '接口不存在'
|
||||
: msg.includes('无法连接') || msg.includes('timeout') ? '连接失败'
|
||||
: '查询失败';
|
||||
const title = msg.includes('401') || msg.includes('403') ? t('monitor.logPanel.authFailed')
|
||||
: msg.includes('404') ? t('monitor.logPanel.apiNotFound')
|
||||
: msg.includes('无法连接') || msg.includes('timeout') ? t('monitor.logPanel.connFailed')
|
||||
: t('monitor.logPanel.queryFailed');
|
||||
toast({ title, description: msg, variant: 'destructive' });
|
||||
setEntries([]); setTotalCount(0); setHasQueried(true);
|
||||
} finally {
|
||||
@@ -303,7 +305,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
setNextCursor(data?.next_cursor || '');
|
||||
setSearchAfter(data?.search_after);
|
||||
} catch (e: unknown) {
|
||||
toast({ title: '加载更多失败', description: (e as Error)?.message, variant: 'destructive' });
|
||||
toast({ title: t('monitor.logPanel.loadMoreFailed'), description: (e as Error)?.message, variant: 'destructive' });
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
@@ -343,8 +345,8 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
const full = `${window.location.origin}${url}`;
|
||||
const ok = await copyToClipboard(full);
|
||||
toast({
|
||||
title: ok ? '分享链接已复制' : '复制失败',
|
||||
description: ok ? '收件人打开链接将自动执行查询' : undefined,
|
||||
title: ok ? t('monitor.logPanel.shareCopied') : t('monitor.logPanel.shareCopyFailed'),
|
||||
description: ok ? t('monitor.logPanel.shareCopiedDesc') : undefined,
|
||||
variant: ok ? 'default' : 'destructive',
|
||||
});
|
||||
}, [location.pathname, query, dsId, esIndex, timeRange, toast]);
|
||||
@@ -423,7 +425,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
const idx = histogramData.findIndex(d => d.timestamp === st.activeLabel);
|
||||
if (idx < 0) return;
|
||||
const sIdx = Math.max(0, idx - 1), eIdx = Math.min(histogramData.length - 1, idx + 1);
|
||||
setTimeRange(p => ({ ...p, start: histogramData[sIdx].timestamp, end: histogramData[eIdx].timestamp, label: '自定义', type: 'absolute' as const }));
|
||||
setTimeRange(p => ({ ...p, start: histogramData[sIdx].timestamp, end: histogramData[eIdx].timestamp, label: t('monitor.logPanel.custom'), type: 'absolute' as const }));
|
||||
}, [histogramData]);
|
||||
|
||||
const formatEntryLine = useCallback((e: LogEntry) => buildLogCopyText(e), []);
|
||||
@@ -437,16 +439,16 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
a.download = `${effEngine}-logs-${Date.now()}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(u);
|
||||
toast({ title: '已导出', description: `${sortedEntries.length} 条日志` });
|
||||
toast({ title: t('monitor.logPanel.exported'), description: t('monitor.logPanel.exportedDesc', { count: sortedEntries.length }) });
|
||||
}, [sortedEntries, effEngine, formatEntryLine, toast]);
|
||||
|
||||
const handleCopyLogs = useCallback(async () => {
|
||||
const text = pagedEntries.map(formatEntryLine).join('\n');
|
||||
const ok = await copyToClipboard(text);
|
||||
if (ok) {
|
||||
toast({ title: '已复制', description: `当前页 ${pagedEntries.length} 条完整日志` });
|
||||
toast({ title: t('monitor.logPanel.copied'), description: t('monitor.logPanel.copiedDesc', { count: pagedEntries.length }) });
|
||||
} else {
|
||||
toast({ title: '复制失败', description: '请使用导出或展开单条复制', variant: 'destructive' });
|
||||
toast({ title: t('monitor.logPanel.copyFailed'), description: t('monitor.logPanel.copyFailedDesc'), variant: 'destructive' });
|
||||
}
|
||||
}, [pagedEntries, formatEntryLine, toast]);
|
||||
|
||||
@@ -470,7 +472,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
onRequestLabelValues={handleRequestLabelValues}
|
||||
/>
|
||||
<button onClick={() => { setLiveTail(false); void handleRun(); }} disabled={loading || liveTail || !currentSource || !query.trim()} className="h-10 px-4 rounded text-xs font-medium text-white flex items-center gap-1.5 flex-shrink-0 transition-opacity hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed" style={{ background: '#22c55e' }}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />} 查询
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />} {t('monitor.logPanel.query')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLiveTail((v) => !v)}
|
||||
@@ -481,7 +483,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
color: liveTail ? '#ef4444' : 'var(--dg-text-secondary)',
|
||||
background: liveTail ? 'rgba(239,68,68,0.1)' : 'var(--dg-bg-tertiary)',
|
||||
}}
|
||||
title="Live Tail — 每 3 秒刷新最近 5 分钟日志"
|
||||
title={t('monitor.logPanel.liveTailTitle')}
|
||||
>
|
||||
<Radio className={`h-3.5 w-3.5 ${liveTail ? 'animate-pulse' : ''}`} /> Tail
|
||||
</button>
|
||||
@@ -492,8 +494,8 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
<TimeRangePicker value={timeRange} onChange={setTimeRange} />
|
||||
|
||||
<select value={dsId} onChange={e => setDsId(e.target.value)} className="h-10 px-2 text-xs rounded flex-shrink-0" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)', color: 'var(--dg-text-primary)' }}>
|
||||
{sources.length === 0 && <option value="">无可用{meta.name}数据源</option>}
|
||||
{sources.map(s => <option key={s.id} value={s.id}>{s.name}{s.is_default ? ' (默认)' : ''}</option>)}
|
||||
{sources.length === 0 && <option value="">{t('monitor.logPanel.noDatasource', { name: meta.name })}</option>}
|
||||
{sources.map(s => <option key={s.id} value={s.id}>{s.name}{s.is_default ? t('monitor.logPanel.defaultSuffix') : ''}</option>)}
|
||||
</select>
|
||||
|
||||
{effEngine === 'elasticsearch' && (
|
||||
@@ -505,7 +507,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-1.5 h-10 px-2 rounded flex-shrink-0" style={{ background: 'var(--dg-bg-tertiary)', border: '1px solid var(--dg-border)' }}>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">上限</span>
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">{t('monitor.logPanel.limit')}</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -522,24 +524,24 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
|
||||
<div className="relative flex-shrink-0">
|
||||
<button onClick={() => setSaveMenuOpen(!saveMenuOpen)} className="h-10 px-3 rounded text-xs flex items-center gap-1" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-secondary)', background: 'var(--dg-bg-tertiary)' }}>
|
||||
<Save className="h-3.5 w-3.5" /> 另存为 <ChevronDown className="h-3 w-3" />
|
||||
<Save className="h-3.5 w-3.5" /> {t('monitor.logPanel.saveAs')} <ChevronDown className="h-3 w-3" />
|
||||
</button>
|
||||
{saveMenuOpen && (<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setSaveMenuOpen(false)} />
|
||||
<div className="absolute right-0 top-full mt-1 z-50 rounded-lg border shadow-xl overflow-hidden" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
|
||||
<button onClick={() => handleSaveMenu('query')} className="w-full text-left px-4 py-2 text-xs flex items-center gap-2 hover:opacity-80" style={{ color: 'var(--dg-text-secondary)' }}><Save className="h-3.5 w-3.5" /> 查询</button>
|
||||
<button onClick={() => handleSaveMenu('alert')} className="w-full text-left px-4 py-2 text-xs flex items-center gap-2 hover:opacity-80" style={{ color: 'var(--dg-text-secondary)', borderTop: '1px solid var(--dg-border)' }}><Bell className="h-3.5 w-3.5" /> 告警</button>
|
||||
<button onClick={() => handleSaveMenu('query')} className="w-full text-left px-4 py-2 text-xs flex items-center gap-2 hover:opacity-80" style={{ color: 'var(--dg-text-secondary)' }}><Save className="h-3.5 w-3.5" /> {t('monitor.logPanel.saveAsQueryItem')}</button>
|
||||
<button onClick={() => handleSaveMenu('alert')} className="w-full text-left px-4 py-2 text-xs flex items-center gap-2 hover:opacity-80" style={{ color: 'var(--dg-text-secondary)', borderTop: '1px solid var(--dg-border)' }}><Bell className="h-3.5 w-3.5" /> {t('monitor.logPanel.saveAsAlertItem')}</button>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
<button onClick={() => { void handleShareLink(); }} className="h-10 px-2.5 flex items-center gap-1 rounded flex-shrink-0 text-xs" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }} title="复制分享链接">
|
||||
<Link2 className="h-3.5 w-3.5" /> 分享
|
||||
<button onClick={() => { void handleShareLink(); }} className="h-10 px-2.5 flex items-center gap-1 rounded flex-shrink-0 text-xs" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }} title={t('monitor.logPanel.shareLinkTitle')}>
|
||||
<Link2 className="h-3.5 w-3.5" /> {t('monitor.logPanel.share')}
|
||||
</button>
|
||||
<button onClick={() => setSavedPanelOpen(true)} className="h-10 w-10 flex items-center justify-center rounded flex-shrink-0" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }} title="已保存查询">
|
||||
<button onClick={() => setSavedPanelOpen(true)} className="h-10 w-10 flex items-center justify-center rounded flex-shrink-0" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }} title={t('monitor.logPanel.savedQueries')}>
|
||||
<Bookmark className="h-4 w-4" />
|
||||
</button>
|
||||
<button onClick={() => setHistoryPanelOpen(true)} className="h-10 w-10 flex items-center justify-center rounded flex-shrink-0" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }} title="查询历史">
|
||||
<button onClick={() => setHistoryPanelOpen(true)} className="h-10 w-10 flex items-center justify-center rounded flex-shrink-0" style={{ border: '1px solid var(--dg-border)', color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }} title={t('monitor.logPanel.history')}>
|
||||
<Clock className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -547,7 +549,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
{/* 无数据源时引导用户前往日志库配置 // Logan.Li */}
|
||||
{sources.length === 0 && (
|
||||
<div className="p-3 rounded border text-xs" style={{ background: 'rgba(245,158,11,0.1)', borderColor: 'rgba(245,158,11,0.3)', color: '#f59e0b' }}>
|
||||
暂无{meta.name}数据源,请先前往「日志库」添加数据源。
|
||||
{t('monitor.logPanel.noDatasourceHint', { name: meta.name })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -582,7 +584,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--dg-border)" vertical={false} />
|
||||
<XAxis dataKey="timestamp" tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} axisLine={{ stroke: 'var(--dg-border)' }} tickLine={false} tickFormatter={formatHistogramTick} interval="preserveStartEnd" />
|
||||
<YAxis tick={{ fontSize: 9, fill: 'var(--dg-text-muted)' }} axisLine={false} tickLine={false} width={36} tickFormatter={formatHistogramCount} />
|
||||
<Tooltip contentStyle={{ background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', borderRadius: '6px', fontSize: '11px', color: 'var(--dg-text-primary)' }} formatter={(v: number) => [`${v} 条日志`, '数量']} labelFormatter={(l: string) => `时间: ${l}`} />
|
||||
<Tooltip contentStyle={{ background: 'var(--dg-bg-secondary)', border: '1px solid var(--dg-border)', borderRadius: '6px', fontSize: '11px', color: 'var(--dg-text-primary)' }} formatter={(v: number) => [`${v} ${t('monitor.logPanel.chartCountSuffix')}`, t('monitor.logPanel.chartCountLabel')]} labelFormatter={(l: string) => `${t('monitor.logPanel.chartTimePrefix')} ${l}`} />
|
||||
<Area type="monotone" dataKey="count" stroke={meta.color} fill={`url(#vol-${effEngine})`} strokeWidth={1.5} dot={false} activeDot={{ r: 4, stroke: meta.color, strokeWidth: 2, fill: 'var(--dg-bg-secondary)' }} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -594,7 +596,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
<div className="rounded-lg border" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
|
||||
<div className="flex items-center gap-0.5 px-2 pt-1.5" style={{ borderBottom: '1px solid var(--dg-border)' }}>
|
||||
{(['logs', 'stats', 'cluster'] as ResultTab[]).map(tab => {
|
||||
const labels: Record<ResultTab, string> = { logs: '原始日志', stats: '统计图表', cluster: '日志聚类' };
|
||||
const labels: Record<ResultTab, string> = { logs: t('monitor.logPanel.tabLogs'), stats: t('monitor.logPanel.tabStats'), cluster: t('monitor.logPanel.tabCluster') };
|
||||
return (
|
||||
<button key={tab} onClick={() => setResultTab(tab)} className="px-3 py-1.5 text-[11px] font-medium rounded-t-md transition-colors"
|
||||
style={{ color: resultTab === tab ? meta.color : 'var(--dg-text-muted)', background: resultTab === tab ? 'var(--dg-bg-secondary)' : 'transparent', borderBottom: resultTab === tab ? `2px solid ${meta.color}` : '2px solid transparent', marginBottom: '-1px' }}>
|
||||
@@ -616,12 +618,12 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
<div className="flex rounded border overflow-hidden" style={{ borderColor: 'var(--dg-border)' }}>
|
||||
{(['raw', 'field', 'table'] as const).map(m => (
|
||||
<button key={m} onClick={() => setViewMode(m)} className="px-3 py-1 text-[11px] transition-colors" style={{ background: viewMode === m ? meta.color : 'var(--dg-bg-tertiary)', color: viewMode === m ? '#fff' : 'var(--dg-text-muted)' }}>
|
||||
{m === 'raw' ? '原始日志' : m === 'field' ? '字段视图' : '表格视图'}
|
||||
{m === 'raw' ? t('monitor.logPanel.viewRaw') : m === 'field' ? t('monitor.logPanel.viewField') : t('monitor.logPanel.viewTable')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<label className="flex items-center gap-1.5 text-[11px] cursor-pointer" style={{ color: 'var(--dg-text-muted)' }} title="开启后折叠行仅显示前 500 字预览"><input type="checkbox" checked={truncate} onChange={e => setTruncate(e.target.checked)} className="h-3.5 w-3.5 rounded" /> 截断预览</label>
|
||||
<label className="flex items-center gap-1.5 text-[11px] cursor-pointer" style={{ color: 'var(--dg-text-muted)' }} title="长日志自动换行显示"><input type="checkbox" checked={wrapLines} onChange={e => setWrapLines(e.target.checked)} className="h-3.5 w-3.5 rounded" /> 换行</label>
|
||||
<label className="flex items-center gap-1.5 text-[11px] cursor-pointer" style={{ color: 'var(--dg-text-muted)' }} title={t('monitor.logPanel.truncateTitle')}><input type="checkbox" checked={truncate} onChange={e => setTruncate(e.target.checked)} className="h-3.5 w-3.5 rounded" /> {t('monitor.logPanel.truncate')}</label>
|
||||
<label className="flex items-center gap-1.5 text-[11px] cursor-pointer" style={{ color: 'var(--dg-text-muted)' }} title={t('monitor.logPanel.wrapLinesTitle')}><input type="checkbox" checked={wrapLines} onChange={e => setWrapLines(e.target.checked)} className="h-3.5 w-3.5 rounded" /> {t('monitor.logPanel.wrapLines')}</label>
|
||||
</div>
|
||||
</>)}
|
||||
{resultTab === 'stats' && <StatChartPanel statTable={statTable} engine={effEngine as LogEngine} query={query} onSaveAsAlert={() => setSaveAlertOpen(true)} onSaveAsQuery={() => setSaveQueryOpen(true)} />}
|
||||
@@ -633,7 +635,7 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
{/* 空状态:从未查询 vs 查询返回 0 结果 */}
|
||||
{!loading && entries.length === 0 && !hasQueried && (
|
||||
<div className="border rounded-lg flex flex-col items-center justify-center gap-2" style={{ borderColor: 'var(--dg-border)', minHeight: '300px' }}>
|
||||
<span className="text-sm" style={{ color: 'var(--dg-text-muted)' }}>输入查询语句并点击「查询」查看结果</span>
|
||||
<span className="text-sm" style={{ color: 'var(--dg-text-muted)' }}>{t('monitor.logPanel.emptyHint')}</span>
|
||||
<span className="text-xs font-mono px-3 py-1 rounded" style={{ color: 'var(--dg-text-muted)', background: 'var(--dg-bg-tertiary)' }}>{meta.queryPlaceholder}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -645,13 +647,13 @@ export function UnifiedQueryPanel({ engine, onRuleCreated }: Props) {
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>未找到匹配的日志记录</p>
|
||||
<p className="text-sm font-medium" style={{ color: 'var(--dg-text-primary)' }}>{t('monitor.logPanel.noMatch')}</p>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--dg-text-muted)' }}>
|
||||
查询已执行({queryTime}ms),但当前时间范围和筛选条件下无匹配结果
|
||||
{t('monitor.logPanel.noMatchDesc', { ms: queryTime })}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={handleRun} className="px-4 py-1.5 rounded text-xs font-medium text-white transition-opacity hover:opacity-90" style={{ background: '#22c55e' }}>
|
||||
<Play className="h-3.5 w-3.5 inline mr-1" />重新查询
|
||||
<Play className="h-3.5 w-3.5 inline mr-1" />{t('monitor.logPanel.requeries')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user