feat(component): 添加组件资源监控功能

master
钟良源 1 week ago
parent bc6b64ec15
commit c229615e66

@ -51,4 +51,9 @@ export function saveComponentEnvConfig(instanceId, params) {
// 本地启动
export function localStart(params) {
return axios.get(`${urlPrefix}/componentInstance/local?componentBaseId=${params.componentBaseId}`);
}
}
// 组件资源
export function getComponentResource(id) {
return axios.get(`${urlPrefix}/componentInstance/resource?id=${id}`);
}

@ -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;
}
}

@ -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<ResourceMonitorModalProps> = ({
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 (
<Modal
title="组件资源监控"
visible={visible}
onCancel={onCancel}
footer={null}
style={{ width: '900px' }}
className={styles.resourceModal}
>
{data ? (
<div className={styles.container}>
{/* 基本信息 */}
<div className={styles.infoSection}>
<div className={styles.infoItem}>
<span className={styles.label}>:</span>
<span className={styles.value}>{data.containerName}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.label}>:</span>
<span className={styles.value}>{data.identifier}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.label}> ID:</span>
<span className={styles.value}>{data.pid}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.label}>:</span>
<span className={`${styles.value} ${data.running ? styles.running : styles.stopped}`}>
{data.running ? '运行中' : '已停止'}
</span>
</div>
<div className={styles.infoItem}>
<span className={styles.label}>:</span>
<span className={styles.value}>{formatElapsedTime(data.elapsedSeconds)}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.label}>:</span>
<span className={styles.value}>
{new Date(data.collectedAt).toLocaleString('zh-CN')}
</span>
</div>
</div>
{/* 仪表盘区域 */}
<div className={styles.gaugeSection}>
<div className={styles.gaugeItem}>
<h3 className={styles.gaugeTitle}>CPU 使</h3>
<ReactECharts
option={getCpuGaugeOption()}
style={{ height: '280px', width: '100%' }}
notMerge={true}
/>
</div>
<div className={styles.gaugeItem}>
<h3 className={styles.gaugeTitle}>使</h3>
<ReactECharts
option={getMemoryGaugeOption()}
style={{ height: '280px', width: '100%' }}
notMerge={true}
/>
</div>
</div>
{/* 内存详情柱状图 */}
<div className={styles.chartSection}>
<h3 className={styles.chartTitle}>使</h3>
<ReactECharts
option={getMemoryBarOption()}
style={{ height: '300px', width: '100%' }}
notMerge={true}
/>
</div>
{/* 详细数据 */}
<div className={styles.detailSection}>
<h3 className={styles.detailTitle}></h3>
<div className={styles.detailGrid}>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>RSS (KB):</span>
<span className={styles.detailValue}>{data.rssKb.toLocaleString()}</span>
</div>
<div className={styles.detailItem}>
<span className={styles.detailLabel}>RSS (Bytes):</span>
<span className={styles.detailValue}>{data.rssBytes.toLocaleString()}</span>
</div>
<div className={styles.detailItem}>
<span className={styles.detailLabel}> (KB):</span>
<span className={styles.detailValue}>{data.vsizeKb.toLocaleString()}</span>
</div>
<div className={styles.detailItem}>
<span className={styles.detailLabel}> (Bytes):</span>
<span className={styles.detailValue}>{data.memLimitBytes.toLocaleString()}</span>
</div>
</div>
</div>
</div>
) : (
<div className={styles.noData}></div>
)}
</Modal>
);
};
export default ResourceMonitorModal;

@ -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<ListNodeProps> = ({ componentData }) => {
const [envModalVisible, setEnvModalVisible] = useState(false);
const [envInstanceId, setEnvInstanceId] = useState<string>('');
// 资源监控 Modal 相关状态
const [resourceModalVisible, setResourceModalVisible] = useState(false);
const [resourceData, setResourceData] = useState<any>(null);
// 获取实例列表
const fetchInstanceList = async () => {
if (!componentData?.identifier) return;
@ -306,6 +312,22 @@ const ListNode: React.FC<ListNodeProps> = ({ 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<ListNodeProps> = ({ componentData }) => {
{!isRunning && (
<Button type="text" onClick={() => handleOpenEnvConfig(record)}></Button>
)}
{!isLocalRun && isRunning && (
<Button type="text" onClick={() => handeViewResource(record)}></Button>
)}
<Button type="text"
onClick={() => handleOpenEdit(record)}
icon={<img
@ -509,6 +534,13 @@ const ListNode: React.FC<ListNodeProps> = ({ componentData }) => {
onCancel={handleEnvConfigCancel}
onSuccess={handleEnvConfigSuccess}
/>
{/* 资源监控 Modal */}
<ResourceMonitorModal
visible={resourceModalVisible}
onCancel={() => setResourceModalVisible(false)}
data={resourceData}
/>
</>
);
};

Loading…
Cancel
Save