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 <[email protected]>
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)
+}