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