feat(component): 添加组件资源监控功能
parent
bc6b64ec15
commit
c229615e66
@ -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;
|
||||
Loading…
Reference in New Issue