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 6e907c43 BIGTOP-4509: Fix query wrong components for a service (#281)
6e907c43 is described below

commit 6e907c43fd19f926755bc94ebd8cbdc91ea011c5
Author: Fdefined <[email protected]>
AuthorDate: Tue Oct 14 18:49:05 2025 +0800

    BIGTOP-4509: Fix query wrong components for a service (#281)
---
 .../create-cluster/components/check-workflow.vue   |  13 +--
 .../components/component-installer.vue             |  14 +--
 bigtop-manager-ui/src/features/job-modal/index.vue |  10 +-
 bigtop-manager-ui/src/features/job/index.vue       |  36 +++----
 .../src/features/metric/category-chart.vue         |  17 ++--
 .../src/features/metric/gauge-chart.vue            |  13 +--
 .../src/features/service-management/components.vue |  75 +++++++++------
 .../src/features/service-management/configs.vue    |  29 +++---
 .../src/features/service-management/index.vue      |  78 +++++++--------
 .../src/features/service-management/overview.vue   | 107 ++++++++++++++-------
 .../src/pages/cluster-manage/cluster/host.vue      |  23 +++--
 .../src/pages/cluster-manage/cluster/index.vue     |  35 ++++---
 .../src/pages/cluster-manage/cluster/overview.vue  |  96 +++++++++---------
 .../src/pages/cluster-manage/cluster/service.vue   |  35 +++----
 .../src/pages/cluster-manage/cluster/user.vue      |  10 +-
 .../src/pages/cluster-manage/hosts/detail.vue      |  11 +--
 .../src/pages/cluster-manage/hosts/index.vue       |  12 +--
 .../src/pages/cluster-manage/hosts/overview.vue    |  42 +++++---
 .../pages/cluster-manage/infrastructures/index.vue |   2 +-
 .../cluster-manage/infrastructures/service.vue     |  49 +++++-----
 bigtop-manager-ui/src/store/tab-state/index.ts     |  29 ++++--
 bigtop-manager-ui/src/utils/constant.ts            |  30 ++++++
 22 files changed, 422 insertions(+), 344 deletions(-)

diff --git 
a/bigtop-manager-ui/src/features/create-cluster/components/check-workflow.vue 
b/bigtop-manager-ui/src/features/create-cluster/components/check-workflow.vue
index da18fbae..48ad2b7b 100644
--- 
a/bigtop-manager-ui/src/features/create-cluster/components/check-workflow.vue
+++ 
b/bigtop-manager-ui/src/features/create-cluster/components/check-workflow.vue
@@ -20,6 +20,7 @@
 <script setup lang="ts">
   import { Empty } from 'ant-design-vue'
   import { getJobDetails, retryJob } from '@/api/job'
+  import { JOB_STATUS } from '@/utils/constant'
 
   import LogsView, { type LogViewProps } from '@/features/log-view/index.vue'
 
@@ -39,14 +40,6 @@
     open: false
   })
 
-  const status = shallowRef<Record<StateType, string>>({
-    Pending: 'installing',
-    Processing: 'processing',
-    Failed: 'failed',
-    Canceled: 'canceled',
-    Successful: 'success'
-  })
-
   const stages = computed(() => {
     if (jobDetail.value.stages) {
       return [...jobDetail.value.stages].sort((a, b) => a.order! - b.order!)
@@ -144,14 +137,14 @@
             <div class="stage-item">
               <span>{{ stage.name }}</span>
               <div style="min-width: 100px">
-                <svg-icon :name="stage.state && 
status[stage.state]"></svg-icon>
+                <svg-icon :name="stage.state && 
JOB_STATUS[stage.state]"></svg-icon>
               </div>
             </div>
           </template>
           <div v-for="task in stage.tasks" :key="task.id" class="task-item">
             <span>{{ task.name }}</span>
             <a-space :size="16">
-              <svg-icon :name="task.state && status[task.state]"></svg-icon>
+              <svg-icon :name="task.state && 
JOB_STATUS[task.state]"></svg-icon>
               <div style="min-width: 62px">
                 <a-button
                   v-if="task.state && !['Canceled', 
'Pending'].includes(task.state)"
diff --git 
a/bigtop-manager-ui/src/features/create-service/components/component-installer.vue
 
b/bigtop-manager-ui/src/features/create-service/components/component-installer.vue
index be1f9de2..08826e43 100644
--- 
a/bigtop-manager-ui/src/features/create-service/components/component-installer.vue
+++ 
b/bigtop-manager-ui/src/features/create-service/components/component-installer.vue
@@ -21,6 +21,8 @@
   import { Empty } from 'ant-design-vue'
   import { getJobDetails, retryJob } from '@/api/job'
   import { CommandVO } from '@/api/command/types'
+
+  import { JOB_STATUS } from '@/utils/constant'
   import { useCreateServiceStore } from '@/store/create-service'
 
   import LogsView, { type LogViewProps } from '@/features/log-view/index.vue'
@@ -31,6 +33,7 @@
 
   const { t } = useI18n()
   const createStore = useCreateServiceStore()
+
   const activeKey = ref<number[]>([])
   const jobDetail = ref<JobVO>({})
   const spinning = ref(false)
@@ -38,13 +41,6 @@
   const logsViewState = reactive<LogViewProps>({
     open: false
   })
-  const status = shallowRef<Record<StateType, string>>({
-    Pending: 'installing',
-    Processing: 'processing',
-    Failed: 'failed',
-    Canceled: 'canceled',
-    Successful: 'success'
-  })
 
   const stages = computed(() => {
     if (jobDetail.value.stages) {
@@ -144,14 +140,14 @@
             <div class="stage-item">
               <span>{{ stage.name }}</span>
               <div style="min-width: 100px">
-                <svg-icon :name="stage.state && 
status[stage.state]"></svg-icon>
+                <svg-icon :name="stage.state && 
JOB_STATUS[stage.state]"></svg-icon>
               </div>
             </div>
           </template>
           <div v-for="task in stage.tasks" :key="task.id" class="task-item">
             <span>{{ task.name }}</span>
             <a-space :size="16">
-              <svg-icon :name="task.state && status[task.state]"></svg-icon>
+              <svg-icon :name="task.state && 
JOB_STATUS[task.state]"></svg-icon>
               <div style="min-width: 62px">
                 <a-button
                   v-if="task.state && !['Canceled', 
'Pending'].includes(task.state)"
diff --git a/bigtop-manager-ui/src/features/job-modal/index.vue 
b/bigtop-manager-ui/src/features/job-modal/index.vue
index 5f95b32f..14ef730c 100644
--- a/bigtop-manager-ui/src/features/job-modal/index.vue
+++ b/bigtop-manager-ui/src/features/job-modal/index.vue
@@ -20,6 +20,7 @@
 <script setup lang="ts">
   import { TableColumnType, TableProps } from 'ant-design-vue'
   import LogsView, { type LogViewProps } from '@/features/log-view/index.vue'
+  import { JOB_STATUS } from '@/utils/constant'
 
   import type { JobVO, StageVO, StateType, TaskListParams, TaskVO } from 
'@/api/job/types'
   import type { CommandRes, JobStageProgressItem } from '@/store/job-progress'
@@ -43,13 +44,6 @@
 
   const { t } = useI18n()
   const breadcrumbs = ref<BreadcrumbItem[]>([])
-  const status = shallowRef<Record<StateType, string>>({
-    Pending: 'installing',
-    Processing: 'processing',
-    Failed: 'failed',
-    Canceled: 'canceled',
-    Successful: 'success'
-  })
   const apiMap = shallowRef([{ key: 'jobId' }, { key: 'stageId' }])
 
   const logsViewState = reactive<LogViewProps>({ open: false })
@@ -212,7 +206,7 @@
         </template>
         <template v-if="column.key === 'state'">
           <job-progress v-if="breadcrumbLen === 1" :key="record.id" 
:state="text" :progress-data="record.progress" />
-          <svg-icon v-else :name="status[record.state as 
StateType]"></svg-icon>
+          <svg-icon v-else :name="JOB_STATUS[record.state as 
StateType]"></svg-icon>
         </template>
       </template>
     </a-table>
diff --git a/bigtop-manager-ui/src/features/job/index.vue 
b/bigtop-manager-ui/src/features/job/index.vue
index 9003f5ce..ff7f5c40 100644
--- a/bigtop-manager-ui/src/features/job/index.vue
+++ b/bigtop-manager-ui/src/features/job/index.vue
@@ -20,11 +20,11 @@
 <script setup lang="ts">
   import { TableColumnType, TableProps } from 'ant-design-vue'
   import { getJobList, getStageList, getTaskList, retryJob } from '@/api/job'
+  import { POLLING_INTERVAL } from '@/utils/constant'
 
   import LogsView, { type LogViewProps } from '@/features/log-view/index.vue'
 
   import type { JobVO, StageVO, StateType, TaskListParams, TaskVO } from 
'@/api/job/types'
-  import type { ClusterVO } from '@/api/cluster/types'
   import type { ListParams } from '@/api/types'
 
   interface BreadcrumbItem {
@@ -33,16 +33,16 @@
     pagination: ListParams
   }
 
-  const POLLING_INTERVAL = 3000
   const { t } = useI18n()
+  const route = useRoute()
   const { confirmModal } = useModal()
 
-  const clusterInfo = useAttrs() as ClusterVO
+  const clusterId = ref(Number(route.params.id ?? 0))
   const pollingIntervalId = ref<any>(null)
   const breadcrumbs = ref<BreadcrumbItem[]>([
     {
       name: computed(() => t('job.job_list')),
-      id: `clusterId-${clusterInfo.id}`,
+      id: `clusterId-${clusterId.value}`,
       pagination: {
         pageNum: 1,
         pageSize: 10,
@@ -51,19 +51,15 @@
     }
   ])
   const apiMap = ref([
-    {
-      key: 'clusterId',
-      api: getJobList
-    },
-    {
-      key: 'jobId',
-      api: getStageList
-    },
-    {
-      key: 'stageId',
-      api: getTaskList
-    }
+    { key: 'clusterId', api: getJobList },
+    { key: 'jobId', api: getStageList },
+    { key: 'stageId', api: getTaskList }
   ])
+
+  const logsViewState = reactive<LogViewProps>({
+    open: false
+  })
+
   const status = shallowRef<Record<StateType, string>>({
     Pending: 'installing',
     Processing: 'processing',
@@ -71,9 +67,7 @@
     Canceled: 'canceled',
     Successful: 'success'
   })
-  const logsViewState = reactive<LogViewProps>({
-    open: false
-  })
+
   const breadcrumbLen = computed(() => breadcrumbs.value.length)
   const currBreadcrumb = computed(() => breadcrumbs.value.at(-1))
   const columns = computed((): TableColumnType[] => [
@@ -217,7 +211,7 @@
     getListData(true)
     pollingIntervalId.value = setInterval(() => {
       getListData()
-    }, POLLING_INTERVAL)
+    }, POLLING_INTERVAL / 10)
   }
 
   const stopPolling = () => {
@@ -232,7 +226,7 @@
       tipText: t('job.retry'),
       async onOk() {
         try {
-          const state = await retryJob({ jobId: row.id!, clusterId: 
clusterInfo.id! })
+          const state = await retryJob({ jobId: row.id!, clusterId: 
clusterId.value! })
           row.state = state.state
         } catch (error) {
           console.log('error :>> ', error)
diff --git a/bigtop-manager-ui/src/features/metric/category-chart.vue 
b/bigtop-manager-ui/src/features/metric/category-chart.vue
index 5185df2e..fa9dbfd7 100644
--- a/bigtop-manager-ui/src/features/metric/category-chart.vue
+++ b/bigtop-manager-ui/src/features/metric/category-chart.vue
@@ -155,25 +155,26 @@
     }))
   }
 
-  onMounted(() => {
-    const selector = document.getElementById(`${chartId.value}`)
-    if (selector) {
-      initChart(selector!, option.value)
-    }
-  })
+  const renderChart = () => {
+    const el = document.getElementById(chartId.value)
+    if (el) initChart(el, option.value)
+  }
+
+  onMounted(renderChart)
+  onActivated(renderChart)
 
   watchEffect(() => {
     let series = [] as any,
       legend = { data: [] } as any
 
-    const { series: temp_series = [] } = data.value
+    const { series: temp_series = [], valueType } = data.value
     const xAxis = xAxisData.value?.map((v) => dayjs(Number(v) * 
1000).format('HH:mm')) ?? []
 
     if (legendMap.value) {
       legend = new Map(legendMap.value).values()
       series = generateChartSeries(data.value, legendMap.value)
     } else {
-      if (temp_series.length > 1) {
+      if (valueType) {
         legend.data = temp_series.map((s) => s.name)
         series = [...temp_series]
       } else {
diff --git a/bigtop-manager-ui/src/features/metric/gauge-chart.vue 
b/bigtop-manager-ui/src/features/metric/gauge-chart.vue
index 73ca8e63..0974c41a 100644
--- a/bigtop-manager-ui/src/features/metric/gauge-chart.vue
+++ b/bigtop-manager-ui/src/features/metric/gauge-chart.vue
@@ -90,12 +90,13 @@
     ]
   })
 
-  onMounted(() => {
-    const selector = document.getElementById(`${chartId.value}`)
-    if (selector) {
-      initChart(document.getElementById(`${chartId.value}`)!, option.value)
-    }
-  })
+  const renderChart = () => {
+    const el = document.getElementById(chartId.value)
+    if (el) initChart(el, option.value)
+  }
+
+  onMounted(renderChart)
+  onActivated(renderChart)
 
   watchEffect(() => {
     setOptions({ series: [{ data: [{ value: percent.value }] }] })
diff --git a/bigtop-manager-ui/src/features/service-management/components.vue 
b/bigtop-manager-ui/src/features/service-management/components.vue
index e0653d35..38c75023 100644
--- a/bigtop-manager-ui/src/features/service-management/components.vue
+++ b/bigtop-manager-ui/src/features/service-management/components.vue
@@ -22,6 +22,9 @@
   import { deleteComponent, getComponents } from '@/api/component'
   import { useStackStore } from '@/store/stack'
   import { useJobProgress } from '@/store/job-progress'
+  import { useTabStore } from '@/store/tab-state'
+
+  import { COMPONENT_STATUS, POLLING_INTERVAL } from '@/utils/constant'
 
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { ComponentVO } from '@/api/component/types'
@@ -30,6 +33,7 @@
   import type { ServiceVO } from '@/api/service/types'
 
   type Key = string | number
+
   interface TableState {
     selectedRowKeys: Key[]
     searchText: string
@@ -37,21 +41,25 @@
     selectedRows: ComponentVO[]
   }
 
-  const POLLING_INTERVAL = 3000
+  interface RouteParams {
+    id: number
+    serviceId: number
+  }
 
   const { t } = useI18n()
-  const { confirmModal } = useModal()
-
-  const jobProgressStore = useJobProgress()
-  const stackStore = useStackStore()
+  const attrs = useAttrs() as Partial<ServiceVO>
   const route = useRoute()
   const router = useRouter()
-  const attrs = useAttrs() as unknown as Required<ServiceVO> & { clusterId: 
number }
+  const tabStore = useTabStore()
+  const jobProgressStore = useJobProgress()
+  const stackStore = useStackStore()
+  const { confirmModal } = useModal()
 
   const { stacks, stackRelationMap } = storeToRefs(stackStore)
+
   const searchInputRef = ref()
   const pollingIntervalId = ref<any>(null)
-  const componentStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
+  const currTab = ref(tabStore.getActiveTab(route.path) ?? '1')
 
   const commandRequest = shallowRef<CommandRequest>({
     command: 'Add',
@@ -66,17 +74,24 @@
     selectedRows: []
   })
 
-  const componentsFromStack = computed(
-    () =>
-      new Map(
-        stacks.value.flatMap(
-          (stack) =>
-            stack.services?.flatMap((service) =>
-              service.name && service.components ? [[service.name, 
service.components]] : []
-            ) ?? []
-        )
-      )
-  )
+  const payload = computed(() => {
+    const { id, serviceId } = route.params as unknown as RouteParams
+    return [id, serviceId] as [number, number]
+  })
+
+  const componentsFromStack = computed(() => {
+    const entries: [string, ComponentVO[]][] = []
+
+    stacks.value.forEach(({ services = [] }) => {
+      services.forEach(({ name, components }) => {
+        if (name && components) {
+          entries.push([name, components])
+        }
+      })
+    })
+
+    return new Map(entries)
+  })
 
   const columns = computed((): TableColumnType<ComponentVO>[] => [
     {
@@ -85,7 +100,7 @@
       key: 'name',
       ellipsis: true,
       filterMultiple: false,
-      filters: [...(componentsFromStack.value.get(attrs.name)?.values() || 
[])]?.map((v) => ({
+      filters: [...(componentsFromStack.value.get(attrs.name!)?.values() || 
[])]?.map((v) => ({
         text: v?.displayName || '',
         value: v?.name || ''
       }))
@@ -182,7 +197,7 @@
           text: t('common.stop', [t('common.selected')])
         }
       ],
-      dropdownMenuClickEvent: (info) => dropdownMenuClick && 
dropdownMenuClick(info)
+      dropdownMenuClickEvent: (info) => dropdownMenuClick!(info)
     }
   ])
 
@@ -234,9 +249,10 @@
   }
 
   const execOperation = (rows?: ComponentVO[]) => {
+    const [clusterId] = payload.value
     const displayNameOfRows: string[] = rows ? rows.map((v) => v.displayName 
?? '').filter((v) => v) : []
     jobProgressStore.processCommand(
-      { ...commandRequest.value, clusterId: attrs.clusterId },
+      { ...commandRequest.value, clusterId },
       async () => {
         getComponentList(true, true)
         state.selectedRowKeys = []
@@ -251,7 +267,8 @@
       tipText: t('common.delete_msg'),
       async onOk() {
         try {
-          const data = await deleteComponent({ clusterId: attrs.clusterId, id: 
row.id! })
+          const [clusterId] = payload.value
+          const data = await deleteComponent({ clusterId, id: row.id! })
           if (data) {
             message.success(t('common.delete_success'))
             getComponentList(true, true)
@@ -264,7 +281,7 @@
   }
 
   const getComponentList = async (isReset = false, isFirstCall = false) => {
-    const { clusterId, id: serviceId } = attrs
+    const [clusterId, serviceId] = payload.value
     if (!paginationProps.value) {
       loading.value = false
       return
@@ -297,7 +314,7 @@
     getComponentList(isReset, isFirstCall)
     pollingIntervalId.value = setInterval(() => {
       getComponentList()
-    }, POLLING_INTERVAL)
+    }, POLLING_INTERVAL / 10)
   }
 
   const stopPolling = () => {
@@ -308,8 +325,9 @@
   }
 
   const addComponent = () => {
-    const creationMode = Number(attrs.clusterId) === 0 ? 'public' : 'internal'
-    const routerName = Number(attrs.clusterId) === 0 ? 'CreateInfraComponent' 
: 'CreateComponent'
+    const [clusterId] = payload.value
+    const creationMode = clusterId === 0 ? 'public' : 'internal'
+    const routerName = clusterId === 0 ? 'CreateInfraComponent' : 
'CreateComponent'
     router.push({
       name: routerName,
       params: { ...route.params, creationMode, type: 'component' }
@@ -317,6 +335,7 @@
   }
 
   onActivated(() => {
+    if (currTab.value != '2') return
     startPolling()
   })
 
@@ -369,8 +388,8 @@
       </template>
       <template #bodyCell="{ record, column }">
         <template v-if="['status'].includes(column.key as string)">
-          <svg-icon style="margin-left: 0" 
:name="componentStatus[record.status].toLowerCase()" />
-          <span>{{ t(`common.${componentStatus[record.status].toLowerCase()}`) 
}}</span>
+          <svg-icon style="margin-left: 0" 
:name="COMPONENT_STATUS[record.status].toLowerCase()" />
+          <span>{{ 
t(`common.${COMPONENT_STATUS[record.status].toLowerCase()}`) }}</span>
         </template>
         <template v-if="column.key === 'quickLink'">
           <span v-if="!record.quickLink">{{ t('common.no_link') }}</span>
diff --git a/bigtop-manager-ui/src/features/service-management/configs.vue 
b/bigtop-manager-ui/src/features/service-management/configs.vue
index a1db8c52..0378abee 100644
--- a/bigtop-manager-ui/src/features/service-management/configs.vue
+++ b/bigtop-manager-ui/src/features/service-management/configs.vue
@@ -29,9 +29,15 @@
 
   type FormStateType = { configs: ServiceConfig[] }
 
+  interface RouteParams {
+    id: number
+    serviceId: number
+  }
+
   const { t } = useI18n()
   const createStore = useCreateServiceStore()
-  const attrs = useAttrs() as unknown as Required<ServiceVO> & { clusterId: 
number }
+  const attrs = useAttrs() as Partial<ServiceVO>
+  const route = useRoute()
 
   const getServiceDetail = inject('getServiceDetail') as () => any
 
@@ -63,6 +69,11 @@
     }
   })
 
+  const payload = computed(() => {
+    const { id: clusterId, serviceId: id } = route.params as unknown as 
RouteParams
+    return { clusterId, id }
+  })
+
   /**
    * Filters service configurations based on a search keyword.
    * Only includes configurations with properties matching the keyword.
@@ -130,13 +141,11 @@
   const saveConfigs = async () => {
     try {
       const valid = await validate()
-      if (!valid) {
-        return
-      }
-      const { id, clusterId } = attrs
+      if (!valid) return
+
       loading.value = true
       const params = createStore.getDiffConfigs(configs.value, 
snapshotConfigs.value)
-      const data = await updateServiceConfigs({ id, clusterId }, params)
+      const data = await updateServiceConfigs({ ...payload.value }, params)
       if (data) {
         message.success(t('common.update_success'))
         getServiceDetail()
@@ -162,18 +171,16 @@
   }
 
   const onCaptureSnapshot = () => {
-    const { id, clusterId } = attrs
-    captureRef.value?.handleOpen({ id, clusterId })
+    captureRef.value?.handleOpen({ ...payload.value })
   }
 
   const openSnapshotManagement = () => {
-    const { id, clusterId } = attrs
-    snapshotRef.value?.handleOpen({ id, clusterId })
+    snapshotRef.value?.handleOpen({ ...payload.value })
   }
 
   onActivated(async () => {
     await getServiceDetail()
-    configs.value = createStore.injectKeysToConfigs(attrs.configs)
+    configs.value = createStore.injectKeysToConfigs(attrs.configs ?? [])
     snapshotConfigs.value = JSON.parse(JSON.stringify(attrs.configs))
   })
 </script>
diff --git a/bigtop-manager-ui/src/features/service-management/index.vue 
b/bigtop-manager-ui/src/features/service-management/index.vue
index d0679b95..7c3e2769 100644
--- a/bigtop-manager-ui/src/features/service-management/index.vue
+++ b/bigtop-manager-ui/src/features/service-management/index.vue
@@ -20,7 +20,7 @@
 <script setup lang="ts">
   import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
-  import { Command } from '@/api/command/types'
+  import { Command, type CommandRequest } from '@/api/command/types'
 
   import Overview from './overview.vue'
   import Components from './components.vue'
@@ -35,30 +35,30 @@
     serviceId: number
   }
 
+  type Key = keyof typeof Command | 'Remove'
+
   const { t } = useI18n()
   const route = useRoute()
   const router = useRouter()
   const serviceStore = useServiceStore()
   const jobProgressStore = useJobProgress()
+  const { activeTab } = useTabState(route.path, '1')
   const { loading, serviceMap } = storeToRefs(serviceStore)
 
-  const activeKey = ref('1')
   const serviceDetail = shallowRef<ServiceVO>()
+  const stepPages = shallowRef([Overview, Components, Configs])
+
+  const getCompName = computed(() => stepPages.value[parseInt(activeTab.value) 
- 1])
+
+  const componentPayload = computed(() => {
+    const { id, serviceId } = route.params as unknown as RouteParams
+    return [id, serviceId] as [number, number]
+  })
 
-  const routeParams = computed(() => route.params as unknown as RouteParams)
   const tabs = computed((): TabItem[] => [
-    {
-      key: '1',
-      title: t('common.overview')
-    },
-    {
-      key: '2',
-      title: t('common.component')
-    },
-    {
-      key: '3',
-      title: t('common.configs')
-    }
+    { key: '1', title: t('common.overview') },
+    { key: '2', title: t('common.component') },
+    { key: '3', title: t('common.configs') }
   ])
 
   const actionGroup = computed<GroupItem[]>(() => [
@@ -72,44 +72,37 @@
         { action: 'Stop', text: t('common.stop', [t('common.service')]) },
         { action: 'Remove', text: t('common.remove', [t('common.service')]), 
divider: true, danger: true }
       ],
-      dropdownMenuClickEvent: (info) => dropdownMenuClick && 
dropdownMenuClick(info)
+      dropdownMenuClickEvent: (info) => dropdownMenuClick!(info)
     }
   ])
 
-  const getCompName = computed(() => {
-    const components = [Overview, Components, Configs]
-    return components[parseInt(activeKey.value) - 1]
-  })
+  const onServiceDeleted = (clusterId: number) => {
+    router.replace({ path: `/cluster-manage/clusters/${clusterId}` })
+  }
 
   const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key 
}) => {
-    const { id: clusterId, serviceId } = routeParams.value
-    const service = serviceMap.value[clusterId].filter((service) => 
Number(serviceId) === service.id)[0]
+    const [clusterId, serviceId] = componentPayload.value
+    const service = serviceMap.value[clusterId].filter((s) => 
Number(serviceId) == s.id)[0]
+    const { name: serviceName, displayName } = service
+
+    const processParams = {
+      command: key as Key,
+      clusterId,
+      commandLevel: 'service',
+      serviceCommands: [{ serviceName, installed: true }]
+    } as CommandRequest
 
     if (key === 'Remove') {
-      serviceStore.removeService(service, clusterId, () => {
-        router.replace({ path: `/cluster-manage/clusters/${clusterId}` })
-      })
+      serviceStore.removeService(service, clusterId, () => 
onServiceDeleted(clusterId))
     } else {
-      jobProgressStore.processCommand(
-        {
-          command: key as keyof typeof Command,
-          clusterId,
-          commandLevel: 'service',
-          serviceCommands: [{ serviceName: service.name!, installed: true }]
-        },
-        getServiceDetail,
-        {
-          displayName: service.displayName
-        }
-      )
+      jobProgressStore.processCommand(processParams, getServiceDetail, { 
displayName })
     }
   }
 
   const getServiceDetail = async () => {
     try {
       loading.value = true
-      const { id: clusterId, serviceId } = routeParams.value
-      serviceDetail.value = await serviceStore.getServiceDetail(clusterId, 
serviceId)
+      serviceDetail.value = await 
serviceStore.getServiceDetail(...componentPayload.value)
     } catch (error) {
       console.log('error :>> ', error)
     } finally {
@@ -132,13 +125,10 @@
       :desc="serviceDetail?.desc"
       :action-groups="actionGroup"
     />
-    <main-card v-model:active-key="activeKey" :tabs="tabs">
+    <main-card v-model:active-key="activeTab" :tabs="tabs">
       <template #tab-item>
         <keep-alive>
-          <component
-            :is="getCompName"
-            v-bind="{ ...serviceDetail, clusterId: routeParams.id, 
...routeParams }"
-          ></component>
+          <component :is="getCompName" v-bind="{ ...serviceDetail 
}"></component>
         </keep-alive>
       </template>
     </main-card>
diff --git a/bigtop-manager-ui/src/features/service-management/overview.vue 
b/bigtop-manager-ui/src/features/service-management/overview.vue
index 89e9001a..47459fc9 100644
--- a/bigtop-manager-ui/src/features/service-management/overview.vue
+++ b/bigtop-manager-ui/src/features/service-management/overview.vue
@@ -18,30 +18,28 @@
 -->
 
 <script setup lang="ts">
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import { Empty } from 'ant-design-vue'
-  import { formatFromByte } from '@/utils/storage.ts'
   import { getServiceMetricsInfo } from '@/api/metrics'
+
+  import { CommonStatus } from '@/enums/state'
+  import { formatFromByte } from '@/utils/storage.ts'
   import { isEmpty } from '@/utils/tools'
+  import { STATUS_COLOR, TIME_RANGES, POLLING_INTERVAL } from 
'@/utils/constant'
+
+  import { useTabStore } from '@/store/tab-state'
 
   import CategoryChart from '@/features/metric/category-chart.vue'
 
-  import type { ServiceVO, ServiceStatusType } from '@/api/service/types'
+  import type { ServiceVO } from '@/api/service/types'
   import type { ServiceMetricItem, ServiceMetrics, ServiceMetricType, 
TimeRangeType } from '@/api/metrics/types'
 
-  const { t } = useI18n()
-  const attrs = useAttrs() as any
-  const currTimeRange = ref<TimeRangeType>('5m')
-  const chartData = ref<Partial<ServiceMetrics>>({})
+  type RouteParams = { id: number; serviceId: number }
 
-  const timeRanges = shallowRef<TimeRangeType[]>(['1m', '5m', '15m', '30m', 
'1h', '2h'])
-  const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
-    1: 'healthy',
-    2: 'unhealthy',
-    3: 'unknown'
-  })
+  type BaseConfigType = Partial<Record<keyof ServiceVO, string>>
+
+  type UnitMapType = Record<Lowercase<ServiceMetricType>, string | ((value: 
number) => string)>
 
-  const unitMap: Record<Lowercase<ServiceMetricType>, string | ((value: 
number) => string)> = {
+  const UNIT_MAP: UnitMapType = {
     number: '',
     percent: '%',
     byte: (val) => formatFromByte(val, 0),
@@ -50,11 +48,25 @@
     nps: 'N/s'
   }
 
-  const clusterId = computed(() => attrs.id)
+  const { t } = useI18n()
+  const route = useRoute()
+  const tabStore = useTabStore()
+  const attrs = useAttrs() as Partial<ServiceVO>
+
+  const isRunning = ref(false)
+  const interval = ref<TimeRangeType>('5m')
+  const chartData = ref<Partial<ServiceMetrics>>({})
+
   const noChartData = computed(() => Object.values(chartData.value).length === 
0)
   const serviceKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ServiceVO)[])
+
+  const payload = computed(() => {
+    const { id: clusterId, serviceId } = route.params as unknown as RouteParams
+    return { clusterId, serviceId }
+  })
+
   const baseConfig = computed(
-    (): Partial<Record<keyof ServiceVO, string>> => ({
+    (): BaseConfigType => ({
       status: t('overview.service_status'),
       displayName: t('overview.service_name'),
       version: t('overview.service_version'),
@@ -65,40 +77,61 @@
     })
   )
 
-  const handleTimeRange = (time: TimeRangeType) => {
-    if (currTimeRange.value === time) return
-    currTimeRange.value = time
-    getServiceMetrics()
+  const getChartFormatter = (chart: ServiceMetricItem) => {
+    const unit = UNIT_MAP[chart.valueType]
+    const valueWithUnit = (val: any) => (typeof unit === 'function' ? unit(val 
as number) : `${val} ${unit}`)
+    return {
+      tooltip: (val: any) => `${isEmpty(val) ? '--' : valueWithUnit(val)}`,
+      yAxis: valueWithUnit
+    }
+  }
+
+  const shouldRunMetrics = () => {
+    const currTab = tabStore.getActiveTab(route.path) ?? '1'
+    const clusterId = payload.value.clusterId ?? 0
+    return clusterId != 0 && currTab === '1'
   }
 
   const getServiceMetrics = async () => {
+    if (isRunning.value) {
+      return
+    }
+
+    isRunning.value = true
+
     try {
-      const res = await getServiceMetricsInfo({ id: attrs.serviceId }, { 
interval: currTimeRange.value })
-      chartData.value = { ...res }
+      const { serviceId: id } = payload.value
+      const data = await getServiceMetricsInfo({ id }, { interval: 
interval.value })
+      chartData.value = { ...data }
     } catch (error) {
       console.log('Failed to fetch service metrics:', error)
+    } finally {
+      isRunning.value = false
     }
   }
 
-  const getChartFormatter = (chart: ServiceMetricItem) => {
-    const unit = unitMap[chart.valueType]
-    const valueWithUnit = (val: any) => (typeof unit === 'function' ? unit(val 
as number) : `${val} ${unit}`)
-    return {
-      tooltip: (val: any) => `${isEmpty(val) ? '--' : valueWithUnit(val)}`,
-      yAxis: valueWithUnit
-    }
+  const { pause, resume } = useIntervalFn(getServiceMetrics, POLLING_INTERVAL, 
{ immediate: true })
+
+  const handleTimeRange = (time: TimeRangeType) => {
+    if (interval.value === time) return
+    interval.value = time
+    if (!shouldRunMetrics()) return
+    getServiceMetrics()
+    restartMetrics()
   }
 
-  const { pause, resume } = useIntervalFn(getServiceMetrics, 30000, { 
immediate: true })
+  const restartMetrics = () => {
+    pause()
+    resume()
+  }
 
   onActivated(() => {
-    if (clusterId.value == 0) return
+    if (!shouldRunMetrics()) return
     getServiceMetrics()
     resume()
   })
 
   onDeactivated(() => {
-    if (clusterId.value == 0) return
     pause()
   })
 </script>
@@ -131,10 +164,10 @@
                         <a-tag
                           v-if="key === 'status'"
                           class="reset-tag"
-                          :color="CommonStatus[statusColors[attrs[key]]]"
+                          :color="CommonStatus[STATUS_COLOR[attrs[key]!]]"
                         >
-                          <status-dot 
:color="CommonStatus[statusColors[attrs[key]]]" />
-                          {{ attrs[key] && 
t(`common.${statusColors[attrs[key]]}`) }}
+                          <status-dot 
:color="CommonStatus[STATUS_COLOR[attrs[key]!]]" />
+                          {{ attrs[key] && 
t(`common.${STATUS_COLOR[attrs[key]]}`) }}
                         </a-tag>
                         <a-typography-text
                           v-else-if="key === 'stack'"
@@ -166,11 +199,11 @@
           <a-typography-text strong :content="t('overview.chart')" />
           <a-space :size="12">
             <div
-              v-for="time in timeRanges"
+              v-for="time in TIME_RANGES"
               :key="time"
               tabindex="0"
               class="time-range"
-              :class="{ 'time-range-activated': currTimeRange === time }"
+              :class="{ 'time-range-activated': interval === time }"
               @click="handleTimeRange(time)"
             >
               {{ time }}
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
index d360f525..b5003294 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -21,6 +21,7 @@
   import { message, TableColumnType, TableProps } from 'ant-design-vue'
   import { getHosts } from '@/api/host'
   import * as hostApi from '@/api/host'
+  import { HOST_STATUS } from '@/utils/constant'
 
   import HostCreate from '@/features/create-host/index.vue'
   import InstallDependencies from 
'@/features/create-host/install-dependencies.vue'
@@ -28,7 +29,6 @@
   import type { FilterConfirmProps, FilterResetProps } from 
'ant-design-vue/es/table/interface'
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { HostVO } from '@/api/host/types'
-  import type { ClusterVO } from '@/api/cluster/types'
   import type { HostReq } from '@/api/command/types'
 
   type Key = string | number
@@ -40,15 +40,14 @@
   }
 
   const { t } = useI18n()
-  const { confirmModal } = useModal()
-
   const router = useRouter()
-  const attrs = useAttrs() as ClusterVO
+  const route = useRoute()
+  const { confirmModal } = useModal()
 
   const searchInputRef = ref()
+  const clusterId = ref(Number(route.params.id))
   const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
   const installRef = ref<InstanceType<typeof InstallDependencies> | null>(null)
-  const hostStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
 
   const state = reactive<TableState>({
     searchText: '',
@@ -143,7 +142,7 @@
   }
 
   const handleEdit = (row: HostVO) => {
-    const formatHost = { ...row, displayName: row.clusterDisplayName, 
clusterId: attrs?.id }
+    const formatHost = { ...row, displayName: row.clusterDisplayName, 
clusterId: clusterId.value }
     hostCreateRef.value?.handleOpen('EDIT', formatHost)
   }
 
@@ -174,11 +173,11 @@
 
   const viewHostDetail = (row: HostVO) => {
     const { id: hostId } = row
-    router.push({ name: 'HostDetail', query: { hostId, clusterId: attrs.id } })
+    router.push({ name: 'HostDetail', query: { hostId, clusterId: 
clusterId.value } })
   }
 
   const addHost = () => {
-    hostCreateRef.value?.handleOpen('ADD', { clusterId: attrs.id })
+    hostCreateRef.value?.handleOpen('ADD', { clusterId: clusterId.value })
   }
 
   const afterSetupHostConfig = async (type: 'ADD' | 'EDIT', item: HostReq) => {
@@ -191,7 +190,7 @@
 
   const getHostList = async (isReset = false) => {
     loading.value = true
-    if (attrs.id == undefined || !paginationProps.value) {
+    if (clusterId.value == undefined || !paginationProps.value) {
       loading.value = false
       return
     }
@@ -199,7 +198,7 @@
       paginationProps.value.current = 1
     }
     try {
-      const res = await getHosts({ ...filtersParams.value, clusterId: attrs.id 
})
+      const res = await getHosts({ ...filtersParams.value, clusterId: 
clusterId.value })
       dataSource.value = res.content
       paginationProps.value.total = res.total
       loading.value = false
@@ -265,8 +264,8 @@
           <a-typography-link underline @click="viewHostDetail(record)"> {{ 
record.hostname }} </a-typography-link>
         </template>
         <template v-if="column.key === 'status'">
-          <svg-icon style="margin-left: 0" 
:name="hostStatus[record.status].toLowerCase()" />
-          <span>{{ t(`common.${hostStatus[record.status].toLowerCase()}`) 
}}</span>
+          <svg-icon style="margin-left: 0" 
:name="HOST_STATUS[record.status].toLowerCase()" />
+          <span>{{ t(`common.${HOST_STATUS[record.status].toLowerCase()}`) 
}}</span>
         </template>
         <template v-if="column.key === 'operation'">
           <button-group
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
index 3df28895..94c83c6e 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
@@ -19,7 +19,8 @@
 
 <script setup lang="ts">
   import { useClusterStore } from '@/store/cluster'
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { STATUS_COLOR } from '@/utils/constant'
+  import { CommonStatus } from '@/enums/state'
   import { useJobProgress } from '@/store/job-progress'
 
   import Overview from './overview.vue'
@@ -31,28 +32,22 @@
   import type { Command } from '@/api/command/types'
   import type { TabItem } from '@/components/base/main-card/types'
   import type { GroupItem } from '@/components/common/button-group/types'
-  import type { ClusterStatusType } from '@/api/cluster/types'
 
   const { t } = useI18n()
   const router = useRouter()
   const route = useRoute()
   const jobProgressStore = useJobProgress()
   const clusterStore = useClusterStore()
+  const { activeTab } = useTabState(route.path, '1')
   const { loading, currCluster } = storeToRefs(clusterStore)
 
-  const { activeTab } = useTabState(route.path, '1')
+  const clusterId = ref(Number(route.params.id))
 
-  const statusColors = shallowRef<Record<ClusterStatusType, keyof typeof 
CommonStatusTexts>>({
-    1: 'healthy',
-    2: 'unhealthy',
-    3: 'unknown'
-  })
+  const getCompName = computed(() => [Overview, Service, Host, User, 
Job][Number(activeTab.value) - 1])
 
   /**
    * Determines the component to render based on the active tab.
    */
-  const getCompName = computed(() => [Overview, Service, Host, User, 
Job][parseInt(activeTab.value) - 1])
-
   const tabs = computed((): TabItem[] => [
     { key: '1', title: t('common.overview') },
     { key: '2', title: t('common.service') },
@@ -89,12 +84,12 @@
       jobProgressStore.processCommand(
         {
           command: key as keyof typeof Command,
-          clusterId: currCluster.value.id,
+          clusterId: clusterId.value,
           commandLevel: 'cluster'
         },
         async () => {
           await clusterStore.loadClusters()
-          await clusterStore.getClusterDetail(currCluster.value.id!)
+          await getClusterInfo()
         },
         { displayName: currCluster.value.displayName }
       )
@@ -104,8 +99,16 @@
   }
 
   const addService: GroupItem['clickEvent'] = () => {
-    router.push({ name: 'CreateService', params: { id: currCluster.value.id, 
creationMode: 'internal' } })
+    router.push({ name: 'CreateService', params: { id: clusterId.value, 
creationMode: 'internal' } })
   }
+
+  const getClusterInfo = async () => {
+    await clusterStore.getClusterDetail(clusterId.value)
+  }
+
+  onMounted(async () => {
+    await getClusterInfo()
+  })
 </script>
 
 <template>
@@ -113,14 +116,14 @@
     <header-card
       :title="currCluster.displayName"
       avatar="cluster"
-      :status="CommonStatus[statusColors[currCluster.status as 
ClusterStatusType]]"
+      :status="CommonStatus[STATUS_COLOR[currCluster.status!]]"
       :desc="currCluster.desc"
       :action-groups="actionGroup"
     />
     <main-card v-model:active-key="activeTab" :tabs="tabs">
       <template #tab-item>
-        <keep-alive :key="activeTab">
-          <component :is="getCompName" v-bind="currCluster" 
v-model:payload="currCluster"></component>
+        <keep-alive :key="clusterId">
+          <component :is="getCompName" :payload="currCluster" />
         </keep-alive>
       </template>
     </main-card>
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 e7c368ee..51f13c87 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
@@ -20,13 +20,13 @@
 <script setup lang="ts">
   import { formatFromByte } from '@/utils/storage'
   import { usePngImage } from '@/utils/tools'
-
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { CommonStatus } from '@/enums/state'
+  import { TIME_RANGES, STATUS_COLOR, POLLING_INTERVAL } from 
'@/utils/constant'
 
   import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
   import { useStackStore } from '@/store/stack'
-  import { useClusterStore } from '@/store/cluster'
+  import { useTabStore } from '@/store/tab-state'
 
   import { Empty } from 'ant-design-vue'
   import { getClusterMetricsInfo } from '@/api/metrics'
@@ -34,31 +34,26 @@
   import type { ClusterStatusType, ClusterVO } from '@/api/cluster/types'
   import type { ServiceVO } from '@/api/service/types'
   import type { StackVO } from '@/api/stack/types'
-  import type { Command } from '@/api/command/types'
   import type { MetricsData, TimeRangeType } from '@/api/metrics/types'
+  import type { CommandRequest } from '@/api/command/types'
 
   const props = defineProps<{ payload: ClusterVO }>()
-  const emits = defineEmits<{ (event: 'update:payload', value: ClusterVO): 
void }>()
 
   const { t } = useI18n()
   const route = useRoute()
+  const tabStore = useTabStore()
   const jobProgressStore = useJobProgress()
   const stackStore = useStackStore()
   const serviceStore = useServiceStore()
-  const clusterStore = useClusterStore()
 
+  const isRunning = ref(false)
   const currTimeRange = ref<TimeRangeType>('5m')
+  const clusterId = ref(Number(route.params.id))
   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 { payload } = toRefs(props)
 
   const clusterDetail = computed(() => ({
@@ -95,59 +90,66 @@
     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 handleServiceOperate = (item: any, service: ServiceVO) => {
-    jobProgressStore.processCommand(
-      {
-        command: item.key as keyof typeof Command,
-        clusterId: clusterId.value,
-        commandLevel: 'service',
-        serviceCommands: [{ serviceName: service.name!, installed: true }]
-      },
-      undefined,
-      {
-        displayName: service.displayName
-      }
+  watchEffect(() => {
+    locateStackWithService.value = stackStore.stacks.filter((item) =>
+      item.services.some((service) => service.name && 
serviceNames.value.includes(service.name))
     )
+  })
+
+  const handleServiceOperate = (item: any, service: ServiceVO) => {
+    const { name, displayName } = service
+    const { key: command } = item
+    const params = {
+      command,
+      clusterId: clusterId.value,
+      commandLevel: 'service',
+      serviceCommands: [{ serviceName: name!, installed: true }]
+    } as CommandRequest
+
+    jobProgressStore.processCommand(params, undefined, { displayName })
   }
 
   const handleTimeRange = (time: TimeRangeType) => {
-    if (currTimeRange.value !== time) {
-      currTimeRange.value = time
-      getClusterMetrics()
-    }
+    if (currTimeRange.value === time) return
+    currTimeRange.value = time
+    getClusterMetrics()
+    pause()
+    resume()
   }
 
-  const servicesFromCurrentCluster = (stack: StackVO) => {
-    return stack.services.filter((v) => serviceNames.value.includes(v.name))
-  }
+  const servicesFromCurrentCluster = (stack: StackVO) =>
+    stack.services.filter((v) => serviceNames.value.includes(v.name))
 
   const getClusterMetrics = async () => {
+    if (isRunning.value) {
+      return
+    }
+
+    isRunning.value = true
+
     try {
       chartData.value = await getClusterMetricsInfo({ id: clusterId.value }, { 
interval: currTimeRange.value })
     } catch (error) {
       console.log('Failed to fetch cluster metrics:', error)
+    } finally {
+      isRunning.value = false
     }
   }
 
-  const { pause, resume } = useIntervalFn(getClusterMetrics, 30000, { 
immediate: true })
+  const { pause, resume } = useIntervalFn(getClusterMetrics, POLLING_INTERVAL, 
{ immediate: true })
 
-  onActivated(async () => {
-    await clusterStore.getClusterDetail(clusterId.value)
-    emits('update:payload', clusterStore.currCluster)
+  onActivated(() => {
+    const currTab = tabStore.getActiveTab(route.path) ?? '1'
+    if (currTab != '1') return
     getClusterMetrics()
     resume()
   })
 
-  onDeactivated(pause)
-
-  watchEffect(() => {
-    locateStackWithService.value = stackStore.stacks.filter((item) =>
-      item.services.some((service) => service.name && 
serviceNames.value.includes(service.name))
-    )
+  onDeactivated(() => {
+    pause()
   })
 </script>
 
@@ -179,11 +181,11 @@
                         <a-tag
                           v-if="base === 'status'"
                           class="reset-tag"
-                          
:color="CommonStatus[statusColors[clusterDetail[base] as ClusterStatusType]]"
+                          
:color="CommonStatus[STATUS_COLOR[clusterDetail[base] as ClusterStatusType]]"
                         >
-                          <status-dot 
:color="CommonStatus[statusColors[clusterDetail[base] as ClusterStatusType]]" />
+                          <status-dot 
:color="CommonStatus[STATUS_COLOR[clusterDetail[base] as ClusterStatusType]]" />
                           {{
-                            clusterDetail[base] && 
t(`common.${statusColors[clusterDetail[base] as ClusterStatusType]}`)
+                            clusterDetail[base] && 
t(`common.${STATUS_COLOR[clusterDetail[base] as ClusterStatusType]}`)
                           }}
                         </a-tag>
                         <a-typography-text
@@ -252,7 +254,7 @@
           <a-typography-text strong :content="t('overview.chart')" />
           <a-space :size="12">
             <div
-              v-for="time in timeRanges"
+              v-for="time in TIME_RANGES"
               :key="time"
               tabindex="0"
               class="time-range"
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
index 38091106..4bcf6f97 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -20,31 +20,30 @@
 <script setup lang="ts">
   import { Empty } from 'ant-design-vue'
   import { useServiceStore } from '@/store/service'
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { CommonStatus } from '@/enums/state'
   import { useJobProgress } from '@/store/job-progress'
+  import { useTabStore } from '@/store/tab-state'
   import { usePngImage } from '@/utils/tools'
+  import { STATUS_COLOR } from '@/utils/constant'
 
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { FilterFormItem } from '@/components/common/form-filter/types'
-  import type { ServiceStatusType, ServiceVO } from '@/api/service/types'
-  import type { ClusterVO } from '@/api/cluster/types'
+  import type { ServiceVO } from '@/api/service/types'
   import type { Command, CommandRequest } from '@/api/command/types'
 
   type GroupItemActionType = keyof typeof Command | 'More'
 
   const { t } = useI18n()
   const router = useRouter()
-  const attrs = useAttrs() as ClusterVO
+  const route = useRoute()
+  const tabStore = useTabStore()
   const jobProgressStore = useJobProgress()
   const serviceStore = useServiceStore()
-  const { services, loading } = toRefs(serviceStore)
+
+  const { services, loading } = storeToRefs(serviceStore)
 
   const filterValue = ref({})
-  const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
-    1: 'healthy',
-    2: 'unhealthy',
-    3: 'unknown'
-  })
+  const clusterId = ref(Number(route.params.id))
 
   const actionGroups = computed((): GroupItem<GroupItemActionType, 
ServiceVO>[] => [
     {
@@ -118,19 +117,19 @@
     if (!['More', 'Remove'].includes(command)) {
       const execCommandParams = {
         command: command,
-        clusterId: attrs.id,
+        clusterId: clusterId.value,
         commandLevel: 'service',
         serviceCommands: [{ serviceName: service.name, installed: true }]
       } as CommandRequest
       jobProgressStore.processCommand(execCommandParams, getServices, { 
displayName: service.displayName })
     } else {
-      serviceStore.removeService(service, attrs.id!, getServices)
+      serviceStore.removeService(service, clusterId.value!, getServices)
     }
   }
 
   const getServices = () => {
-    if (attrs.id != undefined) {
-      serviceStore.getServices(attrs.id, filterValue.value)
+    if (clusterId.value != undefined) {
+      serviceStore.getServices(clusterId.value, filterValue.value)
     }
   }
 
@@ -142,6 +141,8 @@
   }
 
   onActivated(() => {
+    const currTab = tabStore.getActiveTab(route.path ?? '2')
+    if (currTab != '2') return
     getServices()
   })
 </script>
@@ -166,10 +167,10 @@
               <span class="small-gray">{{ item.version }}</span>
             </div>
             <div class="header-base-status">
-              <a-tag :color="CommonStatus[statusColors[item.status]]">
+              <a-tag :color="CommonStatus[STATUS_COLOR[item.status]]">
                 <div class="header-base-status-inner">
-                  <status-dot :color="CommonStatus[statusColors[item.status]]" 
/>
-                  <span class="small">{{ 
t(`common.${statusColors[item.status]}`) }}</span>
+                  <status-dot :color="CommonStatus[STATUS_COLOR[item.status]]" 
/>
+                  <span class="small">{{ 
t(`common.${STATUS_COLOR[item.status]}`) }}</span>
                 </div>
               </a-tag>
             </div>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
index 5fbfbd9e..830038dd 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
@@ -21,11 +21,11 @@
   import { getUserListOfService } from '@/api/cluster'
 
   import type { TableColumnType } from 'ant-design-vue'
-  import type { ClusterVO, ServiceUserVO } from '@/api/cluster/types'
+  import type { ServiceUserVO } from '@/api/cluster/types'
 
   const { t } = useI18n()
-  const attrs = useAttrs() as ClusterVO
-
+  const route = useRoute()
+  const clusterId = ref(Number(route.params.id))
   const columns = computed((): TableColumnType[] => [
     {
       title: '#',
@@ -66,12 +66,12 @@
   })
 
   const loadUserListOfService = async () => {
-    if (attrs.id == undefined || !paginationProps.value) {
+    if (clusterId.value == undefined || !paginationProps.value) {
       loading.value = false
       return
     }
     try {
-      const data = await getUserListOfService(attrs.id, filtersParams.value)
+      const data = await getUserListOfService(clusterId.value, 
filtersParams.value)
       dataSource.value = data.content
       paginationProps.value.total = data.total
     } catch (error) {
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/detail.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/detail.vue
index a155ba45..2bf87749 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/detail.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/detail.vue
@@ -20,7 +20,8 @@
 <script setup lang="ts">
   import { getHost, restartAgent, startAgent, stopAgent } from '@/api/host'
   import { message } from 'ant-design-vue'
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { CommonStatus } from '@/enums/state'
+  import { STATUS_COLOR } from '@/utils/constant'
 
   import Overview from './overview.vue'
 
@@ -39,12 +40,6 @@
     stop: stopAgent
   })
 
-  const statusColors = shallowRef<Record<HostStatusType, keyof typeof 
CommonStatusTexts>>({
-    1: 'healthy',
-    2: 'unhealthy',
-    3: 'unknown'
-  })
-
   const actionGroup = computed<GroupItem[]>(() => [
     {
       shape: 'default',
@@ -112,7 +107,7 @@
     <header-card
       :title="hostInfo.hostname"
       avatar="host"
-      :status="CommonStatus[statusColors[hostInfo.status as HostStatusType]]"
+      :status="CommonStatus[STATUS_COLOR[hostInfo.status as HostStatusType]]"
       :desc="hostInfo.desc"
       :action-groups="actionGroup"
     />
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
index 2ece464c..51508deb 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
@@ -22,6 +22,7 @@
 
   import { useClusterStore } from '@/store/cluster'
   import * as hostApi from '@/api/host'
+  import { HOST_STATUS, POLLING_INTERVAL } from '@/utils/constant'
 
   import useBaseTable from '@/composables/use-base-table'
   import HostCreate from '@/features/create-host/index.vue'
@@ -32,7 +33,6 @@
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { HostVO } from '@/api/host/types'
 
-  const POLLING_INTERVAL = 30000
   type Key = string | number
 
   interface TableState {
@@ -42,20 +42,20 @@
   }
 
   const { t } = useI18n()
-  const { confirmModal } = useModal()
-
   const router = useRouter()
   const clusterStore = useClusterStore()
+  const { confirmModal } = useModal()
+
   const searchInputRef = ref()
   const pollingIntervalId = ref<any>(null)
   const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
   const installRef = ref<InstanceType<typeof InstallDependencies> | null>(null)
-  const hostStatus = shallowRef(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
   const state = reactive<TableState>({
     searchText: '',
     searchedColumn: '',
     selectedRowKeys: []
   })
+
   const filtersOfClusterDisplayName = computed(() =>
     Object.values(clusterStore.clusterMap).map((v) => ({
       text: v.displayName || v.name,
@@ -329,8 +329,8 @@
           </span>
         </template>
         <template v-if="column.key === 'status'">
-          <svg-icon style="margin-left: 0" 
:name="hostStatus[record.status].toLowerCase()" />
-          <span>{{ t(`common.${hostStatus[record.status].toLowerCase()}`) 
}}</span>
+          <svg-icon style="margin-left: 0" 
:name="HOST_STATUS[record.status].toLowerCase()" />
+          <span>{{ t(`common.${HOST_STATUS[record.status].toLowerCase()}`) 
}}</span>
         </template>
         <template v-if="column.key === 'operation'">
           <button-group
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 7e375cb0..78be5a3a 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -21,10 +21,13 @@
   import { Empty } from 'ant-design-vue'
   import { formatFromByte } from '@/utils/storage.ts'
   import { usePngImage, isEmpty } from '@/utils/tools'
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state.ts'
+  import { TIME_RANGES, STATUS_COLOR, POLLING_INTERVAL } from 
'@/utils/constant'
+  import { CommonStatus } from '@/enums/state.ts'
+
   import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
   import { useStackStore } from '@/store/stack'
+
   import { getComponentsByHost } from '@/api/host'
   import { Command } from '@/api/command/types'
   import { getHostMetricsInfo } from '@/api/metrics'
@@ -32,13 +35,11 @@
   import GaugeChart from '@/features/metric/gauge-chart.vue'
   import CategoryChart from '@/features/metric/category-chart.vue'
 
-  import type { HostStatusType, HostVO } from '@/api/host/types'
+  import type { HostVO } from '@/api/host/types'
   import type { ClusterStatusType } from '@/api/cluster/types.ts'
   import type { ComponentVO } from '@/api/component/types.ts'
   import type { MetricsData, TimeRangeType } from '@/api/metrics/types'
 
-  type StatusColorType = Record<HostStatusType, keyof typeof CommonStatusTexts>
-
   interface Props {
     hostInfo: HostVO
   }
@@ -50,13 +51,12 @@
   const serviceStore = useServiceStore()
   const jobProgressStore = useJobProgress()
 
+  const isRunning = ref(false)
   const currTimeRange = ref<TimeRangeType>('5m')
   const chartData = ref<Partial<MetricsData>>({})
 
   const componentsFromCurrentHost = shallowRef<Map<string, ComponentVO[]>>(new 
Map())
   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)
 
@@ -113,18 +113,27 @@
   }
 
   const handleTimeRange = (time: TimeRangeType) => {
-    if (currTimeRange.value == time) {
-      return
+    if (currTimeRange.value !== time) {
+      currTimeRange.value = time
+      pause()
+      getHostMetrics()
+      resume()
     }
-    currTimeRange.value = time
-    getHostMetrics()
   }
 
   const getHostMetrics = async () => {
+    if (isRunning.value) {
+      return
+    }
+
+    isRunning.value = true
+
     try {
       chartData.value = await getHostMetricsInfo({ id: hostInfo.value.id! }, { 
interval: currTimeRange.value })
     } catch (error) {
       console.log('Failed to fetch host metrics:', error)
+    } finally {
+      isRunning.value = false
     }
   }
 
@@ -144,12 +153,13 @@
     }
   }
 
-  const { pause, resume } = useIntervalFn(getHostMetrics, 30000, { immediate: 
true })
+  const { pause, resume } = useIntervalFn(getHostMetrics, POLLING_INTERVAL, { 
immediate: true })
 
   watch(
     () => hostInfo.value,
-    (val) => {
+    async (val) => {
       if (val.id) {
+        pause()
         getComponentInfo()
         getHostMetrics()
         resume()
@@ -192,10 +202,10 @@
                         <a-tag
                           v-if="base === 'status'"
                           class="reset-tag"
-                          :color="CommonStatus[statusColors[hostInfo[base] as 
ClusterStatusType]]"
+                          :color="CommonStatus[STATUS_COLOR[hostInfo[base] as 
ClusterStatusType]]"
                         >
-                          <status-dot 
:color="CommonStatus[statusColors[hostInfo[base] as ClusterStatusType]]" />
-                          {{ hostInfo[base] && 
t(`common.${statusColors[hostInfo[base] as ClusterStatusType]}`) }}
+                          <status-dot 
:color="CommonStatus[STATUS_COLOR[hostInfo[base] as ClusterStatusType]]" />
+                          {{ hostInfo[base] && 
t(`common.${STATUS_COLOR[hostInfo[base] as ClusterStatusType]}`) }}
                         </a-tag>
                         <a-typography-text v-else 
class="desc-sub-item-desc-column">
                           <span 
v-if="Object.keys(unitOfBaseConfig).includes(base as string)">
@@ -268,7 +278,7 @@
           <a-typography-text strong :content="t('overview.chart')" />
           <a-space :size="12">
             <div
-              v-for="time in timeRanges"
+              v-for="time in TIME_RANGES"
               :key="time"
               tabindex="0"
               class="time-range"
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
index 6150d92a..b5d11b7d 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
@@ -59,7 +59,7 @@
     <main-card v-model:active-key="activeTab" :tabs="tabs">
       <template #tab-item>
         <keep-alive>
-          <component :is="getCompName" :key="activeTab" 
v-bind="currCluster"></component>
+          <component :is="getCompName" :key="activeTab" 
:payload="currCluster"></component>
         </keep-alive>
       </template>
     </main-card>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue
index 02e1bfef..04da3813 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue
@@ -18,32 +18,33 @@
 -->
 
 <script setup lang="ts">
-  import { usePngImage } from '@/utils/tools'
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { Empty } from 'ant-design-vue'
   import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
-  import { Empty } from 'ant-design-vue'
 
-  import type { ServiceStatusType, ServiceVO } from '@/api/service/types'
+  import { CommonStatus } from '@/enums/state'
+  import { usePngImage } from '@/utils/tools'
+  import { STATUS_COLOR } from '@/utils/constant'
+
+  import type { ServiceVO } from '@/api/service/types'
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { FilterFormItem } from '@/components/common/form-filter/types'
   import type { Command, CommandRequest } from '@/api/command/types'
+  import type { ClusterVO } from '@/api/cluster/types'
 
   type GroupItemActionType = keyof typeof Command | 'More'
 
+  const props = defineProps<{ payload: ClusterVO }>()
+
   const { t } = useI18n()
   const router = useRouter()
   const jobProgressStore = useJobProgress()
   const serviceStore = useServiceStore()
   const { services, loading } = toRefs(serviceStore)
-  const clusterInfo = useAttrs() as { id: number; name: string }
 
   const filterValue = ref({})
-  const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
-    1: 'healthy',
-    2: 'unhealthy',
-    3: 'unknown'
-  })
+
+  const clusterId = computed(() => props.payload.id)
   const filterFormItems = computed((): FilterFormItem[] => [
     { type: 'search', key: 'name', label: t('service.name') },
     {
@@ -60,9 +61,9 @@
       key: 'status',
       label: t('common.status'),
       options: [
-        { label: t(`common.${statusColors.value[1]}`), value: 1 },
-        { label: t(`common.${statusColors.value[2]}`), value: 2 },
-        { label: t(`common.${statusColors.value[3]}`), value: 3 }
+        { label: t(`common.${STATUS_COLOR[1]}`), value: 1 },
+        { label: t(`common.${STATUS_COLOR[2]}`), value: 2 },
+        { label: t(`common.${STATUS_COLOR[3]}`), value: 3 }
       ]
     }
   ])
@@ -108,29 +109,29 @@
   ])
 
   const infraAction = async (command: GroupItemActionType | 'Remove', service: 
ServiceVO) => {
-    const { id: clusterId } = clusterInfo
     if (!['More', 'Remove'].includes(command)) {
+      const { name: serviceName, displayName } = service
       const execCommandParams = {
-        command: command,
-        clusterId,
+        command,
+        clusterId: clusterId.value,
         commandLevel: 'service',
-        serviceCommands: [{ serviceName: service.name, installed: true }]
+        serviceCommands: [{ serviceName, installed: true }]
       } as CommandRequest
-      jobProgressStore.processCommand(execCommandParams, getServices, { 
displayName: service.displayName })
+      jobProgressStore.processCommand(execCommandParams, getServices, { 
displayName })
     } else {
-      serviceStore.removeService(service, clusterId, getServices)
+      serviceStore.removeService(service, clusterId.value!, getServices)
     }
   }
 
   const getServices = async () => {
-    await serviceStore.getServices(clusterInfo.id, filterValue.value)
+    await serviceStore.getServices(clusterId.value!, filterValue.value)
   }
 
   const viewServiceDetail = (payload: ServiceVO) => {
     router.push({
       name: 'InfraServiceDetail',
       params: {
-        id: clusterInfo.id,
+        id: clusterId.value,
         serviceId: payload.id
       }
     })
@@ -162,10 +163,10 @@
                 <span class="small-gray">{{ item.version }}</span>
               </div>
               <div class="header-base-status">
-                <a-tag :color="CommonStatus[statusColors[item.status]]">
+                <a-tag :color="CommonStatus[STATUS_COLOR[item.status]]">
                   <div class="header-base-status-inner">
-                    <status-dot 
:color="CommonStatus[statusColors[item.status]]" />
-                    <span class="small">{{ 
t(`common.${statusColors[item.status]}`) }}</span>
+                    <status-dot 
:color="CommonStatus[STATUS_COLOR[item.status]]" />
+                    <span class="small">{{ 
t(`common.${STATUS_COLOR[item.status]}`) }}</span>
                   </div>
                 </a-tag>
               </div>
diff --git a/bigtop-manager-ui/src/store/tab-state/index.ts 
b/bigtop-manager-ui/src/store/tab-state/index.ts
index 6b940e6a..bec4b90c 100644
--- a/bigtop-manager-ui/src/store/tab-state/index.ts
+++ b/bigtop-manager-ui/src/store/tab-state/index.ts
@@ -17,16 +17,25 @@
  * under the License.
  */
 
-export const useTabStore = defineStore('tab', () => {
-  const activeTabs = ref<Record<string, string>>({})
+export const useTabStore = defineStore(
+  'tab',
+  () => {
+    const activeTabs = ref<Record<string, string>>({})
 
-  function setActiveTab(pageKey: string, tabIndex: string) {
-    activeTabs.value[pageKey] = tabIndex
-  }
+    function setActiveTab(pageKey: string, tabIndex: string) {
+      activeTabs.value[pageKey] = tabIndex
+    }
 
-  function getActiveTab(pageKey: string) {
-    return activeTabs.value[pageKey]
-  }
+    function getActiveTab(pageKey: string) {
+      return activeTabs.value[pageKey]
+    }
 
-  return { activeTabs, setActiveTab, getActiveTab }
-})
+    return { activeTabs, setActiveTab, getActiveTab }
+  },
+  {
+    persist: {
+      storage: sessionStorage,
+      paths: ['activeTabs']
+    }
+  }
+)
diff --git a/bigtop-manager-ui/src/utils/constant.ts 
b/bigtop-manager-ui/src/utils/constant.ts
index 9d8b8f32..9fd050ba 100644
--- a/bigtop-manager-ui/src/utils/constant.ts
+++ b/bigtop-manager-ui/src/utils/constant.ts
@@ -17,8 +17,38 @@
  * under the License.
  */
 
+import type { ClusterStatusType } from '@/api/cluster/types'
+import type { HostStatusType } from '@/api/host/types'
+import type { StateType } from '@/api/job/types'
+import type { TimeRangeType } from '@/api/metrics/types'
+import type { ServiceStatusType } from '@/api/service/types'
+import { CommonStatusTexts } from '@/enums/state'
+
+export type Status = ServiceStatusType | HostStatusType | ClusterStatusType
+
+export type StatusColorType = Record<Status, keyof typeof CommonStatusTexts>
+
 export const API_RETRY_TIME = 3
 export const API_EXPIRE_TIME = 30 * 1000
 export const JOB_SCHEDULE_INTERVAL = 1000
 export const MONITOR_SCHEDULE_INTERVAL = 10 * 1000
 export const DEFAULT_PAGE_SIZE = 10
+export const POLLING_INTERVAL = 30000
+
+export const TIME_RANGES: TimeRangeType[] = ['1m', '5m', '15m', '30m', '1h', 
'2h']
+export const HOST_STATUS = ['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN']
+export const COMPONENT_STATUS = ['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN']
+
+export const STATUS_COLOR: StatusColorType = {
+  1: 'healthy',
+  2: 'unhealthy',
+  3: 'unknown'
+}
+
+export const JOB_STATUS: Record<StateType, string> = {
+  Pending: 'installing',
+  Processing: 'processing',
+  Failed: 'failed',
+  Canceled: 'canceled',
+  Successful: 'success'
+}

Reply via email to