feat: 全站 PageShell 设计系统与专业 UI 统一
- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
This commit is contained in:
1 parent
1acb0c49af
commit
e363ded445
213 files changed
+7148
-6818
No files matched your search
@@ -8,11 +8,13 @@ import { useHostList, useHostHistory, usePortPolicy } from '@/hooks/api/useHostM
|
||||
import { queryKeys } from '@/hooks/api/queryKeys';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageLoading } from '@/components/layout/PageLoading';
|
||||
import {
|
||||
Cpu, HardDrive, Network, Activity, Server, Loader2,
|
||||
Cpu, HardDrive, Network, Activity, Server,
|
||||
Info, Monitor, Disc, Clock, List, HeartPulse, RefreshCw, XCircle,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
@@ -27,7 +29,7 @@ import { PortPolicyDialog } from './host-monitor/PortPolicyDialog';
|
||||
import { SystemInfoSection } from './host-monitor/SystemInfoSection';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
export function HostMonitor() {
|
||||
export function HostMonitor({ embedded = false }: { embedded?: boolean }) {
|
||||
const qc = useQueryClient();
|
||||
const [searchParams] = useSearchParams();
|
||||
const urlHost = searchParams.get('host') || '';
|
||||
@@ -95,111 +97,108 @@ export function HostMonitor() {
|
||||
const currentVal = (key: string, fallback = '--') =>
|
||||
getCurrentVal(latest, netInfo, diskIOInfo, swapInfo, fdInfo, key, fallback);
|
||||
|
||||
if (loading && !history.length) {
|
||||
return <div className="flex items-center justify-center h-64"><Loader2 className="h-8 w-8 animate-spin text-slate-500" /></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-blue-400" />
|
||||
<Select value={selectedHost} onValueChange={setSelectedHost}>
|
||||
<SelectTrigger className="w-52 h-8 text-xs bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<SelectValue placeholder="选择主机..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
|
||||
{hosts.map((h: JsonData) => (
|
||||
<SelectItem key={h.hostname} value={h.hostname}>{h.hostname}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{serverInfo && (
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-2">
|
||||
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} 核` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={aggType} onValueChange={(v) => setAggType(v as AggType)}>
|
||||
<SelectTrigger className="w-24 h-7 text-xs bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
|
||||
<SelectItem value="avg">平均值</SelectItem>
|
||||
<SelectItem value="max">最大值</SelectItem>
|
||||
<SelectItem value="min">最小值</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-1">
|
||||
{TIME_RANGES.map(tr => (
|
||||
<Button key={tr.hours} variant={rangeHours === tr.hours ? 'default' : 'outline'} size="sm"
|
||||
className="h-7 text-xs px-2.5" onClick={() => setRangeHours(tr.hours)}>{tr.label}</Button>
|
||||
const toolbar = (
|
||||
<div className="flex items-center justify-between flex-wrap gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={selectedHost} onValueChange={setSelectedHost}>
|
||||
<SelectTrigger className="w-52 h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择主机..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{hosts.map((h: JsonData) => (
|
||||
<SelectItem key={h.hostname} value={h.hostname}>{h.hostname}</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{serverInfo && (
|
||||
<span className="text-xs dg-text-muted">
|
||||
{String(serverInfo.os || '')}{serverInfo.cpu_cores ? ` | ${serverInfo.cpu_cores as number | string} 核` : ''}{serverInfo.mem_total ? ` | ${(serverInfo.mem_total as number).toFixed(0)}GB` : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={aggType} onValueChange={(v) => setAggType(v as AggType)}>
|
||||
<SelectTrigger className="w-24 h-7 text-xs dg-input">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="avg">平均值</SelectItem>
|
||||
<SelectItem value="max">最大值</SelectItem>
|
||||
<SelectItem value="min">最小值</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-1">
|
||||
{TIME_RANGES.map(tr => (
|
||||
<Button key={tr.hours} variant={rangeHours === tr.hours ? 'default' : 'outline'} size="sm"
|
||||
className={`h-7 text-xs px-2.5 ${rangeHours === tr.hours ? 'dg-btn-primary' : 'dg-btn-secondary'}`}
|
||||
onClick={() => setRangeHours(tr.hours)}>{tr.label}</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="h-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] gap-0 overflow-x-auto">
|
||||
{[
|
||||
{ id: 'basic', label: '基础监控', icon: <Monitor className="w-3 h-3" /> },
|
||||
{ id: 'disk', label: '磁盘监控', icon: <HardDrive className="w-3 h-3" /> },
|
||||
{ id: 'network', label: '网络监控', icon: <Network className="w-3 h-3" /> },
|
||||
{ id: 'ports', label: '端口监控', icon: <Server className="w-3 h-3" /> },
|
||||
{ id: 'process', label: '进程监控', icon: <List className="w-3 h-3" /> },
|
||||
{ id: 'system', label: '系统信息', icon: <Info className="w-3 h-3" /> },
|
||||
{ id: 'health_probes', label: '健康探针', icon: <HeartPulse className="w-3 h-3" /> },
|
||||
].map(tab => (
|
||||
<TabsTrigger key={tab.id} value={tab.id} className="text-[11px] h-7 gap-1 data-[state=active]:bg-[var(--dg-bg-primary)]">
|
||||
{tab.icon}{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
const monitorBody = (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="h-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] gap-0 overflow-x-auto">
|
||||
{[
|
||||
{ id: 'basic', label: '基础监控', icon: <Monitor className="w-3 h-3" /> },
|
||||
{ id: 'disk', label: '磁盘监控', icon: <HardDrive className="w-3 h-3" /> },
|
||||
{ id: 'network', label: '网络监控', icon: <Network className="w-3 h-3" /> },
|
||||
{ id: 'ports', label: '端口监控', icon: <Server className="w-3 h-3" /> },
|
||||
{ id: 'process', label: '进程监控', icon: <List className="w-3 h-3" /> },
|
||||
{ id: 'system', label: '系统信息', icon: <Info className="w-3 h-3" /> },
|
||||
{ id: 'health_probes', label: '健康探针', icon: <HeartPulse className="w-3 h-3" /> },
|
||||
].map(tab => (
|
||||
<TabsTrigger key={tab.id} value={tab.id} className="text-[11px] h-7 gap-1 data-[state=active]:bg-[var(--dg-bg-primary)]">
|
||||
{tab.icon}{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 text-blue-400" />}
|
||||
<TabsContent value="basic" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<MetricChartCard title="CPU 使用率(%)" value={currentVal('cpu')} icon={<Cpu className="h-4 w-4 dg-text-blue" />}
|
||||
data={metrics.cpu} color="#3b82f6" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock className="h-4 w-4 text-orange-400" />}
|
||||
<MetricChartCard title="CPU IO等待(%)" value={latest?.cpu_iowait != null ? `${(latest.cpu_iowait as number).toFixed(1)}%` : '--'} icon={<Clock className="h-4 w-4 dg-text-orange" />}
|
||||
data={metrics.iowait} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||
<MetricChartCard title="内存使用率(%)" value={currentVal('mem')} icon={<Server className="h-4 w-4 dg-text-green" />}
|
||||
data={metrics.mem} color="#10b981" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server className="h-4 w-4 text-emerald-400" />}
|
||||
<MetricChartCard title="可用内存(GB)" value={latest?.mem_available != null ? `${(latest.mem_available as number).toFixed(1)}GB` : '--'} icon={<Server className="h-4 w-4 dg-text-green" />}
|
||||
data={metrics.memAvail} color="#34d399" unit="GB" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive className="h-4 w-4 text-red-400" />}
|
||||
<MetricChartCard title="Swap 使用率(%)" value={currentVal('swap')} icon={<HardDrive className="h-4 w-4 dg-text-red" />}
|
||||
data={metrics.swap} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 text-purple-400" />}
|
||||
<MetricChartCard title="系统负载(1m)" value={currentVal('load1')} icon={<Activity className="h-4 w-4 dg-text-purple" />}
|
||||
data={metrics.load1} color="#8b5cf6" unit="" domain={[0, 'auto']} aggType={aggType} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="disk" className="mt-3 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<MetricChartCard title="磁盘使用率(%)" value={currentVal('disk')} icon={<HardDrive className="h-4 w-4 text-orange-400" />}
|
||||
<MetricChartCard title="磁盘使用率(%)" value={currentVal('disk')} icon={<HardDrive className="h-4 w-4 dg-text-orange" />}
|
||||
data={metrics.disk} color="#f97316" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="Inode 使用率(%)" value={(() => {
|
||||
const dArr = Array.isArray(parseJSON(latest?.disk_info)) ? parseJSON(latest?.disk_info) : [];
|
||||
const root = dArr.find((d: JsonData) => d.mount === '/') || dArr[0];
|
||||
return root?.inode_usage_pct != null ? `${root.inode_usage_pct.toFixed(1)}%` : '--';
|
||||
})()} icon={<HardDrive className="h-4 w-4 text-purple-400" />}
|
||||
})()} icon={<HardDrive className="h-4 w-4 dg-text-purple" />}
|
||||
data={metrics.inode} color="#8b5cf6" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘读取(KB/s)" value={currentVal('ioR')} icon={<Disc className="h-4 w-4 text-blue-400" />}
|
||||
<MetricChartCard title="磁盘读取(KB/s)" value={currentVal('ioR')} icon={<Disc className="h-4 w-4 dg-text-blue" />}
|
||||
data={metrics.ioRead} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 text-emerald-400" />}
|
||||
<MetricChartCard title="磁盘写入(KB/s)" value={currentVal('ioW')} icon={<Disc className="h-4 w-4 dg-text-green" />}
|
||||
data={metrics.ioWrite} color="#10b981" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc className="h-4 w-4 text-red-400" />}
|
||||
<MetricChartCard title="磁盘IO繁忙度(%)" value={diskIOInfo?.util != null ? `${diskIOInfo.util.toFixed(1)}%` : '--'} icon={<Disc className="h-4 w-4 dg-text-red" />}
|
||||
data={metrics.ioUtil} color="#ef4444" unit="%" domain={[0, 100]} aggType={aggType} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="network" className="mt-3 space-y-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network className="h-4 w-4 text-purple-400" />}
|
||||
<MetricChartCard title="TCP EST" value={currentVal('tcp')} icon={<Network className="h-4 w-4 dg-text-purple" />}
|
||||
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
|
||||
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 dg-text-yellow" />}
|
||||
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP CLOSE_WAIT" value={netInfo?.tcp_close_wait != null ? String(netInfo.tcp_close_wait) : '--'} icon={<Clock className="h-4 w-4 text-red-400" />}
|
||||
<MetricChartCard title="TCP CLOSE_WAIT" value={netInfo?.tcp_close_wait != null ? String(netInfo.tcp_close_wait) : '--'} icon={<Clock className="h-4 w-4 dg-text-red" />}
|
||||
data={metrics.tcpCW} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="TCP 总连接" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network className="h-4 w-4 text-cyan-400" />}
|
||||
<MetricChartCard title="TCP 总连接" value={netInfo ? String((netInfo.tcp_established||0)+(netInfo.tcp_time_wait||0)+(netInfo.tcp_close_wait||0)) : '--'} icon={<Network className="h-4 w-4 dg-text-cyan" />}
|
||||
data={metrics.connTotal} color="#22d3ee" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
</div>
|
||||
<DualLineChart
|
||||
@@ -210,9 +209,9 @@ export function HostMonitor() {
|
||||
aggType={aggType} unit="KB/s"
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||
<MetricChartCard title="网卡接收错误" value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-red-400" />}
|
||||
<MetricChartCard title="网卡接收错误" value={netInfo?.net_rx_errors != null ? String(netInfo.net_rx_errors) : '--'} icon={<XCircle className="h-4 w-4 dg-text-red" />}
|
||||
data={metrics.rxErr} color="#ef4444" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
<MetricChartCard title="网卡发送错误" value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle className="h-4 w-4 text-orange-400" />}
|
||||
<MetricChartCard title="网卡发送错误" value={netInfo?.net_tx_errors != null ? String(netInfo.net_tx_errors) : '--'} icon={<XCircle className="h-4 w-4 dg-text-orange" />}
|
||||
data={metrics.txErr} color="#f97316" unit="个" domain={[0, 'auto']} aggType={aggType} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -221,15 +220,15 @@ export function HostMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />监听端口 {portInfo.length}
|
||||
<Server className="h-4 w-4 dg-text-green" />监听端口 {portInfo.length}
|
||||
<span className="ml-auto flex items-center gap-2">
|
||||
{portPolicyMode === 'blacklist' && portWhitelist.length > 0 && (
|
||||
<Badge variant="outline" className="text-[10px] text-amber-400 border-amber-500/30">
|
||||
<Badge variant="outline" className="text-[10px] dg-text-yellow border-amber-500/30">
|
||||
黑名单: {portWhitelist.join(',')}
|
||||
</Badge>
|
||||
)}
|
||||
{portPolicyMode === 'whitelist' && (
|
||||
<Badge variant="outline" className="text-[10px] text-blue-400 border-blue-500/30">
|
||||
<Badge variant="outline" className="text-[10px] dg-text-blue border-blue-500/30">
|
||||
白名单模式
|
||||
</Badge>
|
||||
)}
|
||||
@@ -256,7 +255,7 @@ export function HostMonitor() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{portInfo.map((p: JsonData, i: number) => (
|
||||
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-slate-800/30 transition-colors">
|
||||
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-[var(--dg-bg-hover)] transition-colors">
|
||||
<td className="py-1.5 font-mono text-[var(--dg-text-primary)]">{p.port}</td>
|
||||
<td className="py-1.5">
|
||||
<Badge variant="outline" className="text-[10px] font-mono">
|
||||
@@ -265,7 +264,7 @@ export function HostMonitor() {
|
||||
</td>
|
||||
<td className="py-1.5 font-mono text-[var(--dg-text-muted)]">{p.process || '—'}</td>
|
||||
<td className="py-1.5 text-right">
|
||||
<Badge variant="default" className="text-[10px] bg-emerald-500/20 text-emerald-400 border-emerald-500/30">
|
||||
<Badge variant="default" className="text-[10px] bg-emerald-500/20 dg-text-green border-emerald-500/30">
|
||||
LISTEN
|
||||
</Badge>
|
||||
</td>
|
||||
@@ -283,7 +282,7 @@ export function HostMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<List className="h-4 w-4 text-cyan-400" />进程 TOP {processTop.length}
|
||||
<List className="h-4 w-4 dg-text-cyan" />进程 TOP {processTop.length}
|
||||
</h3>
|
||||
{processTop.length === 0 ? (
|
||||
<div className="text-center py-6 text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
|
||||
@@ -307,14 +306,14 @@ export function HostMonitor() {
|
||||
const cpup = p.cpu_pct != null ? Number(p.cpu_pct) : null;
|
||||
const memp = p.mem_pct != null ? Number(p.mem_pct) : null;
|
||||
return (
|
||||
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-slate-800/30 transition-colors">
|
||||
<tr key={i} className="border-b border-[var(--dg-border)]/30 hover:bg-[var(--dg-bg-hover)] transition-colors">
|
||||
<td className="py-1.5 pr-2">
|
||||
<div className="flex items-center gap-1 group/cmd">
|
||||
<span className="font-mono text-[var(--dg-text-primary)] truncate max-w-[320px] inline-block align-middle" title={cmd}>
|
||||
{cmd}
|
||||
</span>
|
||||
<button
|
||||
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:text-cyan-400 shrink-0"
|
||||
className="opacity-0 group-hover/cmd:opacity-100 transition-opacity p-0.5 hover:dg-text-cyan shrink-0"
|
||||
title="复制命令行"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -337,7 +336,7 @@ export function HostMonitor() {
|
||||
</td>
|
||||
<td className="py-1.5 text-right font-mono text-[var(--dg-text-muted)]">{p.pid}</td>
|
||||
<td className="py-1.5 text-right font-mono">
|
||||
<span className={memp != null && memp > 50 ? 'text-rose-400' : 'text-cyan-400'}>
|
||||
<span className={memp != null && memp > 50 ? 'text-rose-400' : 'dg-text-cyan'}>
|
||||
{memp != null ? memp.toFixed(2) : '--'}
|
||||
</span>
|
||||
</td>
|
||||
@@ -361,7 +360,7 @@ export function HostMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-violet-400" />FD 文件句柄
|
||||
<HardDrive className="h-4 w-4 dg-text-purple" />FD 文件句柄
|
||||
</h3>
|
||||
{fdInfo?.allocated != null ? (
|
||||
<>
|
||||
@@ -376,7 +375,7 @@ export function HostMonitor() {
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[var(--dg-text-muted)]">使用率 </span>
|
||||
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'text-violet-400'}`}>
|
||||
<span className={`font-mono font-semibold ${fdInfo.usage_pct > 80 ? 'text-rose-400' : 'dg-text-purple'}`}>
|
||||
{fdInfo.usage_pct != null ? `${fdInfo.usage_pct.toFixed(1)}%` : '—'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -410,21 +409,21 @@ export function HostMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
|
||||
<RefreshCw className="h-4 w-4 text-emerald-400" />上下文切换
|
||||
<RefreshCw className="h-4 w-4 dg-text-green" />上下文切换
|
||||
</h3>
|
||||
{csInfo?.ctx_per_sec != null ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">CTX 切换/秒</div>
|
||||
<div className="text-lg font-bold font-mono text-emerald-400">
|
||||
<div className="text-lg font-bold font-mono dg-text-green">
|
||||
{(csInfo.ctx_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.ctx_per_sec?.toLocaleString()} 次</div>
|
||||
</div>
|
||||
<div className="bg-[var(--dg-bg-primary)] rounded-lg p-3 text-center">
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">IRQ 中断/秒</div>
|
||||
<div className="text-lg font-bold font-mono text-amber-400">
|
||||
<div className="text-lg font-bold font-mono dg-text-yellow">
|
||||
{(csInfo.irq_per_sec / 1e6).toFixed(1)}<span className="text-xs font-normal text-[var(--dg-text-muted)] ml-0.5">M</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-1 font-mono">{csInfo.irq_per_sec?.toLocaleString()} 次</div>
|
||||
@@ -432,8 +431,8 @@ export function HostMonitor() {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span className="text-emerald-400">CTX</span>
|
||||
<span className="text-amber-400">IRQ</span>
|
||||
<span className="dg-text-green">CTX</span>
|
||||
<span className="dg-text-yellow">IRQ</span>
|
||||
</div>
|
||||
<div className="h-2 bg-[var(--dg-bg-primary)] rounded-full overflow-hidden flex">
|
||||
{csInfo.ctx_per_sec + csInfo.irq_per_sec > 0 && (
|
||||
@@ -463,19 +462,46 @@ export function HostMonitor() {
|
||||
portInfoCount={portInfo.length}
|
||||
/>
|
||||
|
||||
<HealthProbeSection selectedHost={selectedHost} />
|
||||
</Tabs>
|
||||
<HealthProbeSection selectedHost={selectedHost} />
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
<PortPolicyDialog
|
||||
open={portPolicyDialogOpen}
|
||||
onOpenChange={setPortPolicyDialogOpen}
|
||||
portPolicyMode={portPolicyMode}
|
||||
onModeChange={setPortPolicyMode}
|
||||
portWhitelist={portWhitelist}
|
||||
onWhitelistChange={setPortWhitelist}
|
||||
onSave={handleSavePortPolicy}
|
||||
saving={portPolicySaving}
|
||||
/>
|
||||
</div>
|
||||
const dialogs = (
|
||||
<PortPolicyDialog
|
||||
open={portPolicyDialogOpen}
|
||||
onOpenChange={setPortPolicyDialogOpen}
|
||||
portPolicyMode={portPolicyMode}
|
||||
onModeChange={setPortPolicyMode}
|
||||
portWhitelist={portWhitelist}
|
||||
onWhitelistChange={setPortWhitelist}
|
||||
onSave={handleSavePortPolicy}
|
||||
saving={portPolicySaving}
|
||||
/>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{toolbar}
|
||||
{loading && !history.length ? <PageLoading /> : monitorBody}
|
||||
{dialogs}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title="主机监控"
|
||||
subtitle={selectedHost ? `当前主机: ${selectedHost}` : '选择主机查看监控指标'}
|
||||
icon={<Server className="h-5 w-5" />}
|
||||
toolbar={toolbar}
|
||||
loading={loading && !history.length}
|
||||
loadingLabel="加载监控数据..."
|
||||
contentClassName="space-y-3"
|
||||
>
|
||||
{monitorBody}
|
||||
{dialogs}
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
@@ -6,14 +6,10 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Activity, Search, Filter, BarChart3, Cpu, HardDrive, Network, Database, Plus } from 'lucide-react';
|
||||
import { metricsApi } from '@/api';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
import type { JsonData } from '@/types/common';
|
||||
|
||||
interface _MetricName {
|
||||
name: string;
|
||||
category: string;
|
||||
data_source: string;
|
||||
}
|
||||
|
||||
interface TargetInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -49,7 +45,6 @@ export function MetricsExplorer() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchKw, setSearchKw] = useState('');
|
||||
|
||||
// Load targets
|
||||
const loadTargets = useCallback(async () => {
|
||||
try {
|
||||
const res: JsonData = await metricsApi.getTargets({ page_size: 200 });
|
||||
@@ -58,7 +53,6 @@ export function MetricsExplorer() {
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
// Load metric names
|
||||
const loadMetricNames = useCallback(async () => {
|
||||
try {
|
||||
const names: string[] = await metricsApi.getNames();
|
||||
@@ -66,16 +60,15 @@ export function MetricsExplorer() {
|
||||
} catch { /* empty */ }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadTargets(); loadMetricNames(); }, []);
|
||||
useEffect(() => { loadTargets(); loadMetricNames(); }, [loadTargets, loadMetricNames]);
|
||||
|
||||
// Query metric data when target + metrics selected
|
||||
useEffect(() => {
|
||||
if (!selectedTarget || selectedMetrics.length === 0) return;
|
||||
const target = targets.find(t => String(t.id) === selectedTarget);
|
||||
if (!target) return;
|
||||
|
||||
const to = Math.floor(Date.now() / 1000);
|
||||
const from = to - 3600; // last hour
|
||||
const from = to - 3600;
|
||||
|
||||
Promise.all(
|
||||
selectedMetrics.map(mn =>
|
||||
@@ -88,15 +81,12 @@ export function MetricsExplorer() {
|
||||
});
|
||||
}, [selectedTarget, selectedMetrics, targets]);
|
||||
|
||||
// Filter metric names by search
|
||||
const filteredMetrics = searchKw
|
||||
? metricNames.filter(n => n.toLowerCase().includes(searchKw.toLowerCase()))
|
||||
: metricNames;
|
||||
|
||||
// Selected target info
|
||||
const target = targets.find(t => String(t.id) === selectedTarget);
|
||||
|
||||
// Group metrics by category
|
||||
const grouped = filteredMetrics.reduce((acc: Record<string, string[]>, name) => {
|
||||
const cat = name.split('_')[0] || 'other';
|
||||
if (!acc[cat]) acc[cat] = [];
|
||||
@@ -104,7 +94,6 @@ export function MetricsExplorer() {
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Merge query results into time-aligned data
|
||||
const timeMap: Record<number, Record<string, number>> = {};
|
||||
queryResults.forEach(pt => {
|
||||
if (!timeMap[pt.timestamp]) timeMap[pt.timestamp] = {};
|
||||
@@ -115,61 +104,68 @@ export function MetricsExplorer() {
|
||||
|
||||
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#f97316'];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<Select value={selectedTarget} onValueChange={setSelectedTarget}>
|
||||
<SelectTrigger className="w-64 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<SelectValue placeholder={loading ? '加载中...' : '选择监控目标...'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200 max-h-80">
|
||||
{targets.map(t => (
|
||||
<SelectItem key={t.id} value={String(t.id)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className={`inline-block w-1.5 h-1.5 rounded-full ${t.status === 'online' ? 'bg-emerald-400' : 'bg-slate-500'}`} />
|
||||
{t.name} ({t.ip || t.type})
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-2.5 top-2 h-4 w-4 text-slate-500" />
|
||||
<Input
|
||||
className="pl-8 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] h-9"
|
||||
placeholder="搜索指标..."
|
||||
value={searchKw}
|
||||
onChange={(e) => setSearchKw(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => { setSelectedMetrics([]); setQueryResults([]); }}>
|
||||
清除
|
||||
</Button>
|
||||
const toolbar = (
|
||||
<div className="flex items-center gap-4 flex-wrap w-full">
|
||||
<Select value={selectedTarget} onValueChange={setSelectedTarget}>
|
||||
<SelectTrigger className="w-64 dg-input">
|
||||
<SelectValue placeholder={loading ? '加载中...' : '选择监控目标...'} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input max-h-80">
|
||||
{targets.map(t => (
|
||||
<SelectItem key={t.id} value={String(t.id)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className={`inline-block w-1.5 h-1.5 rounded-full ${t.status === 'online' ? 'bg-emerald-400' : 'bg-[var(--dg-bg-active)]'}`} />
|
||||
{t.name} ({t.ip || t.type})
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="relative flex-1 max-w-sm">
|
||||
<Search className="absolute left-2.5 top-2 h-4 w-4 dg-text-muted" />
|
||||
<Input
|
||||
className="pl-8 dg-input h-9"
|
||||
placeholder="搜索指标..."
|
||||
value={searchKw}
|
||||
onChange={(e) => setSearchKw(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => { setSelectedMetrics([]); setQueryResults([]); }}>
|
||||
清除
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
variant="workspace"
|
||||
title="指标浏览器"
|
||||
subtitle="按分类/主机浏览所有指标,支持叠加对比"
|
||||
icon={<Activity className="h-5 w-5" />}
|
||||
toolbar={toolbar}
|
||||
loading={loading}
|
||||
loadingLabel="加载监控目标..."
|
||||
contentClassName="space-y-6"
|
||||
>
|
||||
{!selectedTarget ? (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-12 text-center">
|
||||
<Filter className="h-12 w-12 text-slate-600 mx-auto mb-4" />
|
||||
<p className="text-[var(--dg-text-muted)]">请先选择监控目标,然后勾选要查看的指标</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<PageEmpty
|
||||
icon={<Filter className="h-12 w-12" />}
|
||||
title="请选择监控目标"
|
||||
description="选择目标主机后,从左侧勾选要查看的指标"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Metric selector */}
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] lg:col-span-1">
|
||||
<Card className="dg-card lg:col-span-1">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<BarChart3 className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<BarChart3 className="h-4 w-4 dg-text-muted" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">指标列表</h3>
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-auto">{selectedMetrics.length} 已选</span>
|
||||
<span className="text-xs dg-text-muted ml-auto">{selectedMetrics.length} 已选</span>
|
||||
</div>
|
||||
<div className="space-y-4 max-h-[60vh] overflow-y-auto">
|
||||
{Object.entries(grouped).map(([cat, names]) => (
|
||||
<div key={cat}>
|
||||
<div className="flex items-center gap-1.5 mb-1.5 text-xs text-[var(--dg-text-muted)]">
|
||||
<div className="flex items-center gap-1.5 mb-1.5 text-xs dg-text-muted">
|
||||
{METRIC_CATEGORIES[cat]?.icon}
|
||||
<span>{METRIC_CATEGORIES[cat]?.label || cat}</span>
|
||||
</div>
|
||||
@@ -177,10 +173,10 @@ export function MetricsExplorer() {
|
||||
{names.map(name => {
|
||||
const selected = selectedMetrics.includes(name);
|
||||
return (
|
||||
<label key={name} className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer text-xs transition-colors ${selected ? 'bg-blue-500/10 text-blue-300' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}>
|
||||
<label key={name} className={`flex items-center gap-2 px-2 py-1 rounded cursor-pointer text-xs transition-colors ${selected ? 'bg-[var(--dg-bg-active)] dg-text-blue' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-tertiary)]'}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded border-slate-600 bg-slate-800 accent-blue-500"
|
||||
className="rounded border-[var(--dg-border-hover)] bg-[var(--dg-bg-tertiary)] accent-[var(--dg-blue)]"
|
||||
checked={selected}
|
||||
onChange={() => {
|
||||
setSelectedMetrics(prev =>
|
||||
@@ -199,34 +195,24 @@ export function MetricsExplorer() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Chart area */}
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] lg:col-span-2">
|
||||
<Card className="dg-card lg:col-span-2">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
|
||||
<Activity className="h-4 w-4 dg-text-muted" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">
|
||||
{target?.name || ''} — 最近 1 小时
|
||||
</h3>
|
||||
</div>
|
||||
{selectedMetrics.length === 0 ? (
|
||||
<div className="h-64 flex items-center justify-center text-sm text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
|
||||
<div className="text-center">
|
||||
<Plus className="h-8 w-8 text-slate-600 mx-auto mb-2" />
|
||||
请从左侧勾选指标
|
||||
</div>
|
||||
</div>
|
||||
<PageEmpty icon={<Plus className="h-8 w-8" />} title="请从左侧勾选指标" className="py-16" />
|
||||
) : sortedTimes.length === 0 ? (
|
||||
<div className="h-64 flex items-center justify-center text-sm text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-lg">
|
||||
暂无数据
|
||||
</div>
|
||||
<PageEmpty title="暂无数据" className="py-16" />
|
||||
) : (
|
||||
<div className="h-80 overflow-x-auto">
|
||||
<svg viewBox={`0 0 ${Math.max(sortedTimes.length * 4, 400)} 300`} className="w-full h-full min-w-[400px]">
|
||||
{/* Grid lines */}
|
||||
{[0, 25, 50, 75, 100].map(pct => (
|
||||
<line key={pct} x1="0" y1={300 - (pct / 100) * 260} x2={Math.max(sortedTimes.length * 4, 400)} y2={300 - (pct / 100) * 260} stroke="#1e293b" strokeDasharray="3 3" />
|
||||
))}
|
||||
{/* Lines per metric */}
|
||||
{selectedMetrics.map((mn, mi) => {
|
||||
const points = sortedTimes
|
||||
.map((t, i) => ({ x: i * 4 + 2, y: 300 - ((timeMap[t][mn] ?? 0) / maxVal) * 260 }))
|
||||
@@ -239,7 +225,6 @@ export function MetricsExplorer() {
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{/* X axis time labels */}
|
||||
{sortedTimes.filter((_, i) => i % 15 === 0).map((t, i) => (
|
||||
<text key={t} x={i * 60 + 2} y="295" fill="#475569" fontSize="9">
|
||||
{new Date(t * 1000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
|
||||
@@ -248,7 +233,6 @@ export function MetricsExplorer() {
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{/* Legend */}
|
||||
{selectedMetrics.length > 0 && (
|
||||
<div className="flex flex-wrap gap-3 mt-3">
|
||||
{selectedMetrics.map((mn, mi) => (
|
||||
@@ -264,12 +248,11 @@ export function MetricsExplorer() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Target metrics overview */}
|
||||
{target && Object.keys(target.latest_metrics || {}).length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
{Object.entries(target.latest_metrics).map(([key, val]) => (
|
||||
<div key={key} className="bg-[var(--dg-bg-secondary)] rounded-lg p-3 border border-[var(--dg-border)] text-center">
|
||||
<p className="text-[10px] text-[var(--dg-text-muted)] truncate">{key}</p>
|
||||
<p className="text-[10px] dg-text-muted truncate">{key}</p>
|
||||
<p className="text-lg font-bold text-[var(--dg-text-primary)]">
|
||||
{typeof val === 'number' ? val.toFixed(1) : '--'}
|
||||
</p>
|
||||
@@ -277,8 +260,8 @@ export function MetricsExplorer() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricsExplorer;
|
||||
export default MetricsExplorer;
|
||||
@@ -54,11 +54,11 @@ function fmtVal(v: number, precision = 0): string {
|
||||
|
||||
function statusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'online': return 'text-emerald-400';
|
||||
case 'warning': return 'text-amber-400';
|
||||
case 'online': return 'dg-text-green';
|
||||
case 'warning': return 'dg-text-yellow';
|
||||
case 'error':
|
||||
case 'offline': return 'text-red-400';
|
||||
default: return 'text-slate-500';
|
||||
case 'offline': return 'dg-text-red';
|
||||
default: return 'dg-text-muted';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function statusBg(status: string): string {
|
||||
case 'warning': return 'bg-amber-400';
|
||||
case 'error':
|
||||
case 'offline': return 'bg-red-400';
|
||||
default: return 'bg-slate-500';
|
||||
default: return 'bg-[var(--dg-bg-active)]';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ export function MiddlewareMonitor() {
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-slate-500 mr-2" />
|
||||
<Loader2 className="h-6 w-6 animate-spin dg-text-muted mr-2" />
|
||||
<span className="text-sm text-[var(--dg-text-muted)]">正在加载中间件指标...</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -182,8 +182,8 @@ export function MiddlewareMonitor() {
|
||||
{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>
|
||||
<Activity className="h-8 w-8 dg-text-red mx-auto mb-2" />
|
||||
<p className="text-sm dg-text-red">{error}</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mt-2">请确认后端服务可用</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -192,7 +192,7 @@ export function MiddlewareMonitor() {
|
||||
{!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" />
|
||||
<Database className="h-12 w-12 dg-text-muted 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">点击「添加监控」配置 Redis / MySQL 连接</p>
|
||||
<Button size="sm" className="mt-4 gap-1.5" onClick={() => { setEditTarget(null); setDialogOpen(true); }}>
|
||||
@@ -208,7 +208,7 @@ export function MiddlewareMonitor() {
|
||||
<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" />
|
||||
<Database className="h-4 w-4 dg-text-red" />
|
||||
<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)]">
|
||||
@@ -237,7 +237,7 @@ export function MiddlewareMonitor() {
|
||||
<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" />
|
||||
<Server className="h-4 w-4 dg-text-blue" />
|
||||
<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)]">
|
||||
@@ -358,7 +358,7 @@ function InstanceCard({
|
||||
</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)]'}`}
|
||||
className={`text-[10px] capitalize ${target.status === 'online' ? 'border-emerald-400/50 dg-text-green' : 'border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||
>
|
||||
{target.status}
|
||||
</Badge>
|
||||
@@ -393,7 +393,7 @@ function InstanceCard({
|
||||
<Pencil className="h-3 w-3" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs gap-1 text-red-400 hover:text-red-300" onClick={onDelete}>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs gap-1 dg-text-red hover:dg-text-red" onClick={onDelete}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
删除
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 监控中心 — 4个监控标签页的容器
|
||||
// 监控中心 — 4个监控标签页的容器 // Logan.Li
|
||||
import { useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Cpu, Database, Radio, Globe } from 'lucide-react';
|
||||
import { Cpu } from 'lucide-react';
|
||||
import { PageShell } from '@/components/layout/PageShell';
|
||||
import { HostMonitor } from './HostMonitor';
|
||||
import { MiddlewareMonitor } from './MiddlewareMonitor';
|
||||
import { SNMPMonitor } from './SNMPMonitor';
|
||||
@@ -9,61 +9,32 @@ import { SiteMonitor } from './SiteMonitor';
|
||||
|
||||
type TabId = 'host' | 'middleware' | 'snmp' | 'site';
|
||||
|
||||
interface Tab {
|
||||
id: TabId;
|
||||
name: string;
|
||||
icon: React.ElementType;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ id: 'host', name: '主机监控', icon: Cpu, description: 'CPU / 内存 / 磁盘 / 网络 / 磁盘IO' },
|
||||
{ id: 'middleware', name: '中间件监控', icon: Database, description: 'Redis / MySQL 指标采集' },
|
||||
{ id: 'snmp', name: 'SNMP 监控', icon: Radio, description: '网络设备 / 服务器硬件 (OID 模板)' },
|
||||
{ id: 'site', name: '站点拨测', icon: Globe, description: 'HTTP / PING / DNS / TCP / WS 拨测' },
|
||||
const tabs: { id: TabId; name: string; description: string }[] = [
|
||||
{ id: 'host', name: '主机监控', description: 'CPU / 内存 / 磁盘 / 网络 / 磁盘IO' },
|
||||
{ id: 'middleware', name: '中间件监控', description: 'Redis / MySQL 指标采集' },
|
||||
{ id: 'snmp', name: 'SNMP 监控', description: '网络设备 / 服务器硬件 (OID 模板)' },
|
||||
{ id: 'site', name: '站点拨测', description: 'HTTP / PING / DNS / TCP / WS 拨测' },
|
||||
];
|
||||
|
||||
export function MonitorCenter() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('host');
|
||||
const current = tabs.find((t) => t.id === activeTab);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--dg-text-primary)]">监控中心</h1>
|
||||
<p className="text-sm text-[var(--dg-text-muted)] mt-1">全面的基础设施与应用监控</p>
|
||||
</div>
|
||||
|
||||
{/* Tab Bar */}
|
||||
<div className="flex gap-1 bg-[var(--dg-bg-secondary)] p-1 rounded-lg border border-[var(--dg-border)]">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-md text-sm font-medium transition-all',
|
||||
activeTab === tab.id
|
||||
? 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-primary)] shadow-sm'
|
||||
: 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)]'
|
||||
)}
|
||||
>
|
||||
<tab.icon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{tab.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Description */}
|
||||
<p className="text-sm text-[var(--dg-text-muted)] -mt-4">
|
||||
{tabs.find(t => t.id === activeTab)?.description}
|
||||
</p>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="min-h-[400px]">
|
||||
{activeTab === 'host' && <HostMonitor />}
|
||||
{activeTab === 'middleware' && <MiddlewareMonitor />}
|
||||
{activeTab === 'snmp' && <SNMPMonitor />}
|
||||
{activeTab === 'site' && <SiteMonitor />}
|
||||
</div>
|
||||
</div>
|
||||
<PageShell
|
||||
variant="list"
|
||||
title="监控中心"
|
||||
subtitle={current?.description ?? '全面的基础设施与应用监控'}
|
||||
icon={<Cpu className="h-5 w-5" />}
|
||||
tabs={tabs.map((t) => ({ id: t.id, label: t.name }))}
|
||||
activeTab={activeTab}
|
||||
onTabChange={(id) => setActiveTab(id as TabId)}
|
||||
contentClassName="min-h-[400px]"
|
||||
>
|
||||
{activeTab === 'host' && <HostMonitor embedded />}
|
||||
{activeTab === 'middleware' && <MiddlewareMonitor />}
|
||||
{activeTab === 'snmp' && <SNMPMonitor />}
|
||||
{activeTab === 'site' && <SiteMonitor />}
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -143,12 +143,12 @@ export function SNMPMonitor() {
|
||||
// ---------- status badge ----------
|
||||
const statusBadge = (status?: string) => {
|
||||
if (status === 'online') {
|
||||
return <Badge className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">在线</Badge>;
|
||||
return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30">在线</Badge>;
|
||||
}
|
||||
if (status === 'abnormal') {
|
||||
return <Badge className="bg-red-500/15 text-red-400 border-red-500/30">异常</Badge>;
|
||||
return <Badge className="bg-red-500/15 dg-text-red border-red-500/30">异常</Badge>;
|
||||
}
|
||||
return <Badge className="bg-slate-500/15 text-slate-400 border-slate-500/30">离线</Badge>;
|
||||
return <Badge className="bg-[var(--dg-bg-active)]/15 dg-text-secondary border-[var(--dg-border)]">离线</Badge>;
|
||||
};
|
||||
|
||||
// ---------- render ----------
|
||||
@@ -159,7 +159,7 @@ export function SNMPMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Router className="h-4 w-4 text-cyan-400" />
|
||||
<Router className="h-4 w-4 dg-text-cyan" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">网络设备</h3>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-3">交换机 / 路由器 / 防火墙</p>
|
||||
@@ -170,7 +170,7 @@ export function SNMPMonitor() {
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-[var(--dg-text-muted)]">在线</span>
|
||||
<span className="text-emerald-400 font-mono">{stats.online}</span>
|
||||
<span className="dg-text-green font-mono">{stats.online}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -179,14 +179,14 @@ export function SNMPMonitor() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<HardDrive className="h-4 w-4 text-purple-400" />
|
||||
<HardDrive className="h-4 w-4 dg-text-purple" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">服务器硬件</h3>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--dg-text-muted)] mb-3">iLO / iDRAC / IPMI</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-[var(--dg-text-muted)]">异常</span>
|
||||
<span className="text-red-400 font-mono">{stats.abnormal}</span>
|
||||
<span className="dg-text-red font-mono">{stats.abnormal}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -221,7 +221,7 @@ export function SNMPMonitor() {
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Router className="h-4 w-4 text-cyan-400" />
|
||||
<Router className="h-4 w-4 dg-text-cyan" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)]">
|
||||
SNMP 设备列表
|
||||
{!loading && (
|
||||
@@ -235,12 +235,12 @@ export function SNMPMonitor() {
|
||||
{/* Add Device Dialog */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" className="bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20 border border-cyan-500/30">
|
||||
<Button size="sm" className="bg-cyan-500/10 dg-text-cyan hover:bg-cyan-500/20 border border-cyan-500/30">
|
||||
<Plus className="h-4 w-4" />
|
||||
添加设备
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="bg-slate-900 border-[var(--dg-border)] text-[var(--dg-text-primary)] sm:max-w-md">
|
||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)]">添加 SNMP 设备</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)]">
|
||||
@@ -307,7 +307,7 @@ export function SNMPMonitor() {
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700 text-slate-200">
|
||||
<SelectContent className="dg-input">
|
||||
<SelectItem value="v2c">v2c</SelectItem>
|
||||
<SelectItem value="v3">v3</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -348,14 +348,14 @@ export function SNMPMonitor() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDialogOpen(false)}
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-muted)] hover:bg-slate-800"
|
||||
className="border-[var(--dg-border)] text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)]"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
disabled={submitting || !form.name.trim() || !form.ip.trim()}
|
||||
className="bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20 border border-cyan-500/30"
|
||||
className="bg-cyan-500/10 dg-text-cyan hover:bg-cyan-500/20 border border-cyan-500/30"
|
||||
>
|
||||
{submitting ? '添加中...' : '确认添加'}
|
||||
</Button>
|
||||
@@ -410,7 +410,7 @@ export function SNMPMonitor() {
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => handleDelete(d.id)}
|
||||
className="text-slate-500 hover:text-red-400 hover:bg-red-500/10"
|
||||
className="dg-text-muted hover:dg-text-red hover:bg-red-500/10"
|
||||
title="删除设备"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
|
||||
@@ -49,11 +49,11 @@ import type { JsonData } from '@/types/common';
|
||||
|
||||
// 拨测协议类型(与后端 model 对齐:http, https, tcp, dns, ping)
|
||||
const probeTypes = [
|
||||
{ icon: Globe, name: 'HTTP', color: 'text-blue-400', desc: 'HTTP URL 状态码 & 响应时间' },
|
||||
{ icon: Globe, name: 'HTTPS', color: 'text-emerald-400', desc: 'HTTPS SSL 证书 & 响应时间' },
|
||||
{ icon: Server, name: 'TCP', color: 'text-orange-400', desc: 'TCP 端口连通性检测' },
|
||||
{ icon: Globe, name: 'DNS', color: 'text-cyan-400', desc: '域名解析延迟 & 结果验证' },
|
||||
{ icon: ArrowUpDown, name: 'PING', color: 'text-purple-400', desc: 'ICMP 连通性 & 延迟' },
|
||||
{ icon: Globe, name: 'HTTP', color: 'dg-text-blue', desc: 'HTTP URL 状态码 & 响应时间' },
|
||||
{ icon: Globe, name: 'HTTPS', color: 'dg-text-green', desc: 'HTTPS SSL 证书 & 响应时间' },
|
||||
{ icon: Server, name: 'TCP', color: 'dg-text-orange', desc: 'TCP 端口连通性检测' },
|
||||
{ icon: Globe, name: 'DNS', color: 'dg-text-cyan', desc: '域名解析延迟 & 结果验证' },
|
||||
{ icon: ArrowUpDown, name: 'PING', color: 'dg-text-purple', desc: 'ICMP 连通性 & 延迟' },
|
||||
] as const;
|
||||
|
||||
const PROBE_TYPE_OPTIONS = ['HTTP', 'HTTPS', 'TCP', 'DNS', 'PING'] as const;
|
||||
@@ -104,22 +104,22 @@ const emptyForm: CreateTaskForm = {
|
||||
function statusIcon(status?: string) {
|
||||
switch (status) {
|
||||
case 'up':
|
||||
return <CheckCircle2 className="h-4 w-4 text-emerald-400" />;
|
||||
return <CheckCircle2 className="h-4 w-4 dg-text-green" />;
|
||||
case 'down':
|
||||
return <XCircle className="h-4 w-4 text-red-400" />;
|
||||
return <XCircle className="h-4 w-4 dg-text-red" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-gray-400" />;
|
||||
return <Clock className="h-4 w-4 dg-text-secondary" />;
|
||||
}
|
||||
}
|
||||
|
||||
function statusBadge(status?: string) {
|
||||
switch (status) {
|
||||
case 'up':
|
||||
return <Badge className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">正常</Badge>;
|
||||
return <Badge className="bg-emerald-500/15 dg-text-green border-emerald-500/30">正常</Badge>;
|
||||
case 'down':
|
||||
return <Badge className="bg-red-500/15 text-red-400 border-red-500/30">异常</Badge>;
|
||||
return <Badge className="bg-red-500/15 dg-text-red border-red-500/30">异常</Badge>;
|
||||
default:
|
||||
return <Badge className="bg-gray-500/15 text-gray-400 border-gray-500/30">未知</Badge>;
|
||||
return <Badge className="dg-badge-muted">未知</Badge>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,9 +300,9 @@ export function SiteMonitor() {
|
||||
{[
|
||||
{ label: '总任务数', value: stats?.total_tasks ?? '--' },
|
||||
{ label: '已启用', value: stats?.enabled_tasks ?? '--' },
|
||||
{ label: '正常', value: stats?.up_count ?? '--', color: 'text-emerald-400' },
|
||||
{ label: '异常', value: stats?.down_count ?? '--', color: 'text-red-400' },
|
||||
{ label: '可用率', value: availStr, color: 'text-blue-400' },
|
||||
{ label: '正常', value: stats?.up_count ?? '--', color: 'dg-text-green' },
|
||||
{ label: '异常', value: stats?.down_count ?? '--', color: 'dg-text-red' },
|
||||
{ label: '可用率', value: availStr, color: 'dg-text-blue' },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="bg-[var(--dg-bg-tertiary)] rounded-lg p-3 text-center">
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">{item.label}</p>
|
||||
@@ -407,7 +407,7 @@ export function SiteMonitor() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
className="h-8 w-8 dg-text-red hover:dg-text-red hover:bg-red-500/10"
|
||||
onClick={() => handleDelete(task)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
|
||||
@@ -33,12 +33,12 @@ export function DualLineChart({
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Network className="h-4 w-4 text-blue-400" />
|
||||
<Network className="h-4 w-4 dg-text-blue" />
|
||||
<span className="text-xs text-[var(--dg-text-primary)] font-medium">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-blue-400 mr-1 align-middle" />接收 <b className="text-blue-300">{currentRx}</b></span>
|
||||
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-400 mr-1 align-middle" />发送 <b className="text-emerald-300">{currentTx}</b></span>
|
||||
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-400 mr-1 align-middle" />发送 <b className="dg-text-green">{currentTx}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-56">
|
||||
|
||||
@@ -110,9 +110,9 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{p.last_status === 'up' ? <CheckCircle className="w-4 h-4 text-emerald-400" /> :
|
||||
p.last_status === 'down' ? <XCircle className="w-4 h-4 text-red-400" /> :
|
||||
<Clock className="w-4 h-4 text-slate-500" />}
|
||||
{p.last_status === 'up' ? <CheckCircle className="w-4 h-4 dg-text-green" /> :
|
||||
p.last_status === 'down' ? <XCircle className="w-4 h-4 dg-text-red" /> :
|
||||
<Clock className="w-4 h-4 dg-text-muted" />}
|
||||
<span className="text-xs font-medium text-[var(--dg-text-primary)]">{String(p.name)}</span>
|
||||
<Badge variant="outline" className="text-[10px] h-5 px-1.5 capitalize">
|
||||
{p.probe_type === 'http_get' ? 'HTTP' : p.probe_type === 'tcp_socket' ? 'TCP' : 'Exec'}
|
||||
@@ -122,7 +122,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={() => openEditDialog(p)}>
|
||||
<Edit className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 text-red-400 hover:text-red-300" onClick={() => handleDeleteProbe(p)}>
|
||||
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 dg-text-red hover:dg-text-red" onClick={() => handleDeleteProbe(p)}>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -138,11 +138,11 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
<div className="truncate font-mono">{String(p.exec_command)}</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<span>状态: <span className={p.last_status === 'up' ? 'text-emerald-400' : p.last_status === 'down' ? 'text-red-400' : 'text-slate-400'}>
|
||||
<span>状态: <span className={p.last_status === 'up' ? 'dg-text-green' : p.last_status === 'down' ? 'dg-text-red' : 'dg-text-secondary'}>
|
||||
{p.last_status ? String(p.last_status).toUpperCase() : '未检测'}
|
||||
</span></span>
|
||||
{Number(p.last_latency_ms) > 0 && <span>延迟: {String(p.last_latency_ms)}ms</span>}
|
||||
{Number(p.fail_count) > 0 && <span className="text-red-400">连续失败: {String(p.fail_count)}次</span>}
|
||||
{Number(p.fail_count) > 0 && <span className="dg-text-red">连续失败: {String(p.fail_count)}次</span>}
|
||||
{p.last_checked_at && <span className="text-[10px]">{new Date(String(p.last_checked_at)).toLocaleString('zh-CN')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针名称 <span className="text-red-400">*</span></label>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">探针名称 <span className="dg-text-red">*</span></label>
|
||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.name} onChange={e => setProbeForm({ ...probeForm, name: e.target.value })} placeholder="如:cex-agent 健康检查" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -183,7 +183,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
|
||||
{probeForm.probe_type === 'http_get' && (
|
||||
<>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="text-red-400">*</span></label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">HTTP URL <span className="dg-text-red">*</span></label>
|
||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs text-[var(--dg-text-primary)]" value={probeForm.http_url} onChange={e => setProbeForm({ ...probeForm, http_url: e.target.value })} placeholder="http://localhost:8080/health" /></div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">期望状态码</label>
|
||||
@@ -204,7 +204,7 @@ export function HealthProbeSection({ selectedHost }: HealthProbeSectionProps) {
|
||||
)}
|
||||
|
||||
{probeForm.probe_type === 'exec_command' && (
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">执行命令 <span className="text-red-400">*</span></label>
|
||||
<div><label className="text-[11px] text-[var(--dg-text-muted)] mb-1 block">执行命令 <span className="dg-text-red">*</span></label>
|
||||
<input className="w-full h-8 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 text-xs font-mono text-[var(--dg-text-primary)]" value={probeForm.exec_command} onChange={e => setProbeForm({ ...probeForm, exec_command: e.target.value })} placeholder="pgrep -x mysqld" /></div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -38,13 +38,13 @@ export function PortPolicyDialog({
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">过滤模式</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 text-blue-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'whitelist' ? 'bg-blue-500/20 border-blue-500 dg-text-blue' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||
onClick={() => onModeChange('whitelist')}
|
||||
>
|
||||
白名单 ─ 全量上报
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 text-amber-400' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||
className={`flex-1 h-9 rounded text-xs font-medium border transition-colors ${portPolicyMode === 'blacklist' ? 'bg-amber-500/20 border-amber-500 dg-text-yellow' : 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-muted)]'}`}
|
||||
onClick={() => onModeChange('blacklist')}
|
||||
>
|
||||
黑名单 ─ 仅上报选中
|
||||
@@ -60,7 +60,7 @@ export function PortPolicyDialog({
|
||||
<div>
|
||||
<label className="text-[11px] text-[var(--dg-text-muted)] mb-1.5 block">
|
||||
端口列表 <span className="text-[var(--dg-text-muted)]">(逗号分隔,如: 80,443,3306,6379)</span>
|
||||
{portPolicyMode === 'whitelist' && <span className="text-blue-400 ml-1">— 白名单模式无需配置</span>}
|
||||
{portPolicyMode === 'whitelist' && <span className="dg-text-blue ml-1">— 白名单模式无需配置</span>}
|
||||
</label>
|
||||
<textarea
|
||||
className={`w-full h-20 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded px-2 py-1.5 text-xs font-mono resize-none ${
|
||||
|
||||
@@ -55,9 +55,9 @@ export function SystemInfoSection({
|
||||
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">系统负载 (Load Average)</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 text-xs">
|
||||
<div><span className="text-[var(--dg-text-muted)]">1min </span><span className="text-sm font-bold text-slate-200">{latest.load1 ?? '--'}</span></div>
|
||||
<div><span className="text-[var(--dg-text-muted)]">5min </span><span className="text-sm font-bold text-slate-200">{latest.load5 ?? '--'}</span></div>
|
||||
<div><span className="text-[var(--dg-text-muted)]">15min </span><span className="text-sm font-bold text-slate-200">{latest.load15 ?? '--'}</span></div>
|
||||
<div><span className="text-[var(--dg-text-muted)]">1min </span><span className="text-sm font-bold text-[var(--dg-text-primary)]">{latest.load1 ?? '--'}</span></div>
|
||||
<div><span className="text-[var(--dg-text-muted)]">5min </span><span className="text-sm font-bold text-[var(--dg-text-primary)]">{latest.load5 ?? '--'}</span></div>
|
||||
<div><span className="text-[var(--dg-text-muted)]">15min </span><span className="text-sm font-bold text-[var(--dg-text-primary)]">{latest.load15 ?? '--'}</span></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -71,11 +71,11 @@ export function SystemInfoSection({
|
||||
{parts.map((p: JsonData, i: number) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
<span className="text-[var(--dg-text-muted)] w-24 font-mono truncate">{p.mount || p.mountpoint || p.device || `disk${i}`}</span>
|
||||
<div className="flex-1 h-1.5 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div className="flex-1 h-1.5 bg-[var(--dg-bg-active)] rounded-full overflow-hidden">
|
||||
<div className="h-full rounded-full bg-blue-500" style={{ width: `${Math.min(p.usage_percent || p.used_percent || 0, 100)}%` }} />
|
||||
</div>
|
||||
<span className="text-[var(--dg-text-primary)] w-28 text-right tabular-nums whitespace-nowrap">{(p.used_gb ?? 0).toFixed(1)}/{(p.total_gb ?? 0).toFixed(0)}GB</span>
|
||||
<span className="text-blue-400 w-14 text-right tabular-nums whitespace-nowrap">{(p.usage_percent ?? p.used_percent ?? 0).toFixed(1)}%</span>
|
||||
<span className="dg-text-blue w-14 text-right tabular-nums whitespace-nowrap">{(p.usage_percent ?? p.used_percent ?? 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user