refactor(frontend): api/components/pages 目录架构统一对齐菜单域 + 日志中心修复

1. 目录架构统一(API = components = 菜单域,一域一目录 + barrel)
   - log/ 提为独立顶层域(日志中心在菜单中与监控中心平级,不再塞在 monitor/)
   - task/ 拆分原 automation.ts → cron-job / script / remote-exec
   - aiops/ 合并 ai.ts + agentCli.ts → assistant + agent-cli
   - agent/ approval/ audit/ 提升为域目录
   - auth 拆分:authApi(认证自服务) + system/user.ts(userApi+ldapApi 管理端)
   - components/automation → components/task,菜单"自动化运维"→"作业任务"
   - pages/ 按域分子目录(approval/audit/monitor),删孤儿 AuditPage.tsx
   - 所有 API 符号名不变,组件经 @/api 聚合引用,运行时行为零变化

2. Loki 查询无返回修复:aiops 容器 DNS 解析外网域名失败
   (宿主机 systemd-resolved 转发链路异常),docker-compose 显式配 dns 上游

3. ES 查询框布局:工具栏单行挤 7 控件(ES 多 150px 索引框),拆两行——
   查询输入独占一行(+查询按钮),次级控件换行 flex-wrap

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
attackerandClaude committed 2026-06-28 06:06:48 +00:00
1 parent da01cbb119
commit c0ee8e4ddd
74 files changed
+229 -216

No files matched your search

@@ -0,0 +1,302 @@
// 中间件监控 — Redis / MySQL 指标采集(实时 API 数据)
import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Database, Server, Activity, Loader2, Circle } from 'lucide-react';
import { metricsApi } from '@/api';
import type { JsonData } from '@/types/common';
interface TargetInfo {
id: number;
name: string;
type: string;
ip: string;
status: string;
latest_metrics: Record<string, number>;
}
interface TargetsResponse {
list: TargetInfo[];
total: number;
}
/** 格式化内存值 (bytes → human readable) */
function fmtMemory(bytes: number): string {
if (bytes == null || bytes === 0) return '--';
if (bytes > 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
if (bytes > 1e6) return `${(bytes / 1e6).toFixed(1)} MB`;
if (bytes > 1e3) return `${(bytes / 1e3).toFixed(1)} KB`;
return `${bytes} B`;
}
/** 格式化命中率 (0-1 float → %) */
function fmtHitRate(v: number): string {
if (v == null || Number.isNaN(v)) return '--';
return `${(v * 100).toFixed(1)}%`;
}
/** 格式化数值 */
function fmtVal(v: number, precision = 0): string {
if (v == null || Number.isNaN(v)) return '--';
return v.toFixed(precision);
}
/** 状态指示器颜色 */
function statusColor(status: string): string {
switch (status) {
case 'online': return 'text-emerald-400';
case 'warning': return 'text-amber-400';
case 'error': return 'text-red-400';
default: return 'text-slate-500';
}
}
function statusBg(status: string): string {
switch (status) {
case 'online': return 'bg-emerald-400';
case 'warning': return 'bg-amber-400';
case 'error': return 'bg-red-400';
default: return 'bg-slate-500';
}
}
export function MiddlewareMonitor() {
const [targets, setTargets] = useState<TargetInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const loadTargets = useCallback(async () => {
try {
setLoading(true);
setError('');
const data = await metricsApi.getTargets({ type: 'middleware', page_size: 200 }) as TargetsResponse;
setTargets(data?.list || []);
} catch (err) {
setError((err as JsonData).message || '加载失败');
setTargets([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { loadTargets(); }, []);
// 按类型分组
const redisTargets = targets.filter(t => t.type === 'redis' || t.name?.toLowerCase().includes('redis'));
const mysqlTargets = targets.filter(t => t.type === 'mysql' || t.name?.toLowerCase().includes('mysql'));
// 聚合 Redis 指标 (取第一个在线实例)
const redisTarget = redisTargets.find(t => t.status === 'online') || redisTargets[0];
const redisMetrics = redisTarget?.latest_metrics || {};
// 聚合 MySQL 指标
const mysqlTarget = mysqlTargets.find(t => t.status === 'online') || mysqlTargets[0];
const mysqlMetrics = mysqlTarget?.latest_metrics || {};
return (
<div className="space-y-6">
{loading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-slate-500 mr-2" />
<span className="text-sm text-[var(--dg-text-muted)]">正在加载中间件指标...</span>
</div>
)}
{error && !loading && (
<Card className="bg-[var(--dg-bg-secondary)] border-red-500/30">
<CardContent className="p-6 text-center">
<Activity className="h-8 w-8 text-red-400 mx-auto mb-2" />
<p className="text-sm text-red-400">{error}</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-2">请确认后端服务可用且已配置中间件采集</p>
</CardContent>
</Card>
)}
{!loading && !error && targets.length === 0 && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-12 text-center">
<Database className="h-12 w-12 text-slate-600 mx-auto mb-4" />
<p className="text-sm text-[var(--dg-text-muted)]">暂无中间件监控目标</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-2">需在 Agent 配置 Redis / MySQL 连接信息后启用采集</p>
</CardContent>
</Card>
)}
{/* Redis 卡片 */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-4">
<Database className="h-4 w-4 text-red-400" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">Redis 监控</h3>
{redisTarget && (
<Badge variant="outline" className="ml-auto text-[10px] border-[var(--dg-border)] text-[var(--dg-text-muted)]">
{redisTarget.name} ({redisTarget.ip})
</Badge>
)}
</div>
{!redisTarget ? (
<p className="text-sm text-[var(--dg-text-muted)] mb-4">需在 Agent 配置 Redis 连接地址和密码后启用采集。</p>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<MetricTile
label="连接数"
value={fmtVal(redisMetrics.redis_connected_clients)}
/>
<MetricTile
label="内存"
value={fmtMemory(redisMetrics.redis_used_memory)}
/>
<MetricTile
label="命中率"
value={fmtHitRate(redisMetrics.redis_hit_rate)}
/>
<MetricTile
label="QPS"
value={fmtVal(redisMetrics.redis_qps, 0)}
/>
</div>
)}
</CardContent>
</Card>
{/* MySQL 卡片 */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-4">
<Server className="h-4 w-4 text-blue-400" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">MySQL 监控</h3>
{mysqlTarget && (
<Badge variant="outline" className="ml-auto text-[10px] border-[var(--dg-border)] text-[var(--dg-text-muted)]">
{mysqlTarget.name} ({mysqlTarget.ip})
</Badge>
)}
</div>
{!mysqlTarget ? (
<p className="text-sm text-[var(--dg-text-muted)] mb-4">需在 Agent 配置 MySQL 连接地址、用户名和密码后启用采集。</p>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<MetricTile
label="连接数"
value={fmtVal(mysqlMetrics.mysql_connections)}
/>
<MetricTile
label="QPS"
value={fmtVal(mysqlMetrics.mysql_qps, 1)}
/>
<MetricTile
label="慢查询"
value={fmtVal(mysqlMetrics.mysql_slow_queries)}
/>
<MetricTile
label="缓冲池命中率"
value={fmtHitRate(mysqlMetrics.mysql_buffer_hit_rate)}
/>
</div>
)}
</CardContent>
</Card>
{/* 中间件实例列表 */}
{targets.length > 0 && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-4">
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">
中间件实例({targets.length})
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{targets.map(target => (
<InstanceCard key={target.id} target={target} />
))}
</div>
</CardContent>
</Card>
)}
{/* 其他中间件类型的扩展指标 */}
{targets.filter(t => !redisTargets.includes(t) && !mysqlTargets.includes(t)).length > 0 && (
<div className="space-y-4">
{targets
.filter(t => !redisTargets.includes(t) && !mysqlTargets.includes(t))
.map(target => (
<Card key={target.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-center gap-2 mb-3">
<Circle className={`h-3 w-3 ${statusColor(target.status)}`} />
<h3 className="text-sm font-medium text-[var(--dg-text-primary)] capitalize">{target.type}</h3>
<Badge variant="outline" className="text-[10px] border-[var(--dg-border)] text-[var(--dg-text-muted)]">
{target.name} ({target.ip})
</Badge>
</div>
{Object.keys(target.latest_metrics || {}).length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{Object.entries(target.latest_metrics).map(([key, val]) => (
<MetricTile key={key} label={key} value={fmtVal(val, 1)} />
))}
</div>
) : (
<p className="text-xs text-[var(--dg-text-muted)]">暂无指标数据</p>
)}
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}
/** 指标小方块 */
function MetricTile({ label, value }: { label: string; value: string }) {
return (
<div className="bg-[var(--dg-bg-tertiary)] rounded-lg p-3 text-center">
<p className="text-xs text-[var(--dg-text-muted)]">{label}</p>
<p className="text-lg font-bold text-[var(--dg-text-primary)]">{value}</p>
</div>
);
}
/** 实例状态卡片 */
function InstanceCard({ target }: { target: TargetInfo }) {
const metrics = target.latest_metrics || {};
const metricCount = Object.keys(metrics).length;
return (
<div className="bg-[var(--dg-bg-tertiary)] rounded-lg p-3 border border-[var(--dg-border)] hover:border-[var(--dg-border)]/80 transition-colors">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 min-w-0">
<span className={`inline-block w-2 h-2 rounded-full ${statusBg(target.status)}`} />
<span className="text-sm font-medium text-[var(--dg-text-primary)] truncate">{target.name}</span>
</div>
<Badge
variant="outline"
className={`text-[10px] capitalize ${target.status === 'online' ? 'border-emerald-400/50 text-emerald-400' : 'border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
>
{target.status}
</Badge>
</div>
<div className="text-xs text-[var(--dg-text-muted)] mb-2">
{target.ip} · {target.type}
</div>
{metricCount > 0 ? (
<div className="flex flex-wrap gap-1.5">
{Object.entries(metrics).slice(0, 4).map(([key, val]) => (
<span key={key} className="inline-flex items-center gap-1 text-[10px] bg-[var(--dg-bg-primary)] rounded px-1.5 py-0.5 text-[var(--dg-text-secondary)]">
<span className="text-[var(--dg-text-muted)]">{key}</span>
<span className="font-mono font-medium text-[var(--dg-text-primary)]">
{typeof val === 'number' ? val.toFixed(1) : val}
</span>
</span>
))}
{metricCount > 4 && (
<span className="text-[10px] text-[var(--dg-text-muted)]">+{metricCount - 4} 项</span>
)}
</div>
) : (
<p className="text-[10px] text-[var(--dg-text-muted)]">暂无指标</p>
)}
</div>
);
}