From 60991bdc8578b15de9577b1beb90d86f64364e28 Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Thu, 4 Jul 2024 13:01:03 +0800 Subject: [PATCH] feat: updated node monitoring tab --- src/components/nav/NavActions.vue | 19 +- src/i18n/lang/en/components/node.ts | 41 ++- src/i18n/lang/zh/components/node.ts | 41 ++- .../components/metric/MetricGroup.d.ts | 7 + src/interfaces/i18n/components/node.d.ts | 15 +- src/layouts/content/detail/DetailLayout.vue | 3 + src/layouts/content/detail/useDetail.ts | 5 +- src/utils/time.ts | 6 + .../detail/tabs/NodeDetailTabMonitoring.vue | 250 +++++++++++++----- 9 files changed, 287 insertions(+), 100 deletions(-) create mode 100644 src/interfaces/components/metric/MetricGroup.d.ts create mode 100644 src/utils/time.ts diff --git a/src/components/nav/NavActions.vue b/src/components/nav/NavActions.vue index 4d84d0965573c..8b3a4c0ec14be 100644 --- a/src/components/nav/NavActions.vue +++ b/src/components/nav/NavActions.vue @@ -6,10 +6,10 @@ const props = defineProps<{ minHeight?: string; }>(); -const originalHeight = ref(null); +const originalHeight = ref(); const height = ref(); -const navActions = ref(null); +const navActions = ref(); const unmounted = ref(true); @@ -20,7 +20,6 @@ const collapsed = computed(() => { const style = computed(() => { return { - // height: height.value, minHeight: height.value, }; }); @@ -32,11 +31,15 @@ const classes = computed(() => { return cls; }); +const getHeight = () => { + return height.value; +}; + const updateHeight = () => { if (!collapsed.value) { - if (originalHeight.value === null) { + if (!originalHeight.value) { if (!navActions.value) return; - originalHeight.value = `calc(${window.getComputedStyle(navActions.value).height} - 1px)`; + originalHeight.value = window.getComputedStyle(navActions.value).height; } height.value = originalHeight.value; } else { @@ -50,6 +53,12 @@ onMounted(() => { updateHeight(); unmounted.value = false; }); + +defineExpose({ + getHeight, + updateHeight, +}); + defineOptions({ name: 'ClNavActions' }); diff --git a/src/i18n/lang/en/components/node.ts b/src/i18n/lang/en/components/node.ts index d300fe2d5d183..156ac463e3158 100644 --- a/src/i18n/lang/en/components/node.ts +++ b/src/i18n/lang/en/components/node.ts @@ -46,24 +46,37 @@ const node: LComponentsNode = { }, metrics: { cpu_usage_percent: 'CPU Usage (%)', - total_memory: 'Total Memory (bytes)', - available_memory: 'Available Memory (bytes)', - used_memory: 'Used Memory (bytes)', + total_memory: 'Total Memory', + available_memory: 'Available Memory', + used_memory: 'Used Memory', used_memory_percent: 'Used Memory (%)', - total_disk: 'Total Disk (bytes)', - available_disk: 'Available Disk (bytes)', - used_disk: 'Used Disk (bytes)', + total_disk: 'Total Disk', + available_disk: 'Available Disk', + used_disk: 'Used Disk', used_disk_percent: 'Used Disk (%)', - disk_read_bytes_rate: 'Disk Read IO (bytes/sec)', - disk_write_bytes_rate: 'Disk Write IO (bytes/sec)', - network_bytes_sent_rate: 'Network Sent IO (bytes/sec)', - network_bytes_recv_rate: 'Network Recv IO (bytes/sec)', + disk_read_bytes_rate: 'Disk Read IO', + disk_write_bytes_rate: 'Disk Write IO', + network_bytes_sent_rate: 'Network Sent IO', + network_bytes_recv_rate: 'Network Recv IO', + }, + groups: { + disk_io_bytes_rate: 'Disk IO', + network_io_bytes_rate: 'Network IO', + }, + timeUnits: { + s: 'sec', + m: 'min', + h: 'hr', + d: 'd', + w: 'wk', + M: 'mon', + y: 'yr', }, timeRanges: { - '1h': '1 Hour', - '1d': '1 Day', - '7d': '7 Days', - '30d': '30 Days', + '1h': 'Past 1 Hour', + '24h': 'Past 24 Hours', + '7d': 'Past 7 Days', + '30d': 'Past 30 Days', }, }, }; diff --git a/src/i18n/lang/zh/components/node.ts b/src/i18n/lang/zh/components/node.ts index 79b99477e99a4..2ed34c5adef44 100644 --- a/src/i18n/lang/zh/components/node.ts +++ b/src/i18n/lang/zh/components/node.ts @@ -46,24 +46,37 @@ const node: LComponentsNode = { }, metrics: { cpu_usage_percent: 'CPU 使用率 (%)', - total_memory: '总内存 (字节)', - available_memory: '可用内存 (字节)', - used_memory: '已用内存 (字节)', + total_memory: '总内存', + available_memory: '可用内存', + used_memory: '已用内存', used_memory_percent: '已用内存 (%)', - total_disk: '总磁盘 (字节)', - available_disk: '可用磁盘 (字节)', - used_disk: '已用磁盘 (字节)', + total_disk: '总磁盘', + available_disk: '可用磁盘', + used_disk: '已用磁盘', used_disk_percent: '已用磁盘 (%)', - disk_read_bytes_rate: '磁盘读取 IO (字节/秒)', - disk_write_bytes_rate: '磁盘写入 IO (字节/秒)', - network_bytes_sent_rate: '网络发送 IO (字节/秒)', - network_bytes_recv_rate: '网络接收 IO (字节/秒)', + disk_read_bytes_rate: '磁盘读取 IO', + disk_write_bytes_rate: '磁盘写入 IO', + network_bytes_sent_rate: '网络发送 IO', + network_bytes_recv_rate: '网络接收 IO', + }, + groups: { + disk_io_bytes_rate: '磁盘 IO', + network_io_bytes_rate: '网络 IO', + }, + timeUnits: { + s: '秒', + m: '分', + h: '小时', + d: '天', + w: '周', + M: '月', + y: '年', }, timeRanges: { - '1h': '1 小时', - '1d': '1 天', - '7d': '7 天', - '30d': '30 天', + '1h': '过去 1 小时', + '24h': '过去 24 小时', + '7d': '过去 7 天', + '30d': '过去 30 天', }, }, }; diff --git a/src/interfaces/components/metric/MetricGroup.d.ts b/src/interfaces/components/metric/MetricGroup.d.ts new file mode 100644 index 0000000000000..896764702274f --- /dev/null +++ b/src/interfaces/components/metric/MetricGroup.d.ts @@ -0,0 +1,7 @@ +export declare global { + interface MetricGroup { + name: string; + label: string; + metrics: keyof Metric[]; + } +} diff --git a/src/interfaces/i18n/components/node.d.ts b/src/interfaces/i18n/components/node.d.ts index 932ba87ab3bac..e471394a627d6 100644 --- a/src/interfaces/i18n/components/node.d.ts +++ b/src/interfaces/i18n/components/node.d.ts @@ -59,9 +59,22 @@ interface LComponentsNode { network_bytes_sent_rate: string; network_bytes_recv_rate: string; }; + groups: { + disk_io_bytes_rate: string; + network_io_bytes_rate: string; + }; + timeUnits: { + s: string; + m: string; + h: string; + d: string; + w: string; + M: string; + y: string; + }; timeRanges: { '1h': string; - '1d': string; + '24h': string; '7d': string; '30d': string; }; diff --git a/src/layouts/content/detail/DetailLayout.vue b/src/layouts/content/detail/DetailLayout.vue index 6f1cc30cf3526..547587783e3c9 100644 --- a/src/layouts/content/detail/DetailLayout.vue +++ b/src/layouts/content/detail/DetailLayout.vue @@ -49,6 +49,7 @@ const { showActionsToggleTooltip, tabs: defaultTabs, contentContainerStyle, + navActions, onNavSidebarSelect, onNavSidebarToggle, onNavTabsSelect, @@ -73,6 +74,8 @@ onMounted(() => { navSidebar.value.scroll(activeId.value); }); +onMounted(() => {}); + // reset form before unmount onBeforeUnmount(() => { if (!activeTabName.value) { diff --git a/src/layouts/content/detail/useDetail.ts b/src/layouts/content/detail/useDetail.ts index a22688b4ebd02..16681c9b65653 100644 --- a/src/layouts/content/detail/useDetail.ts +++ b/src/layouts/content/detail/useDetail.ts @@ -55,8 +55,11 @@ const useDetail = (ns: ListStoreNamespace) => { }); const contentContainerStyle = computed(() => { + const navActionsHeight = navActions.value + ? ` - ${navActions.value?.getHeight()}` + : ''; return { - height: `calc(100% - var(--cl-nav-tabs-height) - 1px${navActions.value ? ' - ' + navActions.value.getHeight() : ''})`, + height: `calc(100% - var(--cl-nav-tabs-height) - 1px${navActionsHeight})`, }; }); diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000000000..adddbac7c6698 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,6 @@ +export const getTimeUnitParts = (timeUnit: string) => { + const groups = timeUnit.match(/(\d+)([a-z])/); + const num = parseInt(groups[1]); + const unit = groups[2]; + return { num, unit }; +}; diff --git a/src/views/node/detail/tabs/NodeDetailTabMonitoring.vue b/src/views/node/detail/tabs/NodeDetailTabMonitoring.vue index 93c98c094049d..83739a3c818d9 100644 --- a/src/views/node/detail/tabs/NodeDetailTabMonitoring.vue +++ b/src/views/node/detail/tabs/NodeDetailTabMonitoring.vue @@ -5,6 +5,7 @@ import { ChartData, ChartOptions } from 'chart.js'; import useRequest from '@/services/request'; import { translate } from '@/utils'; import useDetail from '@/layouts/content/detail/useDetail'; +import { getTimeUnitParts } from '@/utils/time'; const { get } = useRequest(); @@ -15,13 +16,13 @@ const ns = 'node'; const { activeId } = useDetail(ns); const timeRange = ref('1h'); -const timeRanges = ['1h', '1d', '7d', '30d']; -const timeUnits = ['5m', '15m', '6h', '1d']; +const timeRanges = ['1h', '24h', '7d', '30d']; +const timeUnits = ['5m', '1h', '6h', '1d']; const timeRangeOptions = computed(() => { return timeRanges.map((value, index) => { const label = t('components.node.metric.timeRanges.' + value); const timeUnit = timeUnits[index]; - const groups = timeUnit.match(/(\d+)([a-z])/); + const groups = timeRange.value.match(/(\d+)([a-z])/); const num = parseInt(groups[1]); const unit = groups[2]; const start = dayjs().add(-num, unit).toISOString(); @@ -46,9 +47,7 @@ const startEnd = computed(() => { return { start, end }; }); const spanGaps = computed(() => { - const groups = timeUnit.value.match(/(\d+)([a-z])/); - const num = parseInt(groups[1]); - const unit = groups[2]; + const { num, unit } = getTimeUnitParts(timeUnit.value); switch (unit) { case 'm': return num * 60 * 1000; @@ -58,34 +57,105 @@ const spanGaps = computed(() => { return num * 24 * 60 * 60 * 1000; } }); +const timeTooltipFormat = computed(() => { + const { unit } = getTimeUnitParts(timeUnit.value); + switch (unit) { + case 'm': + return 'LLL dd HH:mm:ss'; + case 'h': + return 'LLL dd HH:mm'; + case 'd': + return 'LLL dd'; + } +}); +const timeDisplayFormats = computed(() => { + const { unit } = getTimeUnitParts(timeUnit.value); + switch (unit) { + case 'm': + case 'h': + return { + minute: 'HH:mm', + hour: 'HH:mm', + day: 'MMM dd', + }; + case 'd': + return { + minute: 'HH:mm', + hour: 'HH:mm', + day: 'MMM dd', + }; + } +}); -const allMetricNames = [ - 'cpu_usage_percent', - 'total_memory', - 'available_memory', - 'used_memory', - 'used_memory_percent', - 'total_disk', - 'available_disk', - 'used_disk', - 'used_disk_percent', - 'disk_read_bytes_rate', - 'disk_write_bytes_rate', - 'network_bytes_sent_rate', - 'network_bytes_recv_rate', +const allMetricGroups: MetricGroup[] = [ + { + name: 'cpu_usage_percent', // 'cpu_usage_percent + label: t('components.node.metric.metrics.cpu_usage_percent'), + metrics: ['cpu_usage_percent'], + }, + { + name: 'used_memory_percent', + label: t('components.node.metric.metrics.used_memory_percent'), + metrics: ['used_memory_percent'], + }, + { + name: 'used_disk_percent', + label: t('components.node.metric.metrics.used_disk_percent'), + metrics: ['used_disk_percent'], + }, + { + name: 'disk_io_bytes_rate', + label: t('components.node.metric.groups.disk_io_bytes_rate'), + metrics: ['disk_read_bytes_rate', 'disk_write_bytes_rate'], + }, + { + name: 'network_io_bytes_rate', + label: t('components.node.metric.groups.network_io_bytes_rate'), + metrics: ['network_bytes_recv_rate', 'network_bytes_sent_rate'], + }, + { + name: 'total_memory', + label: t('components.node.metric.metrics.total_memory'), + metrics: ['total_memory'], + }, + { + name: 'available_memory', + label: t('components.node.metric.metrics.available_memory'), + metrics: ['available_memory'], + }, + { + name: 'used_memory', + label: t('components.node.metric.metrics.used_memory'), + metrics: ['used_memory'], + }, + { + name: 'total_disk', + label: t('components.node.metric.metrics.total_disk'), + metrics: ['total_disk'], + }, + { + label: t('components.node.metric.metrics.available_disk'), + metrics: ['available_disk'], + }, + { + name: 'used_disk', + label: t('components.node.metric.metrics.used_disk'), + metrics: ['used_disk'], + }, ]; -const metricNames = ref([ +const metricGroups = ref([ 'cpu_usage_percent', 'used_memory_percent', 'used_disk_percent', + 'disk_io_bytes_rate', + 'network_io_bytes_rate', ]); const metricOptions = computed(() => { - return allMetricNames.map(value => ({ - value, - label: t('components.node.metric.metrics.' + value), - })); + return allMetricGroups.map(({ label, name: value }: MetricGroup) => { + return { label, value }; + }); }); const metricsTimeSeriesData = ref([]); @@ -98,40 +168,67 @@ const getMetricsTimeSeriesData = async () => { start, end, time_unit: timeUnit.value, - metric_names: metricNames.value.join(','), + metric_names: metricGroups.value + .map(groupName => { + const metricGroup = allMetricGroups.find( + ({ name }) => name === groupName + ); + return metricGroup?.metrics?.join(','); + }) + .filter(m => !!m) + .join(','), } ); metricsTimeSeriesData.value = res.data; }; +const colorPalettes = [ + '#409eff', + '#e6a23c', + '#67c23a', + '#f56c6c', + '#909399', + '#0bb2d4', + '#9c27b0', + '#ff5722', + '#795548', + '#607d8b', +]; + const getLineChartData = (name: keyof Metric): ChartData => { const labels: string[] = metricsTimeSeriesData.value.map( ({ _id }: Metric) => new Date(_id) ); - const data: number[] = metricsTimeSeriesData.value.map( - (metric: Metric) => metric[name] + const { metrics } = allMetricGroups.find( + ({ name: groupName }) => groupName === name ); - return { labels, - datasets: [ - { - label: t('components.node.metric.metrics.' + name), + datasets: metrics?.map((m, index) => { + const color = colorPalettes[index % colorPalettes.length]; + const data: number[] = metricsTimeSeriesData.value.map( + (metric: Metric) => metric[m] + ); + return { + label: t('components.node.metric.metrics.' + m), data, - borderColor: '#409eff', - backgroundColor: '#409eff', + borderColor: color, + backgroundColor: color, spanGaps: spanGaps.value, // 允许跨越空数据点 tension: 0.1, - }, - ], + }; + }), }; }; -const getLineChartOptions = (name: keyof Metric): ChartOptions<'line'> => { +const getLineChartOptions = (groupName: string): ChartOptions<'line'> => { + const { label, name } = allMetricGroups.find( + ({ name }) => name === groupName + ); return { plugins: { title: { - text: t('components.node.metric.metrics.' + name), + text: label, padding: { bottom: 20, }, @@ -143,8 +240,13 @@ const getLineChartOptions = (name: keyof Metric): ChartOptions<'line'> => { callbacks: { label: function (tooltipItem) { let value: string; - if (name.match(/(percent|rate)$/)) { + if (name.match(/(percent)$/)) { value = Math.round(tooltipItem.raw) + '%'; + } else if (name.match(/(rate)$/)) { + value = + formatBytes(tooltipItem.raw) + + ' / ' + + t('components.node.metric.timeUnits.s'); } else { value = formatBytes(tooltipItem.raw); } @@ -158,16 +260,11 @@ const getLineChartOptions = (name: keyof Metric): ChartOptions<'line'> => { type: 'time', time: { minUnit: 'minute', - tooltipFormat: 'LLL dd HH:mm', - displayFormats: { - minute: 'HH:mm', - hour: 'HH:mm', - day: 'MMM DD', - }, + tooltipFormat: timeTooltipFormat.value, + displayFormats: timeDisplayFormats.value, }, ticks: { source: 'auto', - stepSize: 5, }, title: { display: false, @@ -180,9 +277,11 @@ const getLineChartOptions = (name: keyof Metric): ChartOptions<'line'> => { beginAtZero: true, ticks: { callback: function (value) { - if (name.match(/(percent|rate)$/)) { + if (name.match(/(percent)$/)) { + if (!value) return '0%'; return Math.round(value) + '%'; } else { + if (!value) return 0; return formatBytes(value); } }, @@ -211,7 +310,7 @@ onBeforeMount(() => { onBeforeMount(() => { clearInterval(handle); }); -watch(metricNames, getMetricsTimeSeriesData); +watch(metricGroups, getMetricsTimeSeriesData); watch(timeUnit, getMetricsTimeSeriesData); defineOptions({ name: 'ClNodeDetailTabMonitoring' }); @@ -222,6 +321,9 @@ defineOptions({ name: 'ClNodeDetailTabMonitoring' });
+
+
- - - - - +
+ + + + + +