This is an automated email from the ASF dual-hosted git repository.

wuzhiguo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/bigtop-manager.git


The following commit(s) were added to refs/heads/main by this push:
     new c49aa517 BIGTOP-4466: Add charts for cluster and host metrics (#242)
c49aa517 is described below

commit c49aa517fdbdb12f0c1ba82600a8394ba3844a5d
Author: Fdefined <55788435+fu-des...@users.noreply.github.com>
AuthorDate: Sun Jul 13 13:26:28 2025 +0800

    BIGTOP-4466: Add charts for cluster and host metrics (#242)
---
 .../src/{utils/storage.ts => api/metrics/index.ts} |  25 +--
 .../src/{utils/storage.ts => api/metrics/types.ts} |  34 ++--
 .../src/components/charts/category-chart.vue       | 144 ++++++++++-----
 .../src/components/charts/gauge-chart.vue          |  16 +-
 .../src/components/service-management/overview.vue |   5 +-
 bigtop-manager-ui/src/composables/use-chart.ts     |  16 +-
 bigtop-manager-ui/src/locales/en_US/overview.ts    |   4 +-
 bigtop-manager-ui/src/locales/zh_CN/overview.ts    |   4 +-
 .../src/pages/cluster-manage/cluster/overview.vue  | 110 +++++++-----
 .../src/pages/cluster-manage/hosts/overview.vue    | 197 ++++++++++++---------
 bigtop-manager-ui/src/utils/storage.ts             |  39 +++-
 11 files changed, 365 insertions(+), 229 deletions(-)

diff --git a/bigtop-manager-ui/src/utils/storage.ts 
b/bigtop-manager-ui/src/api/metrics/index.ts
similarity index 60%
copy from bigtop-manager-ui/src/utils/storage.ts
copy to bigtop-manager-ui/src/api/metrics/index.ts
index 1d35b5b1..e3502554 100644
--- a/bigtop-manager-ui/src/utils/storage.ts
+++ b/bigtop-manager-ui/src/api/metrics/index.ts
@@ -17,22 +17,13 @@
  * under the License.
  */
 
-export const formatFromByte = (value: number): string => {
-  if (isNaN(value)) {
-    return ''
-  }
+import { get } from '@/api/request-util'
+import type { MetricsData, TimeRangeType } from './types'
 
-  if (value < 1024) {
-    return `${value} B`
-  } else if (value < 1024 ** 2) {
-    return `${(value / 1024).toFixed(2)} KB`
-  } else if (value < 1024 ** 3) {
-    return `${(value / 1024 ** 2).toFixed(2)} MB`
-  } else if (value < 1024 ** 4) {
-    return `${(value / 1024 ** 3).toFixed(2)} GB`
-  } else if (value < 1024 ** 5) {
-    return `${(value / 1024 ** 4).toFixed(2)} TB`
-  } else {
-    return `${(value / 1024 ** 5).toFixed(2)} PB`
-  }
+export const getClusterMetricsInfo = (paramsPath: { id: number }, params: { 
interval: TimeRangeType }) => {
+  return get<MetricsData>(`/metrics/clusters/${paramsPath.id}`, params)
+}
+
+export const getHostMetricsInfo = (paramsPath: { id: number }, params: { 
interval: TimeRangeType }) => {
+  return get<MetricsData>(`/metrics/hosts/${paramsPath.id}`, params)
 }
diff --git a/bigtop-manager-ui/src/utils/storage.ts 
b/bigtop-manager-ui/src/api/metrics/types.ts
similarity index 60%
copy from bigtop-manager-ui/src/utils/storage.ts
copy to bigtop-manager-ui/src/api/metrics/types.ts
index 1d35b5b1..86e4ec30 100644
--- a/bigtop-manager-ui/src/utils/storage.ts
+++ b/bigtop-manager-ui/src/api/metrics/types.ts
@@ -17,22 +17,20 @@
  * under the License.
  */
 
-export const formatFromByte = (value: number): string => {
-  if (isNaN(value)) {
-    return ''
-  }
-
-  if (value < 1024) {
-    return `${value} B`
-  } else if (value < 1024 ** 2) {
-    return `${(value / 1024).toFixed(2)} KB`
-  } else if (value < 1024 ** 3) {
-    return `${(value / 1024 ** 2).toFixed(2)} MB`
-  } else if (value < 1024 ** 4) {
-    return `${(value / 1024 ** 3).toFixed(2)} GB`
-  } else if (value < 1024 ** 5) {
-    return `${(value / 1024 ** 4).toFixed(2)} TB`
-  } else {
-    return `${(value / 1024 ** 5).toFixed(2)} PB`
-  }
+export type TimeRangeType = '1m' | '5m' | '15m' | '30m' | '1h' | '2h'
+export type MetricsData = {
+  cpuUsageCur: string
+  memoryUsageCur: string
+  diskUsageCur: string
+  fileDescriptorUsage: string
+  diskReadCur: string
+  diskWriteCur: string
+  cpuUsage: string[]
+  systemLoad1: string[]
+  systemLoad5: string[]
+  systemLoad15: string[]
+  memoryUsage: string[]
+  diskRead: string[]
+  diskWrite: string[]
+  timestamps: string[]
 }
diff --git a/bigtop-manager-ui/src/components/charts/category-chart.vue 
b/bigtop-manager-ui/src/components/charts/category-chart.vue
index c7efe490..19a11619 100644
--- a/bigtop-manager-ui/src/components/charts/category-chart.vue
+++ b/bigtop-manager-ui/src/components/charts/category-chart.vue
@@ -20,25 +20,49 @@
 <script setup lang="ts">
   import dayjs from 'dayjs'
   import { computed, onMounted, toRefs, watchEffect } from 'vue'
+  import { roundFixed } from '@/utils/storage'
   import { type EChartsOption, useChart } from '@/composables/use-chart'
 
-  const props = defineProps<{
+  interface Props {
     chartId: string
     title: string
-    data?: any[]
-    timeDistance?: string
-  }>()
+    data?: any
+    legendMap?: [string, string][] | undefined
+    config?: EChartsOption
+    xAxisData?: string[]
+    formatter?: {
+      yAxis?: (value: unknown) => string
+      tooltip?: (value: unknown) => string
+    }
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    legendMap: undefined,
+    xAxisData: () => {
+      return []
+    },
+    data: () => {
+      return {}
+    },
+    config: () => {
+      return {}
+    },
+    formatter: () => {
+      return {}
+    }
+  })
 
-  const { data, chartId, title, timeDistance } = toRefs(props)
+  const { data, chartId, title, config, legendMap, xAxisData, formatter } = 
toRefs(props)
   const { initChart, setOptions } = useChart()
+  const baseConfig = { type: 'line' }
 
   const option = computed(
     (): EChartsOption => ({
       grid: {
-        top: '20px',
+        top: '30px',
         left: '40px',
         right: '30px',
-        bottom: '20px'
+        bottom: '30px'
       },
       tooltip: {
         trigger: 'axis',
@@ -47,16 +71,12 @@
         textStyle: {
           color: '#fff'
         },
-        axisPointer: {
-          type: 'cross',
-          crossStyle: {
-            color: '#999'
-          }
-        }
+        formatter: createTooltipFormatter(formatter.value.tooltip)
       },
       xAxis: [
         {
           type: 'category',
+          boundaryGap: false,
           data: [],
           axisPointer: {
             type: 'line'
@@ -69,18 +89,11 @@
       yAxis: [
         {
           type: 'value',
-          axisPointer: {
-            type: 'shadow',
-            label: {
-              formatter: '{value} %'
-            }
-          },
-          min: 0,
-          max: 100,
-          interval: 20,
           axisLabel: {
+            width: 32,
             fontSize: 8,
-            formatter: '{value} %'
+            overflow: 'truncate',
+            formatter: formatter.value.yAxis ?? '{value} %'
           }
         }
       ],
@@ -98,31 +111,50 @@
     })
   )
 
-  const intervalToMs = (interval: string): number => {
-    const unit = interval.replace(/\d+/g, '')
-    const value = parseInt(interval)
-
-    switch (unit) {
-      case 'm':
-        return value * 60 * 1000
-      case 'h':
-        return value * 60 * 60 * 1000
-      default:
-        throw new Error('Unsupported interval: ' + interval)
-    }
+  const defaultTooltipFormatter = (val: unknown) => {
+    const num = roundFixed(val)
+    return num ? `${num} %` : '--'
   }
 
-  const getTimePoints = (interval: string = '15m'): string[] => {
-    const now = dayjs()
-    const gap = intervalToMs(interval)
-    const result: string[] = []
+  const tooltipHtml = (item: any) => {
+    return `
+          <div style="display: flex; justify-content: space-between; 
align-items: center; margin-bottom: 4px; gap: 12px">
+            <div style="display: flex; align-items: center;">
+              <div>${item.marker}${item.seriesName}</div>
+            </div>
+            <div>${item.valueText}</div>
+          </div>
+        `
+  }
 
-    for (let i = 5; i >= 0; i--) {
-      const time = now.subtract(i * gap, 'millisecond')
-      result.push(time.format('HH:mm'))
+  const createTooltipFormatter = (formatValue?: (value: unknown) => string) => 
{
+    const format = formatValue ?? defaultTooltipFormatter
+    console.log('format :>> ', format)
+    return (params: any) => {
+      const title = params[0]?.axisValueLabel ?? ''
+      const lines = params
+        .map((item: any) => {
+          const valueText = format(item.value)
+          return tooltipHtml({ ...item, valueText })
+        })
+        .join('')
+      return `<div style="margin-bottom: 4px;">${title}</div>${lines}`
     }
+  }
 
-    return result
+  /**
+   * Generates ECharts series config by mapping legend keys to data and 
formatting values.
+   *
+   * @param data - A partial object containing data arrays for each series key.
+   * @param legendMap - An array of [key, displayName] pairs.
+   * @returns An array of ECharts series config objects with populated and 
formatted data.
+   */
+  const generateChartSeries = <T,>(data: Partial<T>, legendMap: [string, 
string][]) => {
+    return legendMap.map(([key, name]) => ({
+      name,
+      ...baseConfig,
+      data: (data[key] || []).map((v: unknown) => roundFixed(v))
+    }))
   }
 
   onMounted(() => {
@@ -131,14 +163,28 @@
   })
 
   watchEffect(() => {
-    setOptions({
-      xAxis: [{ data: getTimePoints(timeDistance.value) || [] }]
-    })
-  })
+    let series = [] as any,
+      legend = [] as any
+
+    if (legendMap.value) {
+      legend = new Map(legendMap.value).values()
+      series = generateChartSeries(data.value, legendMap.value)
+    } else {
+      series = [
+        {
+          name: title.value.toLowerCase(),
+          data: data.value.map((v) => roundFixed(v))
+        }
+      ]
+    }
 
-  watchEffect(() => {
     setOptions({
-      series: [{ data: [{ value: data.value ?? [] }] }]
+      xAxis: xAxisData.value
+        ? [{ data: xAxisData.value?.map((v) => dayjs(Number(v) * 
1000).format('HH:mm')) || [] }]
+        : [],
+      ...config.value,
+      legend,
+      series
     })
   })
 </script>
diff --git a/bigtop-manager-ui/src/components/charts/gauge-chart.vue 
b/bigtop-manager-ui/src/components/charts/gauge-chart.vue
index 946da83c..099867c3 100644
--- a/bigtop-manager-ui/src/components/charts/gauge-chart.vue
+++ b/bigtop-manager-ui/src/components/charts/gauge-chart.vue
@@ -19,15 +19,17 @@
 
 <script setup lang="ts">
   import { onMounted, shallowRef, toRefs, watchEffect } from 'vue'
+
   import { type EChartsOption, useChart } from '@/composables/use-chart'
+  import { roundFixed } from '@/utils/storage'
 
   interface GaugeChartProps {
     chartId: string
     title: string
-    percent?: number
+    percent?: string
   }
 
-  const props = withDefaults(defineProps<GaugeChartProps>(), { percent: 0 })
+  const props = withDefaults(defineProps<GaugeChartProps>(), { percent: '0.00' 
})
   const { percent, chartId, title } = toRefs(props)
   const { initChart, setOptions } = useChart()
 
@@ -77,13 +79,13 @@
         },
         detail: {
           valueAnimation: true,
-          formatter: '{value}%',
           color: 'inherit',
-          fontSize: 18
+          fontSize: 18,
+          formatter: (val: number) => `${roundFixed(val, 2, '0.00', false)}%`
         },
         data: [
           {
-            value: 0 * 100
+            value: 0.0
           }
         ]
       }
@@ -96,9 +98,7 @@
   })
 
   watchEffect(() => {
-    setOptions({
-      series: [{ data: [{ value: percent.value.toFixed(2) === 'NaN' ? 0 : 
percent.value.toFixed(2) }] }]
-    })
+    setOptions({ series: [{ data: [{ value: percent.value }] }] })
   })
 </script>
 
diff --git a/bigtop-manager-ui/src/components/service-management/overview.vue 
b/bigtop-manager-ui/src/components/service-management/overview.vue
index 3cabfb30..69e29e21 100644
--- a/bigtop-manager-ui/src/components/service-management/overview.vue
+++ b/bigtop-manager-ui/src/components/service-management/overview.vue
@@ -23,6 +23,7 @@
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import GaugeChart from '@/components/charts/gauge-chart.vue'
   import CategoryChart from '@/components/charts/category-chart.vue'
+  import { Empty } from 'ant-design-vue'
   import type { ServiceVO, ServiceStatusType } from '@/api/service/types'
 
   type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
@@ -166,7 +167,7 @@
         </div>
         <template v-if="noChartData">
           <div class="box-empty">
-            <a-empty />
+            <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
           </div>
         </template>
         <a-row v-else class="box-content">
@@ -213,7 +214,7 @@
 
     &-content {
       border-radius: 8px;
-      overflow: hidden;
+      overflow: visible;
       box-sizing: border-box;
       border: 1px solid $color-border;
     }
diff --git a/bigtop-manager-ui/src/composables/use-chart.ts 
b/bigtop-manager-ui/src/composables/use-chart.ts
index c0c61aad..2ca626cc 100644
--- a/bigtop-manager-ui/src/composables/use-chart.ts
+++ b/bigtop-manager-ui/src/composables/use-chart.ts
@@ -19,23 +19,33 @@
 
 import * as echarts from 'echarts/core'
 import { GaugeChart, GaugeSeriesOption, LineChart, LineSeriesOption } from 
'echarts/charts'
-import { GridComponent, GridComponentOption, TooltipComponent, 
TooltipComponentOption } from 'echarts/components'
+import {
+  TitleComponent,
+  GridComponent,
+  GridComponentOption,
+  TooltipComponent,
+  TooltipComponentOption,
+  LegendComponent,
+  LegendComponentOption
+} from 'echarts/components'
 import { UniversalTransition } from 'echarts/features'
 import { CanvasRenderer } from 'echarts/renderers'
 import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
 
 export type EChartsOption = echarts.ComposeOption<
-  GaugeSeriesOption | GridComponentOption | TooltipComponentOption | 
LineSeriesOption
+  GaugeSeriesOption | GridComponentOption | TooltipComponentOption | 
LineSeriesOption | LegendComponentOption
 >
 
 echarts.use([
+  TitleComponent,
   GaugeChart,
   CanvasRenderer,
   GridComponent,
   LineChart,
   TooltipComponent,
   CanvasRenderer,
-  UniversalTransition
+  UniversalTransition,
+  LegendComponent
 ])
 
 export const useChart = () => {
diff --git a/bigtop-manager-ui/src/locales/en_US/overview.ts 
b/bigtop-manager-ui/src/locales/en_US/overview.ts
index 4446a038..78b5039a 100644
--- a/bigtop-manager-ui/src/locales/en_US/overview.ts
+++ b/bigtop-manager-ui/src/locales/en_US/overview.ts
@@ -49,5 +49,7 @@ export default {
   service_name: 'Name',
   service_version: 'Version',
   metrics: 'Metrics',
-  kerberos: 'Kerberos'
+  kerberos: 'Kerberos',
+  system_load: 'System Load',
+  disk_io: 'Disk I/O'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/overview.ts 
b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
index 9e6ddd83..2d3fcc7c 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/overview.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
@@ -49,5 +49,7 @@ export default {
   service_name: '服务名',
   service_version: '服务版本',
   metrics: '指标监控',
-  kerberos: 'Kerberos'
+  kerberos: 'Kerberos',
+  system_load: '系统负载',
+  disk_io: '磁盘 I/O'
 }
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
index 3a20ab30..69b71723 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
@@ -18,7 +18,7 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onActivated, ref, shallowRef, toRefs, watchEffect } from 
'vue'
+  import { computed, onActivated, onDeactivated, onUnmounted, ref, shallowRef, 
toRefs, watchEffect } from 'vue'
   import { useI18n } from 'vue-i18n'
   import { storeToRefs } from 'pinia'
   import { formatFromByte } from '@/utils/storage'
@@ -30,6 +30,8 @@
   import { useClusterStore } from '@/store/cluster'
   import { Empty } from 'ant-design-vue'
   import { useRoute } from 'vue-router'
+  import { getClusterMetricsInfo } from '@/api/metrics'
+  import { useIntervalFn } from '@vueuse/core'
 
   import GaugeChart from '@/components/charts/gauge-chart.vue'
   import CategoryChart from '@/components/charts/category-chart.vue'
@@ -39,12 +41,7 @@
   import type { MenuItem } from '@/store/menu/types'
   import type { StackVO } from '@/api/stack/types'
   import type { Command } from '@/api/command/types'
-
-  type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
-  type TimeRangeItem = {
-    text: TimeRangeText
-    time: string
-  }
+  import type { MetricsData, TimeRangeType } from '@/api/metrics/types'
 
   const props = defineProps<{ payload: ClusterVO }>()
   const emits = defineEmits<{ (event: 'update:payload', value: ClusterVO): 
void }>()
@@ -55,21 +52,19 @@
   const stackStore = useStackStore()
   const serviceStore = useServiceStore()
   const clusterStore = useClusterStore()
-  const currTimeRange = ref<TimeRangeText>('15m')
-  const chartData = ref({
-    chart1: [],
-    chart2: [],
-    chart3: [],
-    chart4: []
-  })
+
+  const currTimeRange = ref<TimeRangeType>('5m')
+  const chartData = ref<Partial<MetricsData>>({})
+
+  const timeRanges = shallowRef<TimeRangeType[]>(['1m', '5m', '15m', '30m', 
'1h', '2h'])
+  const locateStackWithService = shallowRef<StackVO[]>([])
   const statusColors = shallowRef<Record<ClusterStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
     2: 'unhealthy',
     3: 'unknown'
   })
-  const { serviceNames } = storeToRefs(serviceStore)
-  const locateStackWithService = shallowRef<StackVO[]>([])
 
+  const { serviceNames } = storeToRefs(serviceStore)
   const { payload } = toRefs(props)
 
   const clusterDetail = computed(() => ({
@@ -78,17 +73,6 @@
     totalDisk: formatFromByte(payload.value.totalDisk as number)
   }))
 
-  const clusterId = computed(() => route.params.id as unknown as number)
-  const noChartData = computed(() => Object.values(chartData.value).every((v) 
=> v.length === 0))
-  const timeRanges = computed((): TimeRangeItem[] => [
-    { text: '1m', time: '' },
-    { text: '15m', time: '' },
-    { text: '30m', time: '' },
-    { text: '1h', time: '' },
-    { text: '6h', time: '' },
-    { text: '30h', time: '' }
-  ])
-
   const baseConfig = computed(
     (): Partial<Record<keyof ClusterVO, string>> => ({
       status: t('overview.cluster_status'),
@@ -111,12 +95,15 @@
     })
   )
 
+  const serviceOperates = computed(() => ({
+    Start: t('common.start', [t('common.service')]),
+    Restart: t('common.restart', [t('common.service')]),
+    Stop: t('common.stop', [t('common.service')])
+  }))
+
+  const clusterId = computed(() => route.params.id as unknown as number)
+  const noChartData = computed(() => Object.values(chartData.value).length === 
0)
   const detailKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ClusterVO)[])
-  const serviceOperates = computed(() => [
-    { action: 'Start', text: t('common.start', [t('common.service')]) },
-    { action: 'Restart', text: t('common.restart', [t('common.service')]) },
-    { action: 'Stop', text: t('common.stop', [t('common.service')]) }
-  ])
 
   const handleServiceOperate = async (item: MenuItem, service: ServiceVO) => {
     try {
@@ -131,19 +118,38 @@
     }
   }
 
-  const handleTimeRange = (time: TimeRangeItem) => {
-    currTimeRange.value = time.text
+  const handleTimeRange = (time: TimeRangeType) => {
+    if (currTimeRange.value !== time) {
+      currTimeRange.value = time
+      getClusterMetrics()
+    }
   }
 
   const servicesFromCurrentCluster = (stack: StackVO) => {
     return stack.services.filter((v) => serviceNames.value.includes(v.name))
   }
 
+  const getClusterMetrics = async () => {
+    try {
+      chartData.value = await getClusterMetricsInfo({ id: clusterId.value }, { 
interval: currTimeRange.value })
+    } catch (error) {
+      console.log('Failed to fetch cluster metrics:', error)
+    }
+  }
+
+  const { pause, resume } = useIntervalFn(getClusterMetrics, 30000, { 
immediate: true })
+
   onActivated(async () => {
     await clusterStore.getClusterDetail(clusterId.value)
     emits('update:payload', clusterStore.currCluster)
+    getClusterMetrics()
+    resume()
   })
 
+  onDeactivated(() => pause())
+
+  onUnmounted(() => pause())
+
   watchEffect(() => {
     locateStackWithService.value = stackStore.stacks.filter((item) =>
       item.services.some((service) => service.name && 
serviceNames.value.includes(service.name))
@@ -238,8 +244,8 @@
                 </a-button>
                 <template #overlay>
                   <a-menu @click="handleServiceOperate($event, service)">
-                    <a-menu-item v-for="operate in serviceOperates" 
:key="operate.action">
-                      <span>{{ operate.text }}</span>
+                    <a-menu-item v-for="[operate, text] of 
Object.entries(serviceOperates)" :key="operate">
+                      <span>{{ text }}</span>
                     </a-menu-item>
                   </a-menu>
                 </template>
@@ -254,40 +260,54 @@
           <a-space :size="12">
             <div
               v-for="time in timeRanges"
-              :key="time.text"
+              :key="time"
               tabindex="0"
               class="time-range"
-              :class="{ 'time-range-activated': currTimeRange === time.text }"
+              :class="{ 'time-range-activated': currTimeRange === time }"
               @click="handleTimeRange(time)"
             >
-              {{ time.text }}
+              {{ time }}
             </div>
           </a-space>
         </div>
         <template v-if="noChartData">
           <div class="box-empty">
-            <a-empty />
+            <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
           </div>
         </template>
         <a-row v-else class="box-content">
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart1" 
:title="$t('overview.memory_usage')" />
+              <gauge-chart
+                chart-id="chart1"
+                :percent="chartData?.memoryUsageCur"
+                :title="$t('overview.memory_usage')"
+              />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart2" :title="$t('overview.cpu_usage')" 
/>
+              <gauge-chart chart-id="chart2" :percent="chartData?.cpuUsageCur" 
:title="$t('overview.cpu_usage')" />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <category-chart chart-id="chart4" 
:title="$t('overview.cpu_usage')" />
+              <category-chart
+                chart-id="chart3"
+                :x-axis-data="chartData?.timestamps"
+                :data="chartData?.memoryUsage ?? []"
+                :title="$t('overview.memory_usage')"
+              />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <category-chart chart-id="chart3" 
:title="$t('overview.memory_usage')" />
+              <category-chart
+                chart-id="chart4"
+                :x-axis-data="chartData?.timestamps"
+                :data="chartData?.cpuUsage ?? []"
+                :title="$t('overview.cpu_usage')"
+              />
             </div>
           </a-col>
         </a-row>
@@ -313,7 +333,7 @@
 
     &-content {
       border-radius: 8px;
-      overflow: hidden;
+      overflow: visible;
       box-sizing: border-box;
       border: 1px solid $color-border;
     }
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
index ce1b8059..78d1495d 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -18,7 +18,7 @@
 -->
 
 <script setup lang="ts">
-  import { computed, ref, shallowRef, toRefs, watch } from 'vue'
+  import { computed, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'
   import { useI18n } from 'vue-i18n'
   import { Empty } from 'ant-design-vue'
   import { formatFromByte } from '@/utils/storage.ts'
@@ -29,72 +29,43 @@
   import { useStackStore } from '@/store/stack'
   import { getComponentsByHost } from '@/api/hosts'
   import { Command } from '@/api/command/types'
+  import { getHostMetricsInfo } from '@/api/metrics'
+  import { useIntervalFn } from '@vueuse/core'
+
   import GaugeChart from '@/components/charts/gauge-chart.vue'
   import CategoryChart from '@/components/charts/category-chart.vue'
+
+  import type { MenuItem } from '@/store/menu/types'
   import type { HostStatusType, HostVO } from '@/api/hosts/types.ts'
   import type { ClusterStatusType } from '@/api/cluster/types.ts'
   import type { ComponentVO } from '@/api/component/types.ts'
-  import type { MenuItem } from '@/store/menu/types'
+  import type { MetricsData, TimeRangeType } from '@/api/metrics/types'
 
-  type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
-  type TimeRangeItem = {
-    text: TimeRangeText
-    time: string
-  }
+  type StatusColorType = Record<HostStatusType, keyof typeof CommonStatusTexts>
 
   interface Props {
     hostInfo: HostVO
   }
 
   const props = defineProps<Props>()
-  const { hostInfo } = toRefs(props)
+
   const { t } = useI18n()
   const stackStore = useStackStore()
   const serviceStore = useServiceStore()
   const jobProgressStore = useJobProgress()
-  const currTimeRange = ref<TimeRangeText>('15m')
-  const statusColors = shallowRef<Record<HostStatusType, keyof typeof 
CommonStatusTexts>>({
-    1: 'healthy',
-    2: 'unhealthy',
-    3: 'unknown'
-  })
-  const chartData = ref({
-    chart1: [],
-    chart2: [],
-    chart3: [],
-    chart4: []
-  })
+
+  const currTimeRange = ref<TimeRangeType>('5m')
+  const chartData = ref<Partial<MetricsData>>({})
+
   const componentsFromCurrentHost = shallowRef<Map<string, ComponentVO[]>>(new 
Map())
-  const needFormatFormByte = computed(() => ['totalMemorySize', 'totalDisk'])
-  const noChartData = computed(() => Object.values(chartData.value).every((v) 
=> v.length === 0))
-  const timeRanges = computed((): TimeRangeItem[] => [
-    {
-      text: '1m',
-      time: ''
-    },
-    {
-      text: '15m',
-      time: ''
-    },
-    {
-      text: '30m',
-      time: ''
-    },
-    {
-      text: '1h',
-      time: ''
-    },
-    {
-      text: '6h',
-      time: ''
-    },
-    {
-      text: '30h',
-      time: ''
-    }
-  ])
-  const baseConfig = computed((): Partial<Record<keyof HostVO, string>> => {
-    return {
+  const needFormatFormByte = shallowRef(['totalMemorySize', 'totalDisk'])
+  const timeRanges = shallowRef<TimeRangeType[]>(['1m', '5m', '15m', '30m', 
'1h', '2h'])
+  const statusColors = shallowRef<StatusColorType>({ 1: 'healthy', 2: 
'unhealthy', 3: 'unknown' })
+
+  const { hostInfo } = toRefs(props)
+
+  const baseConfig = computed(
+    (): Partial<Record<keyof HostVO, string>> => ({
       status: t('overview.host_status'),
       hostname: t('overview.hostname'),
       desc: t('overview.host_desc'),
@@ -107,29 +78,24 @@
       availableProcessors: t('overview.core_count'),
       totalMemorySize: t('overview.memory'),
       totalDisk: t('overview.disk_size')
-    }
-  })
+    })
+  )
+
   const unitOfBaseConfig = computed(
     (): Partial<Record<keyof HostVO, string>> => ({
       componentNum: t('overview.unit_component'),
       availableProcessors: t('overview.unit_core')
     })
   )
+
+  const componentOperates = computed(() => ({
+    Start: t('common.start', [t('common.component')]),
+    Restart: t('common.restart', [t('common.component')]),
+    Stop: t('common.stop', [t('common.component')])
+  }))
+
   const detailKeys = computed((): (keyof HostVO)[] => 
Object.keys(baseConfig.value))
-  const componentOperates = computed(() => [
-    {
-      action: 'Start',
-      text: t('common.start', [t('common.component')])
-    },
-    {
-      action: 'Restart',
-      text: t('common.restart', [t('common.component')])
-    },
-    {
-      action: 'Stop',
-      text: t('common.stop', [t('common.component')])
-    }
-  ])
+  const noChartData = computed(() => Object.values(chartData.value).length === 
0)
 
   const handleHostOperate = async (item: MenuItem, component: ComponentVO) => {
     const { serviceName } = component
@@ -153,8 +119,20 @@
     }
   }
 
-  const handleTimeRange = (time: TimeRangeItem) => {
-    currTimeRange.value = time.text
+  const handleTimeRange = (time: TimeRangeType) => {
+    if (currTimeRange.value == time) {
+      return
+    }
+    currTimeRange.value = time
+    getHostMetrics()
+  }
+
+  const getHostMetrics = async () => {
+    try {
+      chartData.value = await getHostMetricsInfo({ id: hostInfo.value.id! }, { 
interval: currTimeRange.value })
+    } catch (error) {
+      console.log('Failed to fetch host metrics:', error)
+    }
   }
 
   const getComponentInfo = async () => {
@@ -173,14 +151,24 @@
     }
   }
 
+  const { pause, resume } = useIntervalFn(getHostMetrics, 30000, { immediate: 
true })
+
   watch(
     () => hostInfo.value,
     (val) => {
       if (val.id) {
         getComponentInfo()
+        getHostMetrics()
+        resume()
+      } else {
+        pause()
       }
     }
   )
+
+  onUnmounted(() => {
+    pause()
+  })
 </script>
 
 <template>
@@ -272,8 +260,8 @@
                 </a-button>
                 <template #overlay>
                   <a-menu @click="handleHostOperate($event, comp)">
-                    <a-menu-item v-for="operate in componentOperates" 
:key="operate.action">
-                      <span>{{ operate.text }}</span>
+                    <a-menu-item v-for="[operate, text] of 
Object.entries(componentOperates)" :key="operate">
+                      <span>{{ text }}</span>
                     </a-menu-item>
                   </a-menu>
                 </template>
@@ -288,40 +276,91 @@
           <a-space :size="12">
             <div
               v-for="time in timeRanges"
-              :key="time.text"
+              :key="time"
               tabindex="0"
               class="time-range"
-              :class="{ 'time-range-activated': currTimeRange === time.text }"
+              :class="{ 'time-range-activated': currTimeRange === time }"
               @click="handleTimeRange(time)"
             >
-              {{ time.text }}
+              {{ time }}
             </div>
           </a-space>
         </div>
         <template v-if="noChartData">
           <div class="box-empty">
-            <a-empty />
+            <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
           </div>
         </template>
         <a-row v-else class="box-content">
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart1" 
:title="$t('overview.memory_usage')" />
+              <gauge-chart
+                chart-id="chart1"
+                :percent="chartData?.memoryUsageCur"
+                :title="$t('overview.memory_usage')"
+              />
+            </div>
+          </a-col>
+          <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+            <div class="chart-item-wrp">
+              <gauge-chart chart-id="chart2" :percent="chartData?.cpuUsageCur" 
:title="$t('overview.cpu_usage')" />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart2" :title="$t('overview.cpu_usage')" 
/>
+              <category-chart
+                chart-id="chart3"
+                :x-axis-data="chartData?.timestamps"
+                :data="chartData?.memoryUsage"
+                :title="$t('overview.memory_usage')"
+              />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <category-chart chart-id="chart4" 
:title="$t('overview.cpu_usage')" />
+              <category-chart
+                chart-id="chart4"
+                :x-axis-data="chartData?.timestamps"
+                :data="chartData?.cpuUsage"
+                :title="$t('overview.cpu_usage')"
+              />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <category-chart chart-id="chart3" 
:title="$t('overview.memory_usage')" />
+              <category-chart
+                chart-id="chart5"
+                :x-axis-data="chartData?.timestamps"
+                :data="chartData"
+                :title="$t('overview.system_load')"
+                :formatter="{
+                  tooltip: (val) => `${val == null || val == '' ? '--' : val}`,
+                  yAxis: (val) => `${val}`
+                }"
+                :legend-map="[
+                  ['systemLoad1', 'load1'],
+                  ['systemLoad5', 'load5'],
+                  ['systemLoad15', 'load15']
+                ]"
+              />
+            </div>
+          </a-col>
+          <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+            <div class="chart-item-wrp">
+              <category-chart
+                chart-id="chart6"
+                :x-axis-data="chartData?.timestamps"
+                :data="chartData"
+                :title="$t('overview.disk_io')"
+                :formatter="{
+                  tooltip: (val) => `${val == null || val == '' ? '--' : 
formatFromByte(val as number, 0)}`,
+                  yAxis: (val) => formatFromByte(val as number, 0)
+                }"
+                :legend-map="[
+                  ['diskRead', 'read'],
+                  ['diskWrite', 'write']
+                ]"
+              />
             </div>
           </a-col>
         </a-row>
@@ -347,7 +386,7 @@
 
     &-content {
       border-radius: 8px;
-      overflow: hidden;
+      overflow: visible;
       box-sizing: border-box;
       border: 1px solid $color-border;
     }
diff --git a/bigtop-manager-ui/src/utils/storage.ts 
b/bigtop-manager-ui/src/utils/storage.ts
index 1d35b5b1..2e32d25e 100644
--- a/bigtop-manager-ui/src/utils/storage.ts
+++ b/bigtop-manager-ui/src/utils/storage.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-export const formatFromByte = (value: number): string => {
+export const formatFromByte = (value: number, decimals = 2): string => {
   if (isNaN(value)) {
     return ''
   }
@@ -25,14 +25,41 @@ export const formatFromByte = (value: number): string => {
   if (value < 1024) {
     return `${value} B`
   } else if (value < 1024 ** 2) {
-    return `${(value / 1024).toFixed(2)} KB`
+    return `${(value / 1024).toFixed(decimals)} KB`
   } else if (value < 1024 ** 3) {
-    return `${(value / 1024 ** 2).toFixed(2)} MB`
+    return `${(value / 1024 ** 2).toFixed(decimals)} MB`
   } else if (value < 1024 ** 4) {
-    return `${(value / 1024 ** 3).toFixed(2)} GB`
+    return `${(value / 1024 ** 3).toFixed(decimals)} GB`
   } else if (value < 1024 ** 5) {
-    return `${(value / 1024 ** 4).toFixed(2)} TB`
+    return `${(value / 1024 ** 4).toFixed(decimals)} TB`
   } else {
-    return `${(value / 1024 ** 5).toFixed(2)} PB`
+    return `${(value / 1024 ** 5).toFixed(decimals)} PB`
   }
 }
+
+/**
+ * Safely rounds a value to a fixed number of decimal places.
+ *
+ * @param num - The value to round.
+ * @param decimals - Decimal places to keep (default: 2).
+ * @param fallback - Fallback string if value is not finite (default: '0.00').
+ * @param preserveEmpty - If true, returns null or '' as-is; otherwise, falls 
back (default: true).
+ * @returns Rounded string, fallback, or original empty/null input.
+ */
+export const roundFixed = (
+  num: unknown,
+  decimals = 2,
+  fallback = '0.00',
+  preserveEmpty = true
+): string | null | undefined => {
+  if (preserveEmpty && (num === '' || num === null || num === undefined)) {
+    return num
+  }
+
+  const n = Number(num)
+  if (!isFinite(n)) return fallback
+
+  const factor = 10 ** decimals
+  const rounded = Math.round((n + Number.EPSILON) * factor) / factor
+  return rounded.toFixed(decimals)
+}


Reply via email to