refactor(frontend): P1-P2 续 — API 子模块化、大组件拆分、Query 铺开

- api/server/ 与 api/cloud/ 子目录拆分,保持 @/api 导入不变
- hooks/api 新增 useCredentials、usePromDatasources,增强 useServers
- 拆分 CredentialManager、DeploymentWorkload、PrometheusDatasourceManager
- ServerManager、CredentialManager、PromDs 接入 TanStack Query
This commit is contained in:
Logan.Li committed 2026-06-26 00:22:50 +08:00
1 parent f45e203e21
commit 480e677042
32 files changed
+2280 -1940

No files matched your search

@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { cn } from '@/lib/utils';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -39,7 +40,7 @@ import {
ChevronLeft,
ChevronRight,
} from 'lucide-react';
import { serverApi } from '@/api';
import { useServers, useServerMutations, queryKeys } from '@/hooks/api';
import { ServerStatsStrip } from './ServerStatsStrip';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { toast } from 'sonner';
@@ -55,9 +56,8 @@ import { usePermission } from '@/hooks/usePermission';
export function ServerManager() {
const confirm = useConfirm();
const queryClient = useQueryClient();
const { hasPermission } = usePermission();
const [servers, setServers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedServers, setSelectedServers] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
@@ -82,7 +82,18 @@ export function ServerManager() {
// 分页状态
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const queryParams = useMemo(() => {
const params: Record<string, unknown> = { page, page_size: pageSize };
if (searchQuery) params.keyword = searchQuery;
if (statusFilter && statusFilter !== 'all') params.status = statusFilter;
return params;
}, [page, pageSize, searchQuery, statusFilter]);
const { data, isLoading: loading, refetch } = useServers(queryParams);
const { createMutation, updateMutation, deleteMutation, batchDeleteMutation } = useServerMutations();
const servers = data?.list ?? [];
const total = data?.total ?? 0;
const openTerminal = ( server: any) => {
setTerminalServer({ id: server.id, name: server.hostname, ip: server.ip, hasAuth: server.has_auth ?? true });
@@ -94,25 +105,6 @@ export function ServerManager() {
setFileBrowserOpen(true);
};
const loadData = async (kw?: string, st?: string) => {
try {
setLoading(true);
const q = kw ?? searchQuery;
const s = st ?? statusFilter;
const params: Record<string, any> = { page, page_size: pageSize };
if (q) params.keyword = q;
if (s && s !== 'all') params.status = s;
const res: Record<string, any> = await serverApi.getServers(params);
setServers(res?.list || []);
setTotal(res?.total || 0);
} catch (e) {
console.error('加载服务器列表失败:', e);
setServers([]);
} finally {
setLoading(false);
}
};
// 搜索/筛选变化时重置到第1页
const handleSearch = (val: string) => {
setSearchQuery(val);
@@ -123,10 +115,6 @@ export function ServerManager() {
setPage(1);
};
useEffect(() => {
loadData();
}, [page, pageSize]);
// WebSocket 实时接收 Agent 指标推送 // Logan.Li
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -152,27 +140,30 @@ export function ServerManager() {
try {
const msg = JSON.parse(event.data);
if (msg.type === 'metrics_update') {
setServers((prev) =>
prev.map((s) => {
// 通过 hostname 匹配(Agent 上报时使用 hostname)
if (s.hostname === msg.hostname) {
return {
...s,
cpu_usage: msg.cpu_usage,
memory_usage: msg.memory_usage,
disk_usage: msg.disk_usage,
status: msg.status,
tcp_established: msg.tcp_established,
tcp_time_wait: msg.tcp_time_wait,
net_rx_bps: msg.net_rx_bps,
net_tx_bps: msg.net_tx_bps,
disk_read_bps: msg.disk_read_bps,
disk_write_bps: msg.disk_write_bps,
};
}
return s;
})
);
queryClient.setQueryData(queryKeys.servers(queryParams), (old: typeof data) => {
if (!old) return old;
return {
...old,
list: old.list.map((s) => {
if (s.hostname === msg.hostname) {
return {
...s,
cpu_usage: msg.cpu_usage,
memory_usage: msg.memory_usage,
disk_usage: msg.disk_usage,
status: msg.status,
tcp_established: msg.tcp_established,
tcp_time_wait: msg.tcp_time_wait,
net_rx_bps: msg.net_rx_bps,
net_tx_bps: msg.net_tx_bps,
disk_read_bps: msg.disk_read_bps,
disk_write_bps: msg.disk_write_bps,
};
}
return s;
}),
};
});
}
} catch {
// 忽略非 JSON 消息
@@ -201,14 +192,14 @@ export function ServerManager() {
wsRef.current = null;
}
};
}, []);
}, [queryClient, queryParams]);
// 导出 CSV
const toggleSelectAll = () => {
if (selectedServers.length === servers.length) {
setSelectedServers([]);
} else {
setSelectedServers(servers.map((s) => s.id));
setSelectedServers(servers.map((s) => String(s.id)));
}
};
@@ -230,9 +221,8 @@ export function ServerManager() {
});
if (!ok) return;
try {
await serverApi.deleteServer(id);
await deleteMutation.mutateAsync(id);
toast.success('服务器已删除');
loadData();
} catch (e) {
console.error('删除服务器失败:', e);
toast.error('删除失败');
@@ -250,10 +240,9 @@ export function ServerManager() {
});
if (!ok) return;
try {
await serverApi.batchDelete(selectedServers.map((id) => Number(id)));
await batchDeleteMutation.mutateAsync(selectedServers.map((id) => Number(id)));
toast.success(`已删除 ${selectedServers.length} 台服务器`);
setSelectedServers([]);
loadData();
} catch (e) {
console.error('批量删除失败:', e);
toast.error('批量删除失败');
@@ -326,13 +315,12 @@ export function ServerManager() {
}
}
if (editingId) {
await serverApi.updateServer(editingId, payload);
await updateMutation.mutateAsync({ id: editingId, data: payload });
} else {
await serverApi.createServer(payload);
await createMutation.mutateAsync(payload);
}
toast.success(editingId ? '服务器已更新' : '服务器已创建');
setFormOpen(false);
loadData();
} catch (e) {
console.error('保存服务器失败:', e);
toast.error('保存失败,请重试');
@@ -349,7 +337,7 @@ export function ServerManager() {
}
const headers = ['主机名', 'IP地址', '公网IP', '端口', '操作系统', 'CPU(核)', '内存(GB)', '磁盘(GB)', '状态', '标签'];
const rows = servers.map((s) => {
const memGB = s.memory ? Math.round(s.memory / 1024) : 0;
const memGB = s.memory ? Math.round(Number(s.memory) / 1024) : 0;
const tags = getTags(s).join(';');
return [s.hostname || '', s.ip || '', s.public_ip || '', s.port || 22, s.os || '',
s.cpu || 0, memGB, s.disk || 0, s.status || '', tags];
@@ -381,7 +369,7 @@ export function ServerManager() {
<Button
variant="outline"
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
onClick={() => loadData()}
onClick={() => refetch()}
>
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
刷新
@@ -527,7 +515,7 @@ export function ServerManager() {
<ServerTableRow
key={server.id}
server={server}
selected={selectedServers.includes(server.id)}
selected={selectedServers.includes(String(server.id))}
onToggleSelect={toggleSelect}
onOpenTerminal={openTerminal}
onOpenFiles={openFileBrowser}
@@ -602,14 +590,14 @@ export function ServerManager() {
open={cloudImportOpen}
onClose={() => {
setCloudImportOpen(false);
loadData();
refetch();
}}
/>
<ExcelServerImport
open={excelImportOpen}
onClose={() => {
setExcelImportOpen(false);
loadData();
refetch();
}}
/>