1.18升级

This commit is contained in:
dengqichen 2025-12-11 13:43:58 +08:00
parent 8b66e7d100
commit 38b6e873c8
7 changed files with 370 additions and 23 deletions

View File

@ -66,6 +66,8 @@
"cmdk": "^1.1.1",
"dagre": "^0.8.5",
"dayjs": "^1.11.13",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"form-render": "^2.5.6",
"less": "^4.2.1",
"monaco-editor": "^0.52.2",

View File

@ -173,6 +173,12 @@ importers:
dayjs:
specifier: ^1.11.13
version: 1.11.13
echarts:
specifier: ^6.0.0
version: 6.0.0
echarts-for-react:
specifier: ^3.0.5
version: 3.0.5(echarts@6.0.0)(react@18.3.1)
form-render:
specifier: ^2.5.6
version: 2.5.6(@types/react@18.3.18)(antd@5.27.5(date-fns@2.30.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -2752,6 +2758,15 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
echarts-for-react@3.0.5:
resolution: {integrity: sha512-YpEI5Ty7O/2nvCfQ7ybNa+S90DwE8KYZWacGvJW4luUqywP7qStQ+pxDlYOmr4jGDu10mhEkiAuMKcUlT4W5vg==}
peerDependencies:
echarts: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
react: ^15.0.0 || >=16.0.0
echarts@6.0.0:
resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==}
electron-to-chromium@1.5.73:
resolution: {integrity: sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==}
@ -4106,6 +4121,9 @@ packages:
simple-swizzle@0.2.4:
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
size-sensor@1.0.2:
resolution: {integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -4242,6 +4260,9 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@ -4411,6 +4432,9 @@ packages:
zod@3.24.1:
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
zrender@6.0.0:
resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==}
zustand@4.5.5:
resolution: {integrity: sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==}
engines: {node: '>=12.7.0'}
@ -7272,6 +7296,18 @@ snapshots:
eastasianwidth@0.2.0: {}
echarts-for-react@3.0.5(echarts@6.0.0)(react@18.3.1):
dependencies:
echarts: 6.0.0
fast-deep-equal: 3.1.3
react: 18.3.1
size-sensor: 1.0.2
echarts@6.0.0:
dependencies:
tslib: 2.3.0
zrender: 6.0.0
electron-to-chromium@1.5.73: {}
emoji-regex@8.0.0: {}
@ -8777,6 +8813,8 @@ snapshots:
dependencies:
is-arrayish: 0.3.4
size-sensor@1.0.2: {}
slash@3.0.0: {}
source-map-js@1.2.1: {}
@ -8923,6 +8961,8 @@ snapshots:
ts-interface-checker@0.1.13: {}
tslib@2.3.0: {}
tslib@2.8.1: {}
type-check@0.4.0:
@ -9052,6 +9092,10 @@ snapshots:
zod@3.24.1: {}
zrender@6.0.0:
dependencies:
tslib: 2.3.0
zustand@4.5.5(@types/react@18.3.18)(immer@10.1.1)(react@18.3.1):
dependencies:
use-sync-external-store: 1.2.2(react@18.3.1)

View File

@ -16,6 +16,7 @@ import {
Tag,
FileText,
RefreshCw,
BarChart3,
} from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@ -38,12 +39,13 @@ interface ServerCardProps {
onDelete: (server: ServerResponse) => void;
onSSHConnect: (server: ServerResponse) => void;
onCollectHardware: (server: ServerResponse) => void;
onMonitor: (server: ServerResponse) => void;
isTesting?: boolean;
isCollecting?: boolean;
getOsIcon: (osType?: string) => React.ReactNode;
}
export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit, onDelete, onSSHConnect, onCollectHardware, isTesting, isCollecting, getOsIcon }) => {
export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit, onDelete, onSSHConnect, onCollectHardware, onMonitor, isTesting, isCollecting, getOsIcon }) => {
const [expanded, setExpanded] = React.useState(false);
const formatTime = (time?: string) => {
@ -73,6 +75,11 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
<Card className={`group relative flex h-full flex-col justify-between overflow-visible border transition-all duration-200 hover:border-primary/40 hover:shadow-lg ${
expanded ? 'rounded-b-none border-b-0' : ''
}`}>
{/* ID Badge - 悬浮在左上角,类似通知样式 */}
<div className="absolute -left-2 -top-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-foreground shadow-md">
{server.id}
</div>
<CardContent className="flex flex-1 flex-col gap-3 p-3">
{/* 基础信息 */}
<div className="flex items-start gap-3">
@ -233,6 +240,20 @@ export const ServerCard: React.FC<ServerCardProps> = ({ server, onTest, onEdit,
)}
</div>
<div className="flex items-center gap-1.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-primary"
onClick={() => onMonitor(server)}
>
<BarChart3 className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button

View File

@ -9,6 +9,7 @@ import {
HelpCircle,
Terminal,
RefreshCw,
BarChart3,
} from 'lucide-react';
import {
Table,
@ -38,6 +39,7 @@ interface ServerTableProps {
onDelete: (server: ServerResponse) => void;
onSSHConnect: (server: ServerResponse) => void;
onCollectHardware: (server: ServerResponse) => void;
onMonitor: (server: ServerResponse) => void;
isTesting?: (serverId: number) => boolean;
isCollecting?: (serverId: number) => boolean;
getOsIcon: (osType?: string) => React.ReactNode;
@ -50,6 +52,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
onDelete,
onSSHConnect,
onCollectHardware,
onMonitor,
isTesting,
isCollecting,
getOsIcon,
@ -74,6 +77,7 @@ export const ServerTable: React.FC<ServerTableProps> = ({
<Table minWidth="1200px">
<TableHeader>
<TableRow>
<TableHead width="80px">ID</TableHead>
<TableHead width="180px"></TableHead>
<TableHead width="150px">IP地址</TableHead>
<TableHead width="120px"></TableHead>
@ -89,13 +93,16 @@ export const ServerTable: React.FC<ServerTableProps> = ({
<TableBody>
{servers.length === 0 ? (
<TableRow>
<TableCell colSpan={10} className="text-center py-8">
<TableCell colSpan={11} className="text-center py-8">
<span className="text-muted-foreground"></span>
</TableCell>
</TableRow>
) : (
servers.map((server) => (
<TableRow key={server.id}>
<TableCell>
<span className="font-mono text-sm text-muted-foreground">{server.id}</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{server.serverName}</span>
@ -141,6 +148,19 @@ export const ServerTable: React.FC<ServerTableProps> = ({
</TableCell>
<TableCell sticky className="text-right">
<div className="flex items-center justify-end gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => onMonitor(server)}
className="h-8 w-8 p-0"
>
<BarChart3 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button

View File

@ -44,6 +44,7 @@ import { ServerCard } from './components/ServerCard';
import { ServerTable } from './components/ServerTable';
import { SSHWindowManager } from './components/SSHWindowManager';
import { FileManagerWindowManager } from '@/components/FileManager';
import { ServerMonitorDialog } from './components/ServerMonitorDialog';
const ServerList: React.FC = () => {
const { toast } = useToast();
@ -81,6 +82,8 @@ const ServerList: React.FC = () => {
const [serverToDelete, setServerToDelete] = useState<ServerResponse | null>(null);
const [testingServerId, setTestingServerId] = useState<number | null>(null);
const [collectingServerId, setCollectingServerId] = useState<number | null>(null);
const [monitorDialogOpen, setMonitorDialogOpen] = useState(false);
const [monitoringServer, setMonitoringServer] = useState<ServerResponse | null>(null);
const [statsData, setStatsData] = useState<{
total: number;
online: number;
@ -103,10 +106,10 @@ const ServerList: React.FC = () => {
try {
// 并行请求各个状态的数量
const [totalResult, onlineResult, offlineResult, unknownResult] = await Promise.all([
getServers({ categoryId: selectedCategoryId, osType: selectedOsType, pageNum: 0, size: 1 }),
getServers({ categoryId: selectedCategoryId, status: 'ONLINE', osType: selectedOsType, pageNum: 0, size: 1 }),
getServers({ categoryId: selectedCategoryId, status: 'OFFLINE', osType: selectedOsType, pageNum: 0, size: 1 }),
getServers({ categoryId: selectedCategoryId, status: 'UNKNOWN', osType: selectedOsType, pageNum: 0, size: 1 }),
getServers({ categoryId: selectedCategoryId, osType: selectedOsType, pageNum: 0, pageSize: 1 }),
getServers({ categoryId: selectedCategoryId, status: 'ONLINE', osType: selectedOsType, pageNum: 0, pageSize: 1 }),
getServers({ categoryId: selectedCategoryId, status: 'OFFLINE', osType: selectedOsType, pageNum: 0, pageSize: 1 }),
getServers({ categoryId: selectedCategoryId, status: 'UNKNOWN', osType: selectedOsType, pageNum: 0, pageSize: 1 }),
]);
setStatsData({
@ -173,7 +176,7 @@ const ServerList: React.FC = () => {
const result = await getServers({
...params,
pageNum: pageIndex,
size: pageSize,
pageSize: pageSize,
});
if (result) {
setServers(result.content || []);
@ -280,6 +283,12 @@ const ServerList: React.FC = () => {
setDeleteDialogOpen(true);
};
// 监控数据
const handleMonitor = (server: ServerResponse) => {
setMonitoringServer(server);
setMonitorDialogOpen(true);
};
// SSH连接 - 使用多窗口系统
const handleSSHConnect = (server: ServerResponse) => {
// 调用全局方法打开新的SSH窗口
@ -345,15 +354,12 @@ const ServerList: React.FC = () => {
// 监听筛选条件和视图模式变化使用全局loading
useEffect(() => {
loadServers(false, false); // 筛选加载使用全局loading
// 筛选条件变化时重置分页到第一页
setPageIndex(0);
}, [pageSize, selectedCategoryId, selectedStatus, selectedOsType, viewMode]);
// 监听分页变化单独处理使用pageLoading
// 注意这个useEffect只处理用户主动翻页的情况
useEffect(() => {
if (viewMode === 'table' && pageIndex > 0) {
// pageIndex > 0 说明是用户翻页操作使用pageLoading
if (viewMode === 'table') {
// 表格模式下,分页变化时加载数据
loadServers(false, true);
}
}, [pageIndex]);
@ -615,6 +621,7 @@ const ServerList: React.FC = () => {
onDelete={handleDelete}
onSSHConnect={handleSSHConnect}
onCollectHardware={handleCollectHardware}
onMonitor={handleMonitor}
isTesting={testingServerId === server.id}
isCollecting={collectingServerId === server.id}
getOsIcon={getOsIcon}
@ -642,6 +649,7 @@ const ServerList: React.FC = () => {
onDelete={handleDelete}
onSSHConnect={handleSSHConnect}
onCollectHardware={handleCollectHardware}
onMonitor={handleMonitor}
isTesting={(serverId) => testingServerId === serverId}
isCollecting={(serverId) => collectingServerId === serverId}
getOsIcon={getOsIcon}
@ -726,6 +734,13 @@ const ServerList: React.FC = () => {
confirmText="确定"
/>
{/* 监控数据对话框 */}
<ServerMonitorDialog
open={monitorDialogOpen}
onOpenChange={setMonitorDialogOpen}
server={monitoringServer}
/>
{/* SSH多窗口管理器 */}
<SSHWindowManager />

View File

@ -10,6 +10,9 @@ import type {
AlertRuleResponse,
AlertRuleQuery,
AlertRuleRequest,
ServerMonitorResponse,
TimeRange,
MetricType,
} from './types';
import type { ServerFormValues } from './schema';
import type { Page } from '@/types/base';
@ -18,6 +21,7 @@ import type { Page } from '@/types/base';
const CATEGORY_URL = '/api/v1/server-category';
const SERVER_URL = '/api/v1/server';
const ALERT_RULE_URL = '/api/v1/server/alert-rule';
const MONITOR_URL = '/api/v1/server/monitor';
// ==================== 服务器分类 ====================
@ -78,22 +82,14 @@ export const getServerList = (params?: {
/**
*
*/
export const getServers = (params: {
export const getServers = (params?: {
categoryId?: number;
status?: string;
osType?: string;
pageNum?: number;
size?: number;
pageSize?: number;
}) =>
request.get<Page<ServerResponse>>(`${SERVER_URL}/page`, {
params: {
pageNum: params.pageNum ?? 0,
size: params.size ?? 20,
categoryId: params.categoryId,
status: params.status,
osType: params.osType,
},
});
request.get<Page<ServerResponse>>(`${SERVER_URL}/page`, { params });
/**
*
@ -177,3 +173,22 @@ export const updateAlertRule = (id: number, data: AlertRuleRequest) =>
export const deleteAlertRule = (id: number) =>
request.delete<void>(`${ALERT_RULE_URL}/${id}`);
// ==================== 服务器监控 ====================
/**
*
* @param serverId ID
* @param timeRange
* @param metrics
*/
export const getServerMonitorMetrics = (
serverId: number,
timeRange: TimeRange,
metrics?: MetricType[]
) =>
request.get<ServerMonitorResponse>(`${MONITOR_URL}/${serverId}/metrics`, {
params: {
timeRange,
metrics: metrics?.join(','),
},
});

View File

@ -371,3 +371,233 @@ export const AlertTypeLabels: Record<AlertType, { label: string; unit: string; d
[AlertType.SERVER_STATUS]: { label: '服务器状态', unit: '次', description: '服务器连接状态告警' },
};
// ==================== 服务器监控 ====================
/**
*
*/
export enum TimeRange {
/** 最近1小时 */
LAST_1_HOUR = 'LAST_1_HOUR',
/** 最近6小时 */
LAST_6_HOURS = 'LAST_6_HOURS',
/** 最近24小时 */
LAST_24_HOURS = 'LAST_24_HOURS',
/** 最近7天 */
LAST_7_DAYS = 'LAST_7_DAYS',
/** 最近30天 */
LAST_30_DAYS = 'LAST_30_DAYS',
}
/**
*
*/
export enum MetricType {
/** CPU */
CPU = 'CPU',
/** 内存 */
MEMORY = 'MEMORY',
/** 磁盘 */
DISK = 'DISK',
/** 网络 */
NETWORK = 'NETWORK',
}
/**
*
*/
export const TimeRangeLabels: Record<TimeRange, { label: string; interval: string; description: string }> = {
[TimeRange.LAST_1_HOUR]: { label: '最近1小时', interval: '5分钟', description: '最近1小时的监控数据' },
[TimeRange.LAST_6_HOURS]: { label: '最近6小时', interval: '5分钟', description: '最近6小时的监控数据' },
[TimeRange.LAST_24_HOURS]: { label: '最近24小时', interval: '15分钟', description: '最近24小时的监控数据' },
[TimeRange.LAST_7_DAYS]: { label: '最近7天', interval: '1小时', description: '最近7天的监控数据' },
[TimeRange.LAST_30_DAYS]: { label: '最近30天', interval: '4小时', description: '最近30天的监控数据' },
};
/**
*
*/
export const MetricTypeLabels: Record<MetricType, { label: string; unit: string; color: string }> = {
[MetricType.CPU]: { label: 'CPU使用率', unit: '%', color: '#3b82f6' },
[MetricType.MEMORY]: { label: '内存使用率', unit: '%', color: '#10b981' },
[MetricType.DISK]: { label: '磁盘使用率', unit: '%', color: '#f59e0b' },
[MetricType.NETWORK]: { label: '网络流量', unit: 'MB/s', color: '#8b5cf6' },
};
/**
* CPU监控数据点
*/
export interface CpuMetricData {
/** 时间点 */
time: string;
/** CPU使用率(%) */
value: number;
/** 采集状态 */
status: 'SUCCESS' | 'FAILURE';
}
/**
*
*/
export interface MemoryMetricData {
/** 时间点 */
time: string;
/** 内存使用率(%) */
usagePercent: number;
/** 已用内存(GB) */
usedGB: number;
/** 采集状态 */
status: 'SUCCESS' | 'FAILURE';
}
/**
*
*/
export interface NetworkMetricData {
/** 时间点 */
time: string;
/** 接收字节数(累计) */
rxBytes: number;
/** 发送字节数(累计) */
txBytes: number;
/** 接收速率(MB/s) */
rxMBps: number;
/** 发送速率(MB/s) */
txMBps: number;
}
/**
*
*/
export interface DiskPartitionData {
/** 挂载点 */
mountPoint: string;
/** 文件系统 */
fileSystem: string;
/** 总容量(GB) */
totalSizeGB: number;
/** 已用容量(GB) */
usedSizeGB: number;
/** 使用率(%) */
usagePercent: number;
}
/**
*
*/
export interface DiskMetricData {
/** 最新采集时间 */
latestTime: string;
/** 分区信息列表 */
partitions: DiskPartitionData[];
/** 时间范围内最大使用率(%) */
maxUsagePercent: number;
/** 最大使用率的分区 */
maxUsagePartition: string;
}
/**
* CPU统计信息
*/
export interface CpuStatistics {
/** 平均值(%) */
avg: number;
/** 最大值(%) */
max: number;
/** 最小值(%) */
min: number;
/** 峰值时间 */
maxTime: string;
}
/**
*
*/
export interface MemoryStatistics {
/** 平均使用率(%) */
avgPercent: number;
/** 最大使用率(%) */
maxPercent: number;
/** 最小使用率(%) */
minPercent: number;
/** 峰值时间 */
maxTime: string;
}
/**
*
*/
export interface NetworkStatistics {
/** 总接收字节数 */
totalRxBytes: number;
/** 总发送字节数 */
totalTxBytes: number;
}
/**
*
*/
export interface MonitorServerInfo {
/** 服务器ID */
serverId: number;
/** 服务器名称 */
serverName: string;
/** 主机IP */
hostIp: string;
/** 服务器状态 */
status: string;
}
/**
*
*/
export interface MonitorTimeRangeInfo {
/** 开始时间 */
startTime: string;
/** 结束时间 */
endTime: string;
/** 数据聚合间隔 */
interval: string;
/** 实际返回的数据点数量 */
dataPoints: number;
}
/**
*
*/
export interface MonitorMetrics {
/** CPU数据 */
cpu: CpuMetricData[] | null;
/** 内存数据 */
memory: MemoryMetricData[] | null;
/** 网络数据 */
network: NetworkMetricData[] | null;
/** 磁盘数据 */
disk: DiskMetricData | null;
}
/**
*
*/
export interface MonitorStatistics {
/** CPU统计 */
cpu: CpuStatistics | null;
/** 内存统计 */
memory: MemoryStatistics | null;
/** 网络统计 */
network: NetworkStatistics | null;
}
/**
*
*/
export interface ServerMonitorResponse {
/** 服务器信息 */
server: MonitorServerInfo;
/** 时间范围信息 */
timeRange: MonitorTimeRangeInfo;
/** 指标数据 */
metrics: MonitorMetrics;
/** 统计信息 */
statistics: MonitorStatistics;
}