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
@@ -23,7 +23,7 @@ import {
|
||||
ChevronLeft,
|
||||
Loader2,
|
||||
} from 'lucide-react';
|
||||
import { regionApi, serverApi } from '@/services/api';
|
||||
import { regionApi, serverApi, credentialApi } from '@/services/api';
|
||||
|
||||
interface RegionOption {
|
||||
id: string;
|
||||
@@ -94,12 +94,13 @@ function isValidIP(ip: string): boolean {
|
||||
return /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && ip.split('.').every((o) => Number(o) >= 0 && Number(o) <= 255);
|
||||
}
|
||||
|
||||
// 下载 CSV 导入模板 // Logan.Li
|
||||
// 下载 CSV 导入模板 — 只需 hostname + ip,端口默认22 账号默认root,Agent 安装后自动补全 // Logan.Li
|
||||
function downloadTemplate() {
|
||||
const csv = [
|
||||
'hostname,ip,port,username,os,cpu,memory,disk,tags',
|
||||
'web-01,192.168.1.10,22,root,CentOS 7,4,8,100,"web,prod"',
|
||||
'db-01,192.168.1.20,22,root,Ubuntu 22.04,8,16,200,"db,prod"',
|
||||
'hostname,ip',
|
||||
'web-01,192.168.1.10',
|
||||
'db-01,192.168.1.20',
|
||||
'cache-01,192.168.1.30',
|
||||
].join('\n');
|
||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -119,30 +120,36 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
const [previewData, setPreviewData] = useState<PreviewRow[]>([]);
|
||||
const [regions, setRegions] = useState<RegionOption[]>([]);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
// 凭据选择 // Logan.Li
|
||||
const [credentials, setCredentials] = useState<any[]>([]);
|
||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('');
|
||||
|
||||
// 对话框打开时拉取区域列表 // Logan.Li
|
||||
// 对话框打开时拉取区域列表 + 凭据列表 // Logan.Li
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await regionApi.list();
|
||||
const [regionResp, credResp] = await Promise.all([
|
||||
regionApi.list(),
|
||||
credentialApi.list(),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
const list = resp?.list ?? [];
|
||||
setRegions(
|
||||
list.map((r: any) => ({
|
||||
id: String(r.id ?? ''),
|
||||
name: r.name ?? '',
|
||||
code: r.code ?? '',
|
||||
}))
|
||||
);
|
||||
const list = regionResp?.list ?? [];
|
||||
setRegions(list.map((r: any) => ({
|
||||
id: String(r.id ?? ''),
|
||||
name: r.name ?? '',
|
||||
code: r.code ?? '',
|
||||
})));
|
||||
setCredentials(credResp?.list ?? []);
|
||||
} catch {
|
||||
if (!cancelled) setRegions([]);
|
||||
if (!cancelled) {
|
||||
setRegions([]);
|
||||
setCredentials([]);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
return () => { cancelled = true; };
|
||||
}, [open]);
|
||||
|
||||
const handleNext = async () => {
|
||||
@@ -239,6 +246,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
servers,
|
||||
region_id: selectedRegion || undefined,
|
||||
auth_type: connectionMethod,
|
||||
credential_id: selectedCredentialId ? Number(selectedCredentialId) : undefined,
|
||||
});
|
||||
const r = resp ?? {};
|
||||
const success = r.success ?? 0;
|
||||
@@ -273,7 +281,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-3xl bg-[var(--dg-bg-primary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Excel导入服务器</DialogTitle>
|
||||
<DialogTitle className="text-xl">CSV 批量导入服务器</DialogTitle>
|
||||
<DialogDescription className="sr-only">通过 CSV 文件批量导入服务器资产</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -313,7 +321,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-[var(--dg-text-primary)]">主机导入模板</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">下载Excel模板,填写服务器信息后上传</p>
|
||||
<p className="text-xs text-[var(--dg-text-muted)]">下载 CSV 模板,只填主机名和 IP,端口默认22、账号默认root</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
|
||||
@@ -341,6 +349,32 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 凭据选择(SSH密钥/密码) — 选中后自动关联到导入的每台服务器 // Logan.Li */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">
|
||||
选择凭据
|
||||
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(可选,用于SSH连接)</span>
|
||||
</label>
|
||||
{credentials.length === 0 ? (
|
||||
<p className="text-xs text-[var(--dg-text-muted)] py-2">
|
||||
暂无凭据,先去「凭证配置」添加 SSH 密钥或密码
|
||||
</p>
|
||||
) : (
|
||||
<select
|
||||
value={selectedCredentialId}
|
||||
onChange={(e) => setSelectedCredentialId(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-[var(--dg-bg-secondary)] 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.type === 'api_token' ? '🌐' : '🔒'} {c.name} ({c.type === 'ssh_key' ? 'SSH密钥' : c.type === 'api_token' ? 'API Token' : '密码'})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connection Method */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">连接方式</label>
|
||||
@@ -452,8 +486,6 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">行号</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">主机名</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">IP地址</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">端口</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">用户名</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -463,8 +495,6 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-muted)]">{row.row}</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-primary)]">{row.hostname || '-'}</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-secondary)]">{row.ip}</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-secondary)]">{row.port}</td>
|
||||
<td className="px-3 py-2 text-sm text-[var(--dg-text-secondary)]">{row.username}</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{row.status === 'valid' && (
|
||||
|
||||
Reference in new issue
Block a user