diff --git a/src/api/componentInstance.ts b/src/api/componentInstance.ts index 587e804..349b7b6 100644 --- a/src/api/componentInstance.ts +++ b/src/api/componentInstance.ts @@ -51,4 +51,9 @@ export function saveComponentEnvConfig(instanceId, params) { // 本地启动 export function localStart(params) { return axios.get(`${urlPrefix}/componentInstance/local?componentBaseId=${params.componentBaseId}`); -} \ No newline at end of file +} + +// 组件资源 +export function getComponentResource(id) { + return axios.get(`${urlPrefix}/componentInstance/resource?id=${id}`); +} diff --git a/src/components/ResourceMonitorModal/index.module.less b/src/components/ResourceMonitorModal/index.module.less new file mode 100644 index 0000000..eb6eb25 --- /dev/null +++ b/src/components/ResourceMonitorModal/index.module.less @@ -0,0 +1,135 @@ +.resourceModal { + :global(.arco-modal-content) { + max-height: 90vh; + overflow-y: auto; + } +} + +.container { + padding: 16px; +} + +.infoSection { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px 24px; + padding: 16px; + background: var(--color-fill-2); + border-radius: 8px; + margin-bottom: 24px; +} + +.infoItem { + display: flex; + align-items: center; + font-size: 14px; +} + +.label { + color: var(--color-text-2); + margin-right: 8px; + white-space: nowrap; +} + +.value { + color: var(--color-text-1); + font-weight: 500; + word-break: break-all; +} + +.running { + color: var(--color-success-6); +} + +.stopped { + color: var(--color-danger-6); +} + +.gaugeSection { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + margin-bottom: 24px; +} + +.gaugeItem { + background: var(--color-fill-2); + border-radius: 8px; + padding: 16px; +} + +.gaugeTitle { + font-size: 16px; + font-weight: 600; + color: var(--color-text-1); + margin: 0 0 8px 0; + text-align: center; +} + +.chartSection { + background: var(--color-fill-2); + border-radius: 8px; + padding: 16px; + margin-bottom: 24px; +} + +.chartTitle { + font-size: 16px; + font-weight: 600; + color: var(--color-text-1); + margin: 0 0 16px 0; +} + +.detailSection { + background: var(--color-fill-2); + border-radius: 8px; + padding: 16px; +} + +.detailTitle { + font-size: 16px; + font-weight: 600; + color: var(--color-text-1); + margin: 0 0 16px 0; +} + +.detailGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px 24px; +} + +.detailItem { + display: flex; + align-items: center; + font-size: 14px; + padding: 8px 0; +} + +.detailLabel { + color: var(--color-text-2); + margin-right: 8px; + white-space: nowrap; +} + +.detailValue { + color: var(--color-text-1); + font-weight: 500; + font-family: 'Consolas', 'Monaco', monospace; +} + +.noData { + text-align: center; + padding: 60px 0; + color: var(--color-text-3); + font-size: 16px; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .infoSection, + .gaugeSection, + .detailGrid { + grid-template-columns: 1fr; + } +} diff --git a/src/components/ResourceMonitorModal/index.tsx b/src/components/ResourceMonitorModal/index.tsx new file mode 100644 index 0000000..6b41434 --- /dev/null +++ b/src/components/ResourceMonitorModal/index.tsx @@ -0,0 +1,369 @@ +import React from 'react'; +import { Modal } from '@arco-design/web-react'; +import ReactECharts from 'echarts-for-react'; +import type { EChartsOption } from 'echarts'; +import styles from './index.module.less'; + +interface ResourceData { + collectedAt: number; + containerName: string; + cpuPercent: number; + elapsedSeconds: number; + identifier: string; + instanceId: string; + memLimitBytes: number; + memLimitMb: number; + memPercent: number; + message: string; + pid: number; + rssBytes: number; + rssKb: number; + rssMb: number; + running: boolean; + vsizeKb: number; +} + +interface ResourceMonitorModalProps { + visible: boolean; + onCancel: () => void; + data: ResourceData | null; +} + +const ResourceMonitorModal: React.FC = ({ + visible, + onCancel, + data, +}) => { + // 格式化运行时长 + const formatElapsedTime = (seconds: number): string => { + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + const parts = []; + if (days > 0) parts.push(`${days}天`); + if (hours > 0) parts.push(`${hours}小时`); + if (minutes > 0) parts.push(`${minutes}分钟`); + if (secs > 0 || parts.length === 0) parts.push(`${secs}秒`); + + return parts.join(' '); + }; + + // CPU 使用率仪表盘配置 + const getCpuGaugeOption = (): EChartsOption => { + return { + series: [ + { + type: 'gauge', + startAngle: 180, + endAngle: 0, + min: 0, + max: 100, + splitNumber: 10, + itemStyle: { + color: data && data.cpuPercent > 80 ? '#F53F3F' : data && data.cpuPercent > 50 ? '#FF7D00' : '#00B42A', + }, + progress: { + show: true, + width: 18, + }, + pointer: { + show: false, + }, + axisLine: { + lineStyle: { + width: 18, + }, + }, + axisTick: { + distance: -30, + splitNumber: 5, + lineStyle: { + width: 2, + color: '#999', + }, + }, + splitLine: { + distance: -40, + length: 14, + lineStyle: { + width: 3, + color: '#999', + }, + }, + axisLabel: { + distance: -20, + color: '#999', + fontSize: 14, + }, + anchor: { + show: false, + }, + title: { + show: false, + }, + detail: { + valueAnimation: true, + width: '60%', + lineHeight: 40, + borderRadius: 8, + offsetCenter: [0, '10%'], + fontSize: 40, + fontWeight: 'bolder', + formatter: '{value}%', + color: 'inherit', + }, + data: [ + { + value: data?.cpuPercent || 0, + name: 'CPU 使用率', + }, + ], + }, + ], + }; + }; + + // 内存使用率仪表盘配置 + const getMemoryGaugeOption = (): EChartsOption => { + return { + series: [ + { + type: 'gauge', + startAngle: 180, + endAngle: 0, + min: 0, + max: 100, + splitNumber: 10, + itemStyle: { + color: data && data.memPercent > 80 ? '#F53F3F' : data && data.memPercent > 50 ? '#FF7D00' : '#00B42A', + }, + progress: { + show: true, + width: 18, + }, + pointer: { + show: false, + }, + axisLine: { + lineStyle: { + width: 18, + }, + }, + axisTick: { + distance: -30, + splitNumber: 5, + lineStyle: { + width: 2, + color: '#999', + }, + }, + splitLine: { + distance: -40, + length: 14, + lineStyle: { + width: 3, + color: '#999', + }, + }, + axisLabel: { + distance: -20, + color: '#999', + fontSize: 14, + }, + anchor: { + show: false, + }, + title: { + show: false, + }, + detail: { + valueAnimation: true, + width: '60%', + lineHeight: 40, + borderRadius: 8, + offsetCenter: [0, '10%'], + fontSize: 40, + fontWeight: 'bolder', + formatter: '{value}%', + color: 'inherit', + }, + data: [ + { + value: data?.memPercent || 0, + name: '内存使用率', + }, + ], + }, + ], + }; + }; + + // 内存详情柱状图配置 + const getMemoryBarOption = (): EChartsOption => { + return { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow', + }, + formatter: (params: any) => { + const param = params[0]; + return `${param.name}: ${param.value.toFixed(2)} MB`; + }, + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + xAxis: { + type: 'category', + data: ['已使用内存 (RSS)', '内存限制'], + axisLabel: { + interval: 0, + rotate: 0, + }, + }, + yAxis: { + type: 'value', + name: 'MB', + axisLabel: { + formatter: '{value}', + }, + }, + series: [ + { + name: '内存', + type: 'bar', + data: [ + { + value: data?.rssMb || 0, + itemStyle: { + color: '#3491FA', + }, + }, + { + value: data?.memLimitMb || 0, + itemStyle: { + color: '#86909C', + }, + }, + ], + barWidth: '40%', + label: { + show: true, + position: 'top', + formatter: '{c} MB', + }, + }, + ], + }; + }; + + return ( + + {data ? ( +
+ {/* 基本信息 */} +
+
+ 容器名称: + {data.containerName} +
+
+ 实例标识: + {data.identifier} +
+
+ 进程 ID: + {data.pid} +
+
+ 运行状态: + + {data.running ? '运行中' : '已停止'} + +
+
+ 运行时长: + {formatElapsedTime(data.elapsedSeconds)} +
+
+ 采集时间: + + {new Date(data.collectedAt).toLocaleString('zh-CN')} + +
+
+ + {/* 仪表盘区域 */} +
+
+

CPU 使用率

+ +
+
+

内存使用率

+ +
+
+ + {/* 内存详情柱状图 */} +
+

内存使用详情

+ +
+ + {/* 详细数据 */} +
+

详细数据

+
+
+ RSS 内存 (KB): + {data.rssKb.toLocaleString()} +
+
+ RSS 内存 (Bytes): + {data.rssBytes.toLocaleString()} +
+
+ 虚拟内存 (KB): + {data.vsizeKb.toLocaleString()} +
+
+ 内存限制 (Bytes): + {data.memLimitBytes.toLocaleString()} +
+
+
+
+ ) : ( +
暂无数据
+ )} +
+ ); +}; + +export default ResourceMonitorModal; diff --git a/src/pages/componentDevelopment/componentDeployment/listNode.tsx b/src/pages/componentDevelopment/componentDeployment/listNode.tsx index a587ab0..ec4f680 100644 --- a/src/pages/componentDevelopment/componentDeployment/listNode.tsx +++ b/src/pages/componentDevelopment/componentDeployment/listNode.tsx @@ -19,12 +19,14 @@ import { startInstance, stopInstance, getInstanceLog, - refreshInstanceDependency + refreshInstanceDependency, + getComponentResource } from '@/api/componentInstance'; import { runStatusConstant, runStatusDic, runTypeConstant, runTypeDic } from '@/const/isdp/componentDeploy'; import dayjs from 'dayjs'; import EditInstanceModal from './editInstanceModal'; import EnvConfigModal from './envConfigModal'; +import ResourceMonitorModal from '@/components/ResourceMonitorModal'; const { RangePicker } = DatePicker; const { TextArea } = Input; @@ -59,6 +61,10 @@ const ListNode: React.FC = ({ componentData }) => { const [envModalVisible, setEnvModalVisible] = useState(false); const [envInstanceId, setEnvInstanceId] = useState(''); + // 资源监控 Modal 相关状态 + const [resourceModalVisible, setResourceModalVisible] = useState(false); + const [resourceData, setResourceData] = useState(null); + // 获取实例列表 const fetchInstanceList = async () => { if (!componentData?.identifier) return; @@ -306,6 +312,22 @@ const ListNode: React.FC = ({ componentData }) => { fetchInstanceList(); // 刷新列表 }; + // 查看组件资源 + const handeViewResource = async (record) => { + try { + const res: any = await getComponentResource(record.id); + if (res.code === 200 && res.data) { + setResourceData(res.data); + setResourceModalVisible(true); + } else { + Message.error(res.msg || '获取资源信息失败'); + } + } catch (error) { + console.error('获取资源信息失败:', error); + Message.error('获取资源信息失败'); + } + }; + const columns: TableColumnProps[] = [ { title: '#', @@ -378,6 +400,9 @@ const ListNode: React.FC = ({ componentData }) => { {!isRunning && ( )} + {!isLocalRun && isRunning && ( + + )}