Files
aiops/frontend/src/components/agent/HostDetailDialog.tsx
T
attacker 39a4432f79 refactor(i18n): U43 深度残余硬编码中文 UI 文本清零 — 17 文件 ~40 处→0
趋势标签(上升/下降/平稳) + K8s 卡片标题(Pod级别/安全上下文/实例/详情) + data-table 默认值(搜索/暂无数据) + Deployment 编辑器标签(数据卷/挂载/只读写/manifest提示/镜像拉取凭证) + HostSettings 4 个命名空间描述 + 7 个 K8s 页面刷新按钮 + 资源编辑器新建/编辑 + Agent 执行命令描述 + 账单保存成功 toast + 容量预警标题

新增 common 30 语义化翻译键, zh/en 7898→7928 对齐
tsc -b=0, npm build✓ (16.55s), ESLint=0/43/any=0, go build=0, 17/17 测试通过
2026-07-14 05:27:49 +08:00

205 lines
8.8 KiB
TypeScript

// 主机详情弹窗:展示历史指标趋势图 + 端口发现列表(从 AgentMonitor.tsx 提取) // Logan.Li
import { useState, useEffect } from 'react';
import { agentApi } from '@/api';
import { isOnline, timeAgo, asList } from './agentUtils';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Server, Activity, Network, Terminal, Loader2 } from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
export function HostDetailDialog({
host,
onClose,
onCommand,
}: {
host?: JsonData;
onClose: () => void;
onCommand: (hostname: string) => void;
}) {
const [history, setHistory] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(false);
const [hours, setHours] = useState(1);
useEffect(() => {
if (!host) return;
let active = true;
setLoading(true);
agentApi
.getHistory(host.hostname, { hours })
.then((data) => {
if (active) setHistory(asList(data));
})
.catch(() => { if (active) setHistory([]); })
.finally(() => { if (active) setLoading(false); });
return () => { active = false; };
}, [host, hours]);
if (!host) return null;
const online = isOnline(host.last_report || host.last_reported_at || host.updated_at);
const ports: JsonData[] = Array.isArray(host.ports) ? host.ports : [];
const chartData = history.map((h) => ({
// eslint-disable-next-line react-hooks/purity
time: new Date(h.timestamp || h.time || h.created_at || Date.now()).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
cpu: Number(h.cpu_percent ?? h.cpu ?? 0),
mem: Number(h.mem_percent ?? h.mem ?? 0),
disk: Number(h.disk_percent ?? h.disk ?? 0),
}));
return (
<Dialog open={!!host} onOpenChange={(v) => { if (!v) onClose(); }}>
<DialogContent className="max-w-3xl dg-card text-[var(--dg-text-primary)]">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
<Server className="h-5 w-5 dg-text-blue" />
{host.hostname}
{online ? (
<Badge className="bg-emerald-500/15 dg-text-green border border-emerald-500/30"> {i18n.t('agent.agentOnline')} </Badge>
) : (
<Badge className="bg-red-500/15 dg-text-red border border-red-500/30"> {i18n.t('agent.agentOffline')} </Badge>
)}
</DialogTitle>
<DialogDescription className="sr-only"> {i18n.t('agent.hostDetailDialog.desc')} </DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 基本信息 */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<InfoCell label={i18n.t('agent.agentIp')} value={host.ip || '--'} />
<InfoCell label={i18n.t('agent.agentOs')} value={host.os || host.platform || '--'} />
<InfoCell label={i18n.t('agent.agentLastSeen')} value={timeAgo(host.last_report || host.last_reported_at || host.updated_at)} />
<InfoCell label={i18n.t('agent.portCount')} value={String(ports.length)} />
</div>
{/* 趋势图 */}
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-[var(--dg-text-primary)] flex items-center gap-1.5">
<Activity className="h-4 w-4 dg-text-blue" />
{i18n.t('common.metricTrend')}
</span>
<Select value={String(hours)} onValueChange={(v) => setHours(Number(v))}>
<SelectTrigger size="sm" className="h-7 w-28 dg-input text-[var(--dg-text-secondary)] text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent className="dg-input">
<SelectItem value="1"> {i18n.t('agent.hostDetailDialog.last1h')} </SelectItem>
<SelectItem value="6"> {i18n.t('agent.hostDetailDialog.last6h')} </SelectItem>
<SelectItem value="24"> {i18n.t('agent.hostDetailDialog.last24h')} </SelectItem>
</SelectContent>
</Select>
</div>
<div className="h-56 rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3">
{loading ? (
<div className="h-full flex items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin dg-text-muted" />
</div>
) : chartData.length === 0 ? (
<div className="h-full flex items-center justify-center text-sm dg-text-muted"> {i18n.t('agent.hostDetailDialog.noHistoryData')} </div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 5, right: 5, bottom: 0, left: -20 }}>
<defs>
<linearGradient id="gCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.4} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
<linearGradient id="gMem" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.4} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
<linearGradient id="gDisk" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.4} />
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#475569" fontSize={11} />
<YAxis stroke="#475569" fontSize={11} domain={[0, 100]} />
<Tooltip
contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '6px', color: '#f8fafc', fontSize: '12px' }}
/>
<Area type="monotone" dataKey="cpu" stroke="#3b82f6" fill="url(#gCpu)" name="CPU" />
<Area type="monotone" dataKey="mem" stroke="#10b981" fill="url(#gMem)" name={i18n.t('kubernetes.pod.memory')} />
<Area type="monotone" dataKey="disk" stroke="#f59e0b" fill="url(#gDisk)" name={i18n.t('monitor.dashboard.col.disk')} />
</AreaChart>
</ResponsiveContainer>
)}
</div>
</div>
{/* 端口列表 */}
<div>
<span className="text-sm font-medium text-[var(--dg-text-primary)] flex items-center gap-1.5 mb-2">
<Network className="h-4 w-4 dg-text-blue" />
{i18n.t('common.portDiscovery', { count: ports.length })}
</span>
{ports.length === 0 ? (
<p className="text-xs dg-text-muted"> {i18n.t('agent.hostDetailDialog.noPortData')} </p>
) : (
<div className="flex flex-wrap gap-1.5 max-h-32 overflow-y-auto">
{ports.map((p, idx) => (
<Badge key={idx} className="bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] border border-[var(--dg-border)]">
<Network className="h-3 w-3 mr-1" />
{p.port ?? p}
</Badge>
))}
</div>
)}
</div>
</div>
<DialogFooter className="gap-2">
<Button
onClick={() => onCommand(host.hostname)}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
<Terminal className="h-4 w-4 mr-1.5" />
{i18n.t('common.remoteCommand')}
</Button>
<Button variant="outline" onClick={onClose} className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">
{i18n.t('common.close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function InfoCell({ label, value }: { label: string; value: string }) {
return (
<div className="bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-lg px-3 py-2">
<div className="text-xs dg-text-muted">{label}</div>
<div className="text-sm text-[var(--dg-text-primary)] font-medium truncate">{value}</div>
</div>
);
}
export default HostDetailDialog;