feat: Agent采集uptime, 后端持久化kernel/uptime/diskIO, 前端磁盘BPS+IOPS展示
- Agent: system.go 添加 readUptime(), SystemStat 携带 Uptime - Backend: Server 模型新增 Kernel 字段, ReceiveReport 持久化 kernel+uptime - Backend: /agent/latest 返回 kernel/uptime/port_info/disk_io_info/disk_total - Frontend: 磁盘监控改为 BPS+IOPS (4张卡片), 系统信息修复内核/运行时长/监听端口/磁盘容量 - 图表单位从 MB/s 改为 KB/s, 添加 formatBPS 工具函数
This commit is contained in:
1 parent
31c2e338a3
commit
5949ec5d0c
48 files changed
+2182
-956
No files matched your search
@@ -5,12 +5,15 @@ import {
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Terminal as XTerm } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import 'xterm/css/xterm.css';
|
||||
import { Maximize2, Minimize2, X } from 'lucide-react';
|
||||
import { Maximize2, Minimize2, X, Key, Shield } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUserStore } from '@/store';
|
||||
import { credentialApi, serverCredentialApi } from '@/services/api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface TerminalDialogProps {
|
||||
open: boolean;
|
||||
@@ -18,6 +21,8 @@ interface TerminalDialogProps {
|
||||
serverId: number | null;
|
||||
serverName: string;
|
||||
serverIp: string;
|
||||
/** 服务器是否有可用的认证凭据(密码或SSH密钥) */
|
||||
hasAuth?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,6 +35,7 @@ export function TerminalDialog({
|
||||
serverId,
|
||||
serverName,
|
||||
serverIp,
|
||||
hasAuth = true,
|
||||
}: TerminalDialogProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const termRef = useRef<XTerm | null>(null);
|
||||
@@ -38,9 +44,44 @@ export function TerminalDialog({
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
|
||||
// 连接终端
|
||||
// 凭据选择状态(hasAuth=false 时显示) // Logan.Li
|
||||
const [credentials, setCredentials] = useState<any[]>([]);
|
||||
const [selectedCredId, setSelectedCredId] = useState<string>('');
|
||||
const [assigning, setAssigning] = useState(false);
|
||||
const [authReady, setAuthReady] = useState(hasAuth); // 认证就绪标志
|
||||
|
||||
// 加载凭据列表
|
||||
useEffect(() => {
|
||||
if (!open || !serverId) return;
|
||||
if (!open || hasAuth) return;
|
||||
credentialApi.list().then((res: any) => setCredentials(res?.list || [])).catch(() => {});
|
||||
}, [open, hasAuth]);
|
||||
|
||||
// 关联凭据
|
||||
const handleAssignCredential = async () => {
|
||||
if (!selectedCredId || !serverId) return;
|
||||
try {
|
||||
setAssigning(true);
|
||||
await serverCredentialApi.assign(serverId, Number(selectedCredId));
|
||||
toast.success('凭据已关联');
|
||||
setAuthReady(true);
|
||||
} catch (e: any) {
|
||||
toast.error(e?.message || '关联失败');
|
||||
} finally {
|
||||
setAssigning(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置状态
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setAuthReady(hasAuth);
|
||||
setSelectedCredId('');
|
||||
}
|
||||
}, [open, hasAuth]);
|
||||
|
||||
// 连接终端(authReady=true 时才执行)
|
||||
useEffect(() => {
|
||||
if (!open || !serverId || !authReady) return;
|
||||
|
||||
// 创建终端
|
||||
const term = new XTerm({
|
||||
@@ -115,7 +156,7 @@ export function TerminalDialog({
|
||||
wsRef.current = null;
|
||||
setConnected(false);
|
||||
};
|
||||
}, [open, serverId]);
|
||||
}, [open, serverId, authReady]);
|
||||
|
||||
// 用 ResizeObserver 监听容器实际尺寸变化,自动 fit 终端
|
||||
// 这比 window resize 或 setTimeout 更可靠:
|
||||
@@ -189,12 +230,47 @@ export function TerminalDialog({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* 凭据选择器(无认证凭据时显示) */}
|
||||
{!authReady && (
|
||||
<div className="p-6 flex flex-col items-center justify-center gap-4" style={{ height: '480px' }}>
|
||||
<Shield className="h-12 w-12 text-amber-400" />
|
||||
<div className="text-center">
|
||||
<p className="text-[var(--dg-text-primary)] font-medium mb-1">服务器未配置认证凭据</p>
|
||||
<p className="text-sm text-[var(--dg-text-muted)]">请选择一个凭据用于 SSH 连接</p>
|
||||
</div>
|
||||
{credentials.length === 0 ? (
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">暂无可用凭据,请先去「凭证配置」添加</p>
|
||||
) : (
|
||||
<select
|
||||
value={selectedCredId}
|
||||
onChange={(e) => setSelectedCredId(e.target.value)}
|
||||
className="w-64 px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-sm"
|
||||
>
|
||||
<option value="">-- 选择凭据 --</option>
|
||||
{credentials.map((c: any) => (
|
||||
<option key={c.id} value={String(c.id)}>
|
||||
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleAssignCredential}
|
||||
disabled={!selectedCredId || assigning}
|
||||
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
|
||||
>
|
||||
{assigning ? '关联中...' : '确认连接'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* 终端容器 */}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bg-[#0a0e14]"
|
||||
style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}
|
||||
/>
|
||||
{authReady && (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="bg-[#0a0e14]"
|
||||
style={{ height: fullscreen ? 'calc(100vh - 49px)' : '480px' }}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user