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