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 036a85db BIGTOP-4395: Add service details page (#195)
036a85db is described below

commit 036a85dbfe9c4326a669cd50b597c9f0a2b86d77
Author: Fdefined <[email protected]>
AuthorDate: Fri Mar 28 17:58:40 2025 +0800

    BIGTOP-4395: Add service details page (#195)
---
 bigtop-manager-ui/src/api/component/index.ts       |  15 +
 bigtop-manager-ui/src/api/component/types.ts       |  19 +
 bigtop-manager-ui/src/api/service/index.ts         |   2 +-
 bigtop-manager-ui/src/api/service/types.ts         |   5 +-
 .../src/components/common/button-group/index.vue   |  56 +--
 .../src/components/common/button-group/types.ts    |   5 +-
 .../src/components/common/header-card/index.vue    |  13 +-
 bigtop-manager-ui/src/locales/en_US/cluster.ts     |   1 +
 bigtop-manager-ui/src/locales/en_US/common.ts      |  16 +-
 bigtop-manager-ui/src/locales/en_US/overview.ts    |   9 +-
 bigtop-manager-ui/src/locales/en_US/service.ts     |   7 +-
 bigtop-manager-ui/src/locales/zh_CN/common.ts      |  16 +-
 bigtop-manager-ui/src/locales/zh_CN/overview.ts    |   9 +-
 bigtop-manager-ui/src/locales/zh_CN/service.ts     |   7 +-
 .../cluster/components/host-config.vue             |   2 +-
 .../src/pages/cluster-manage/cluster/host.vue      |   2 +-
 .../cluster/service-management/components.vue      | 412 +++++++++++++++++++++
 .../components/capture-snapshot.vue                | 124 +++++++
 .../components/snapshot-management.vue             | 184 +++++++++
 .../cluster/service-management/configs.vue         | 244 ++++++++++++
 .../cluster/service-management/index.vue           | 129 +++++++
 .../service-management}/overview.vue               | 201 +++-------
 .../src/pages/cluster-manage/cluster/service.vue   |  21 +-
 .../src/pages/cluster-manage/hosts/index.vue       |   2 +-
 .../cluster-manage/hosts/install-dependencies.vue  |   2 +-
 .../src/pages/cluster-manage/hosts/overview.vue    |   2 +-
 .../pages/cluster-manage/infrastructures/index.vue |  21 +-
 .../src/router/routes/modules/clusters.ts          |  17 +
 bigtop-manager-ui/src/store/menu/index.ts          |   3 +
 29 files changed, 1335 insertions(+), 211 deletions(-)

diff --git a/bigtop-manager-ui/src/api/component/index.ts 
b/bigtop-manager-ui/src/api/component/index.ts
index 60cba9a8..5166ad10 100644
--- a/bigtop-manager-ui/src/api/component/index.ts
+++ b/bigtop-manager-ui/src/api/component/index.ts
@@ -16,3 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+import { get, del } from '../request-util'
+import type { ComponentParams, ComponentVO, ComponentList } from './types'
+
+export const getComponents = (params: ComponentParams) => {
+  return get<ComponentList>(`/clusters/${params.clusterId}/components`, params)
+}
+
+export const getComponent = (pathParams: { clusterId: number; id: number }) => 
{
+  return 
get<ComponentVO>(`/clusters/${pathParams.clusterId}/components/${pathParams.id}`)
+}
+
+export const deleteComponent = (pathParams: { clusterId: number; id: number }) 
=> {
+  return 
del<boolean>(`/clusters/${pathParams.clusterId}/components/${pathParams.id}`)
+}
diff --git a/bigtop-manager-ui/src/api/component/types.ts 
b/bigtop-manager-ui/src/api/component/types.ts
index 86e619dc..be9c1c86 100644
--- a/bigtop-manager-ui/src/api/component/types.ts
+++ b/bigtop-manager-ui/src/api/component/types.ts
@@ -17,6 +17,25 @@
  * under the License.
  */
 
+import type { PageVO } from '../types'
+
+export type ComponentList = PageVO<ComponentVO>
+
+export interface ComponentParams {
+  clusterId?: number
+  hostId?: number
+  hostname?: string
+  hostnames?: string[]
+  name?: string
+  orderBy?: string
+  pageNum?: number
+  pageSize?: number
+  serviceId?: number
+  serviceNames?: string[]
+  sort?: string
+  [property: string]: any
+}
+
 /**
  * ComponentVO
  */
diff --git a/bigtop-manager-ui/src/api/service/index.ts 
b/bigtop-manager-ui/src/api/service/index.ts
index 507061e7..1e4b11cd 100644
--- a/bigtop-manager-ui/src/api/service/index.ts
+++ b/bigtop-manager-ui/src/api/service/index.ts
@@ -42,7 +42,7 @@ export const getServiceConfigs = (pathParams: ServiceParams) 
=> {
   return 
get<ServiceConfig[]>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/configs`)
 }
 
-export const updateServiceConfigs = (pathParams: ServiceParams, data: 
ServiceConfig) => {
+export const updateServiceConfigs = (pathParams: ServiceParams, data: 
ServiceConfig[]) => {
   return 
post<ServiceConfig[]>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/configs`,
 data)
 }
 
diff --git a/bigtop-manager-ui/src/api/service/types.ts 
b/bigtop-manager-ui/src/api/service/types.ts
index 7d81c6aa..f244e206 100644
--- a/bigtop-manager-ui/src/api/service/types.ts
+++ b/bigtop-manager-ui/src/api/service/types.ts
@@ -23,7 +23,7 @@ import type { PageVO } from '@/api/types'
 export type ServiceList = PageVO<ServiceVO>
 export type ServiceParams = { clusterId: number; id: number }
 export type SnapshotData = { desc?: string; name?: string }
-export type SnapshotRecovery = ServiceParams & { snapshotId: string }
+export type SnapshotRecovery = ServiceParams & { snapshotId: number }
 export type ServiceStatusType = 1 | 2 | 3
 export type ServiceListParams = {
   name?: string
@@ -45,6 +45,8 @@ export interface ServiceVO {
   user?: string
   version?: string
   isInstalled?: boolean
+  metrics?: boolean
+  kerberos?: boolean
 }
 
 export interface ServiceConfig {
@@ -59,6 +61,7 @@ export interface Property {
   displayName?: string
   name: string
   value?: string
+  [property: string]: any
 }
 
 export interface Attr {
diff --git a/bigtop-manager-ui/src/components/common/button-group/index.vue 
b/bigtop-manager-ui/src/components/common/button-group/index.vue
index b2dd417f..43ee306a 100644
--- a/bigtop-manager-ui/src/components/common/button-group/index.vue
+++ b/bigtop-manager-ui/src/components/common/button-group/index.vue
@@ -29,7 +29,7 @@
     groupType: 'text',
     space: 8,
     groups: () => {
-      return []
+      return [] as any[]
     }
   })
 
@@ -52,13 +52,38 @@
 <template>
   <a-space :size="space" :wrap="true" :class="{ 'text-compact': 
$props.textCompact, 'btn-auto': $props.auto }">
     <template v-for="(item, _index) in groups" :key="_index">
-      <a-dropdown v-if="item.dropdownMenu">
+      <template v-if="!item.hidden">
+        <a-dropdown v-if="item.dropdownMenu">
+          <a-button
+            :disabled="item.disabled"
+            :shape="item.shape || groupShape || 'default'"
+            :type="item.type || groupType || 'default'"
+            :title="checkTitle(item)"
+            @click="item.clickEvent ? item.clickEvent(item) : () => {}"
+          >
+            <template #icon>
+              <slot name="icon" :item="item" />
+            </template>
+            <span v-if="item.text">
+              {{ $props.i18n && item.text ? $t(`${$props.i18n}.${item.text}`) 
: item.text }}
+            </span>
+          </a-button>
+          <template #overlay>
+            <a-menu v-if="!item.disabled" @click="item.dropdownMenuClickEvent">
+              <a-menu-item v-for="actionItem in item.dropdownMenu" 
v-bind="actionItem" :key="actionItem.action">
+                {{ actionItem.text }}
+              </a-menu-item>
+            </a-menu>
+          </template>
+        </a-dropdown>
         <a-button
+          v-else
+          :danger="item.danger"
           :disabled="item.disabled"
           :shape="item.shape || groupShape || 'default'"
           :type="item.type || groupType || 'default'"
           :title="checkTitle(item)"
-          @click="item.clickEvent ? item.clickEvent(item) : () => {}"
+          @click="item.clickEvent ? item.clickEvent(item, args) : () => {}"
         >
           <template #icon>
             <slot name="icon" :item="item" />
@@ -67,30 +92,7 @@
             {{ $props.i18n && item.text ? $t(`${$props.i18n}.${item.text}`) : 
item.text }}
           </span>
         </a-button>
-        <template #overlay>
-          <a-menu v-if="!item.disabled" @click="item.dropdownMenuClickEvent">
-            <a-menu-item v-for="actionItem in item.dropdownMenu" 
:key="actionItem.action">
-              {{ actionItem.text }}
-            </a-menu-item>
-          </a-menu>
-        </template>
-      </a-dropdown>
-      <a-button
-        v-else
-        :danger="item.danger"
-        :disabled="item.disabled"
-        :shape="item.shape || groupShape || 'default'"
-        :type="item.type || groupType || 'default'"
-        :title="checkTitle(item)"
-        @click="item.clickEvent ? item.clickEvent(item, args) : () => {}"
-      >
-        <template #icon>
-          <slot name="icon" :item="item" />
-        </template>
-        <span v-if="item.text">
-          {{ $props.i18n && item.text ? $t(`${$props.i18n}.${item.text}`) : 
item.text }}
-        </span>
-      </a-button>
+      </template>
     </template>
   </a-space>
 </template>
diff --git a/bigtop-manager-ui/src/components/common/button-group/types.ts 
b/bigtop-manager-ui/src/components/common/button-group/types.ts
index 7f2d17d4..4125e867 100644
--- a/bigtop-manager-ui/src/components/common/button-group/types.ts
+++ b/bigtop-manager-ui/src/components/common/button-group/types.ts
@@ -26,17 +26,18 @@ export interface DropdownMenu extends MenuItemProps {
   action: string
   text: string
 }
-export interface GroupItem<T = string> {
+export interface GroupItem<T = any> {
   icon?: string
   tip?: string
   text?: string
   action?: T
+  hidden?: boolean
   type?: BtnType
   shape?: ShapeType
   disabled?: boolean
   danger?: boolean
   dropdownMenu?: DropdownMenu[]
-  clickEvent?: (item?: GroupItem, ...args: any[]) => void
+  clickEvent?: (item?: GroupItem<T>, ...args: any[]) => void
   dropdownMenuClickEvent?: MenuProps['onClick']
 }
 
diff --git a/bigtop-manager-ui/src/components/common/header-card/index.vue 
b/bigtop-manager-ui/src/components/common/header-card/index.vue
index d6fd6274..9c1d88ae 100644
--- a/bigtop-manager-ui/src/components/common/header-card/index.vue
+++ b/bigtop-manager-ui/src/components/common/header-card/index.vue
@@ -60,7 +60,9 @@
   <div class="header-card">
     <slot>
       <div class="header-card-info">
-        <a-avatar v-if="showAvatar" :src="avatarSrc" shape="square" :size="54" 
/>
+        <slot name="avatar">
+          <a-avatar v-if="showAvatar" :src="avatarSrc" shape="square" 
:size="54" />
+        </slot>
         <div class="card-info-title">
           <div class="card-info-status">
             <a-typography-title :level="5">{{ $props.title 
}}</a-typography-title>
@@ -106,4 +108,13 @@
       }
     }
   }
+
+  :deep(.ant-avatar) {
+    flex-shrink: 0;
+    border-radius: 4px;
+
+    img {
+      object-fit: contain !important;
+    }
+  }
 </style>
diff --git a/bigtop-manager-ui/src/locales/en_US/cluster.ts 
b/bigtop-manager-ui/src/locales/en_US/cluster.ts
index 05612202..ad9de32b 100644
--- a/bigtop-manager-ui/src/locales/en_US/cluster.ts
+++ b/bigtop-manager-ui/src/locales/en_US/cluster.ts
@@ -21,6 +21,7 @@ export default {
   cluster_config: 'Cluster Configuration',
   cluster_info: 'Cluster',
   component_info: 'Stack',
+  component: 'Component',
   host_config: 'Hosts',
   create: 'Create',
   name: 'Name',
diff --git a/bigtop-manager-ui/src/locales/en_US/common.ts 
b/bigtop-manager-ui/src/locales/en_US/common.ts
index 28441444..89aeb245 100644
--- a/bigtop-manager-ui/src/locales/en_US/common.ts
+++ b/bigtop-manager-ui/src/locales/en_US/common.ts
@@ -21,8 +21,10 @@ export default {
   prev: 'Previous',
   next: 'Next',
   done: 'Done',
+  save: 'Save',
   exit: 'Exit',
   action: 'Action',
+  operation: 'Operation',
   status: 'Status',
   healthy: 'Healthy',
   unhealthy: 'Unhealthy',
@@ -32,8 +34,9 @@ export default {
   edit: 'Edit',
   rename: 'Rename',
   delete: 'Delete',
-  remove: 'Remove',
+  remove: 'Remove {0}',
   bulk_remove: 'Bulk Remove',
+  batch_operation: 'Batch Operation',
   disable: 'Disable',
   enable: 'Enable',
   submit: 'Submit',
@@ -58,6 +61,9 @@ export default {
   service: 'Service',
   stack: 'Stack',
   component: 'Component',
+  componentname: 'Name',
+  quick_link: 'Quick Link',
+  no_link: 'No Link',
   stage: 'Stage',
   search: 'Search',
   reset: 'Reset',
@@ -92,6 +98,7 @@ export default {
   add_error: 'Please add {0}',
   delete_msg: 'Are you sure you want to delete this?',
   delete_msgs: 'Are you sure you want to delete these?',
+  restore_msg: 'Are you sure you want to restore this?',
   delete_empty: 'Please select the records you want to delete.',
   total: 'Total {0} items',
   note: 'Note: Changes to the source address will take effect for all 
clusters.',
@@ -108,9 +115,14 @@ export default {
   restart: 'Restart {0}',
   stop: 'Stop {0}',
   add: 'Add {0}',
+  success_msg: 'Added successfully',
+  configs: 'Configs',
   more_operations: 'More Operations',
   ok: 'Ok',
   yes: 'Yes',
   no: 'No',
-  all: 'All'
+  all: 'All',
+  disabled: 'Disabled',
+  restore: 'Restore',
+  selected: 'Selected'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/overview.ts 
b/bigtop-manager-ui/src/locales/en_US/overview.ts
index 6c2dd09c..4446a038 100644
--- a/bigtop-manager-ui/src/locales/en_US/overview.ts
+++ b/bigtop-manager-ui/src/locales/en_US/overview.ts
@@ -38,11 +38,16 @@ export default {
   unit_component: 'Components',
   component_info: 'Component Information',
   host_status: 'Status',
-  host_name: 'Name',
+  hostname: 'Name',
   host_desc: 'Description',
   component_count: 'Component Count',
   os: 'OS',
   ip_v4: 'IPv4',
   ip_v6: 'IPv6',
-  arch: 'Architecture'
+  arch: 'Architecture',
+  service_status: 'Status',
+  service_name: 'Name',
+  service_version: 'Version',
+  metrics: 'Metrics',
+  kerberos: 'Kerberos'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/service.ts 
b/bigtop-manager-ui/src/locales/en_US/service.ts
index 9e281799..b34603db 100644
--- a/bigtop-manager-ui/src/locales/en_US/service.ts
+++ b/bigtop-manager-ui/src/locales/en_US/service.ts
@@ -32,5 +32,10 @@ export default {
   component_host_assignment: 'Assign at least one host for each component',
   service_selection: 'Please select services to install',
   dependencies_conflict_msg: '{0} requires infra service {1} to be installed 
first',
-  dependencies_msg: '{0} requires service {1}, add it also?'
+  dependencies_msg: '{0} requires service {1}, add it also?',
+  capture_snapshot: 'Capture Snapshot',
+  snapshot_management: 'Snapshot Management',
+  snapshot_name: 'Name',
+  snapshot_description: 'Description',
+  snapshot_notes: 'Snapshot Notes'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/common.ts 
b/bigtop-manager-ui/src/locales/zh_CN/common.ts
index 3493fe8c..0fdfffec 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/common.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/common.ts
@@ -21,8 +21,10 @@ export default {
   prev: '上一步',
   next: '下一步',
   done: '完成',
+  save: '保存',
   exit: '退出',
   action: '操作',
+  operation: '操作',
   status: '状态',
   healthy: '健康',
   unhealthy: '不健康',
@@ -31,8 +33,9 @@ export default {
   failed: '异常',
   edit: '编辑',
   delete: '删除',
-  remove: '移除',
+  remove: '移除{0}',
   bulk_remove: '批量删除',
+  batch_operation: '批量操作',
   rename: '重命名',
   disable: '停用',
   enable: '使用',
@@ -58,6 +61,9 @@ export default {
   service: '服务',
   stack: '组件栈',
   component: '组件',
+  componentname: '组件名',
+  quick_link: '快速链接',
+  no_link: '无链接',
   stage: '阶段',
   search: '搜索',
   required: '需重启',
@@ -90,6 +96,7 @@ export default {
   select_error: '请选择{0}',
   add_error: '请添加{0}',
   delete_msg: '确认要删除该记录吗?',
+  restore_msg: '确认要恢复该记录吗?',
   delete_msgs: '确认要删除这些记录吗?',
   delete_empty: '请选择需要删除的记录',
   total: '共{0}条数据',
@@ -107,10 +114,15 @@ export default {
   restart: '重启{0}',
   stop: '停止{0}',
   add: '添加{0}',
+  success_msg: '添加成功',
   reset: '重置',
+  configs: '配置',
   more_operations: '其他操作',
   ok: '确认',
   yes: '是',
   no: '否',
-  all: '全部'
+  all: '全部',
+  disabled: '关闭',
+  restore: '恢复',
+  selected: '选中'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/overview.ts 
b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
index c1be8803..9e6ddd83 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/overview.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
@@ -38,11 +38,16 @@ export default {
   unit_component: '个组件',
   component_info: '组件信息',
   host_status: '主机状态',
-  host_name: '主机名',
+  hostname: '主机名',
   host_desc: '主机备注',
   component_count: '组件数',
   os: '操作系统',
   ip_v4: 'IPv4',
   ip_v6: 'IPv6',
-  arch: '架构'
+  arch: '架构',
+  service_status: '服务状态',
+  service_name: '服务名',
+  service_version: '服务版本',
+  metrics: '指标监控',
+  kerberos: 'Kerberos'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/service.ts 
b/bigtop-manager-ui/src/locales/zh_CN/service.ts
index db6692b5..515c839e 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/service.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/service.ts
@@ -32,5 +32,10 @@ export default {
   component_host_assignment: '每个组件至少分配一个主机',
   service_selection: '请选择需要安装的服务',
   dependencies_conflict_msg: '{0} 依赖于基础服务 {1},请先前往安装',
-  dependencies_msg: '{0} 依赖于服务 {1}, 是否一起安装?'
+  dependencies_msg: '{0} 依赖于服务 {1}, 是否一起安装?',
+  capture_snapshot: '拍摄快照',
+  snapshot_management: '快照管理',
+  snapshot_name: '快照名',
+  snapshot_description: '快照描述',
+  snapshot_notes: '快照备注'
 }
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
index d0ea468e..7c0f7266 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
@@ -79,7 +79,7 @@
       ellipsis: true
     },
     {
-      title: t('common.action'),
+      title: t('common.operation'),
       key: 'operation',
       width: '160px',
       fixed: 'right'
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 2dddd13e..e0e2ba9f 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -100,7 +100,7 @@
       ]
     },
     {
-      title: t('common.action'),
+      title: t('common.operation'),
       key: 'operation',
       width: '160px',
       fixed: 'right'
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
new file mode 100644
index 00000000..89ed8599
--- /dev/null
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
@@ -0,0 +1,412 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { computed, onActivated, onDeactivated, reactive, ref, shallowRef, 
useAttrs } from 'vue'
+  import { storeToRefs } from 'pinia'
+  import { useI18n } from 'vue-i18n'
+  import { useRoute } from 'vue-router'
+  import { deleteComponent, getComponents } from '@/api/component'
+  import { execCommand } from '@/api/command'
+  import { useStackStore } from '@/store/stack'
+  import useBaseTable from '@/composables/use-base-table'
+  import type { GroupItem } from '@/components/common/button-group/types'
+  import { message, Modal, type TableColumnType, type TableProps } from 
'ant-design-vue'
+  import type { ComponentVO } from '@/api/component/types'
+  import type { FilterConfirmProps, FilterResetProps } from 
'ant-design-vue/es/table/interface'
+  import type { Command, CommandRequest } from '@/api/command/types'
+
+  type Key = string | number
+  interface TableState {
+    selectedRowKeys: Key[]
+    searchText: string
+    searchedColumn: keyof ComponentVO
+    selectedRows: ComponentVO[]
+  }
+
+  interface ServieceInfo {
+    cluster: string
+    id: number
+    service: string
+    serviceId: number
+  }
+
+  const POLLING_INTERVAL = 3000
+  const { t } = useI18n()
+  const stackStore = useStackStore()
+  const route = useRoute()
+  const attrs = useAttrs()
+  const { stacks } = storeToRefs(stackStore)
+  const searchInputRef = ref()
+  const pollingIntervalId = ref<any>(null)
+  const componentStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
+  const commandRequest = shallowRef<CommandRequest>({
+    command: 'Add',
+    commandLevel: 'cluster',
+    componentCommands: []
+  })
+  const state = reactive<TableState>({
+    searchText: '',
+    searchedColumn: '',
+    selectedRowKeys: [],
+    selectedRows: []
+  })
+
+  const currServiceInfo = computed(() => route.params as unknown as 
ServieceInfo)
+  const componentsFromStack = computed(
+    () =>
+      new Map(
+        stacks.value.flatMap(
+          (stack) =>
+            stack.services?.flatMap((service) =>
+              service.name && service.components ? [[service.name, 
service.components]] : []
+            ) ?? []
+        )
+      )
+  )
+
+  const columns = computed((): TableColumnType<ComponentVO>[] => [
+    {
+      title: t('common.componentname'),
+      dataIndex: 'displayName',
+      key: 'name',
+      ellipsis: true,
+      filterMultiple: false,
+      filters: 
[...(componentsFromStack.value.get(currServiceInfo.value.service)?.values() || 
[])]?.map((v) => ({
+        text: v?.displayName || '',
+        value: v?.name || ''
+      }))
+    },
+    {
+      title: t('host.hostname'),
+      dataIndex: 'hostname',
+      key: 'hostname',
+      ellipsis: true,
+      customFilterDropdown: true
+    },
+    {
+      title: t('common.status'),
+      dataIndex: 'status',
+      key: 'status',
+      width: 160,
+      ellipsis: true,
+      filterMultiple: false,
+      filters: [
+        {
+          text: t('common.success'),
+          value: 1
+        },
+        {
+          text: t('common.failed'),
+          value: 2
+        },
+        {
+          text: t('common.unknown'),
+          value: 3
+        }
+      ]
+    },
+    {
+      title: t('common.quick_link'),
+      key: 'quickLink',
+      dataIndex: 'quickLink',
+      ellipsis: true
+    },
+    {
+      title: t('common.operation'),
+      width: 280,
+      key: 'operation',
+      fixed: 'right'
+    }
+  ])
+
+  const { loading, dataSource, paginationProps, filtersParams, onChange } = 
useBaseTable<ComponentVO>({
+    columns: columns.value,
+    rows: []
+  })
+
+  const operations = shallowRef<GroupItem<keyof typeof Command>[]>([
+    {
+      text: 'start',
+      action: 'Start',
+      clickEvent: (item, args) => handleTableOperation(item!, args)
+    },
+    {
+      text: 'stop',
+      action: 'Stop',
+      clickEvent: (item, args) => handleTableOperation(item!, args)
+    },
+    {
+      text: 'restart',
+      action: 'Restart',
+      clickEvent: (item, args) => handleTableOperation(item!, args)
+    },
+    {
+      text: 'remove',
+      disabled: true,
+      danger: true,
+      clickEvent: (_, args) => handleDelete(args)
+    }
+  ])
+
+  const batchOperations = computed((): GroupItem[] => [
+    {
+      text: t('common.batch_operation'),
+      type: 'primary',
+      dropdownMenu: [
+        {
+          action: 'Start',
+          text: t('common.start', [t('common.selected')])
+        },
+        {
+          action: 'Restart',
+          text: t('common.restart', [t('common.selected')])
+        },
+        {
+          action: 'Stop',
+          text: t('common.stop', [t('common.selected')])
+        }
+      ],
+      dropdownMenuClickEvent: (info) => dropdownMenuClick && 
dropdownMenuClick(info)
+    }
+  ])
+
+  const onSelectChange = (selectedRowKeys: Key[], selectedRows: ComponentVO[]) 
=> {
+    state.selectedRowKeys = selectedRowKeys
+    state.selectedRows = selectedRows
+  }
+
+  const handleSearch = (selectedKeys: Key[], confirm: (param?: 
FilterConfirmProps) => void, dataIndex: string) => {
+    confirm()
+    state.searchText = selectedKeys[0] as string
+    state.searchedColumn = dataIndex
+    stopPolling()
+    startPolling()
+  }
+
+  const handleReset = (clearFilters: (param?: FilterResetProps) => void) => {
+    clearFilters({ confirm: true })
+    state.searchText = ''
+    stopPolling()
+    startPolling()
+  }
+
+  const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key 
}) => {
+    if (state.selectedRows.length === 0) {
+      message.error(t('common.select_error', 
[`${t('common.component')}`.toLowerCase()]))
+      return
+    }
+    commandRequest.value.command = key as keyof typeof Command
+    const map = state.selectedRows.reduce((map, v) => {
+      if (!map.has(v.name)) {
+        map.set(v.name, { componentName: v.name, hostnames: [v.hostname] })
+      } else {
+        map.get(v.name).hostnames.push(v.hostname)
+      }
+      return map
+    }, new Map())
+    commandRequest.value.componentCommands = [...map.values()]
+    await execOperation()
+    getComponentList(true, true)
+  }
+
+  const handleTableOperation = async (item: GroupItem<keyof typeof Command>, 
row: ComponentVO) => {
+    commandRequest.value.command = item.action!
+    commandRequest.value.componentCommands?.push({
+      componentName: row.name!,
+      hostnames: [row.hostname!]
+    })
+    await execOperation()
+    getComponentList(true, true)
+  }
+
+  const execOperation = async () => {
+    try {
+      await execCommand({ ...commandRequest.value, clusterId: 
currServiceInfo.value.id })
+      state.selectedRowKeys = []
+      state.selectedRows = []
+    } catch (error) {
+      console.log('error :>> ', error)
+    }
+  }
+
+  const handleDelete = async (row: ComponentVO) => {
+    Modal.confirm({
+      title: t('common.delete_msg'),
+      async onOk() {
+        try {
+          const data = await deleteComponent({ clusterId: 
currServiceInfo.value.id, id: row.id! })
+          if (data) {
+            message.success(t('common.delete_success'))
+            getComponentList(true, true)
+          }
+        } catch (error) {
+          console.log('error :>> ', error)
+        }
+      }
+    })
+  }
+
+  const getComponentList = async (isReset = false, isFirstCall = false) => {
+    const { id: clusterId, serviceId } = currServiceInfo.value
+    if (attrs.id == undefined || !paginationProps.value) {
+      loading.value = false
+      return
+    }
+    if (isReset) {
+      paginationProps.value.current = 1
+    }
+    try {
+      if (isFirstCall) {
+        loading.value = true
+      }
+      const res = await getComponents({ ...filtersParams.value, clusterId, 
serviceId })
+      dataSource.value = res.content
+      paginationProps.value.total = res.total
+      loading.value = false
+    } catch (error) {
+      console.log('error :>> ', error)
+    } finally {
+      loading.value = false
+    }
+  }
+
+  const tableChange: TableProps['onChange'] = (...args) => {
+    onChange(...args)
+    stopPolling()
+    startPolling()
+  }
+
+  const startPolling = () => {
+    getComponentList(true, true)
+    pollingIntervalId.value = setInterval(() => {
+      getComponentList()
+    }, POLLING_INTERVAL)
+  }
+
+  const stopPolling = () => {
+    if (pollingIntervalId.value) {
+      clearInterval(pollingIntervalId.value)
+      pollingIntervalId.value = null
+    }
+  }
+
+  onActivated(() => {
+    startPolling()
+  })
+
+  onDeactivated(() => {
+    stopPolling()
+  })
+</script>
+
+<template>
+  <div class="component">
+    <header>
+      <div class="header-title">{{ $t('common.component') }}</div>
+      <div class="list-operation">
+        <a-button type="primary" :disabled="true">{{ $t('common.add', 
[`${$t('common.component')}`]) }}</a-button>
+        <button-group :groups="batchOperations" group-shape="default" />
+      </div>
+    </header>
+    <a-table
+      row-key="id"
+      :loading="loading"
+      :data-source="dataSource"
+      :columns="columns"
+      :pagination="paginationProps"
+      :scroll="{ x: 900, y: 1000 }"
+      :row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: 
onSelectChange }"
+      @change="tableChange"
+    >
+      <template #customFilterDropdown="{ setSelectedKeys, selectedKeys, 
confirm, clearFilters, column }">
+        <div class="search">
+          <a-input
+            ref="searchInputRef"
+            :placeholder="$t('common.enter_error', [column.title])"
+            :value="selectedKeys[0]"
+            @change="(e: any) => setSelectedKeys(e.target?.value ? 
[e.target?.value] : [])"
+            @press-enter="handleSearch(selectedKeys, confirm, 
column.dataIndex)"
+          />
+          <div class="search-option">
+            <a-button size="small" @click="handleReset(clearFilters)">
+              {{ $t('common.reset') }}
+            </a-button>
+            <a-button type="primary" size="small" 
@click="handleSearch(selectedKeys, confirm, column.dataIndex)">
+              {{ $t('common.search') }}
+            </a-button>
+          </div>
+        </div>
+      </template>
+      <template #customFilterIcon="{ filtered, column }">
+        <svg-icon v-if="!['name', 'status'].includes(column.key)" 
:name="filtered ? 'search_activated' : 'search'" />
+        <svg-icon v-else :name="filtered ? 'filter_activated' : 'filter'" />
+      </template>
+      <template #bodyCell="{ record, column }">
+        <template v-if="['status'].includes(column.key)">
+          <svg-icon style="margin-left: 0" 
:name="componentStatus[record.status].toLowerCase()" />
+          <span>{{ 
$t(`common.${componentStatus[record.status].toLowerCase()}`) }}</span>
+        </template>
+        <template v-if="column.key === 'quickLink'">
+          <span v-if="!record.quickLink">{{ $t('common.no_link') }}</span>
+          <a-typography-link v-else :href="record.quickLink.url" 
target="_blank">
+            {{ record.quickLink.displayName }}
+          </a-typography-link>
+        </template>
+        <template v-if="column.key === 'operation'">
+          <button-group
+            i18n="common"
+            :text-compact="true"
+            :space="24"
+            :groups="operations"
+            :args="record"
+            group-shape="default"
+            group-type="link"
+          />
+        </template>
+      </template>
+    </a-table>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+  .component {
+    .list-operation {
+      display: flex;
+      justify-content: space-between;
+    }
+  }
+  header {
+    margin-bottom: $space-md;
+  }
+  .search {
+    display: grid;
+    gap: $space-sm;
+    padding: $space-sm;
+    &-option {
+      width: 100%;
+      display: grid;
+      gap: $space-sm;
+      grid-template-columns: 1fr 1fr;
+      button {
+        width: 100%;
+      }
+    }
+  }
+</style>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/capture-snapshot.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/capture-snapshot.vue
new file mode 100644
index 00000000..41fb6957
--- /dev/null
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/capture-snapshot.vue
@@ -0,0 +1,124 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { useI18n } from 'vue-i18n'
+  import { computed, ref, shallowRef } from 'vue'
+  import { takeServiceConfigSnapshot } from '@/api/service'
+  import type { FormItemState } from '@/components/common/auto-form/types'
+  import type { ServiceParams, SnapshotData } from '@/api/service/types'
+  import { message } from 'ant-design-vue'
+
+  const { t } = useI18n()
+  const open = ref(false)
+  const loading = ref(false)
+  const serviceInfo = shallowRef<ServiceParams>()
+  const autoFormRef = ref<Comp.AutoFormInstance | null>(null)
+  const formValue = ref<SnapshotData>({})
+  const emits = defineEmits(['success'])
+  const formItems = computed((): FormItemState[] => [
+    {
+      type: 'input',
+      field: 'name',
+      formItemProps: {
+        name: 'name',
+        label: t('service.snapshot_name'),
+        rules: [
+          {
+            required: true,
+            message: t('common.enter_error', 
[`${t('service.snapshot_name')}`.toLowerCase()]),
+            trigger: 'blur'
+          }
+        ]
+      },
+      controlProps: {
+        placeholder: t('common.enter_error', 
[`${t('service.snapshot_name')}`.toLowerCase()])
+      }
+    },
+    {
+      type: 'textarea',
+      field: 'desc',
+      formItemProps: {
+        name: 'desc',
+        label: t('service.snapshot_description'),
+        rules: [
+          {
+            required: true,
+            message: t('common.enter_error', 
[`${t('service.snapshot_description')}`.toLowerCase()]),
+            trigger: 'blur'
+          }
+        ]
+      },
+      controlProps: {
+        placeholder: t('common.enter_error', 
[`${t('service.snapshot_description')}`.toLowerCase()])
+      }
+    }
+  ])
+
+  const handleOpen = (data: ServiceParams) => {
+    serviceInfo.value = data
+    open.value = true
+  }
+
+  const handleOk = async () => {
+    const validate = await autoFormRef.value?.getFormValidation()
+    if (!validate || !serviceInfo.value) return
+    loading.value = true
+    try {
+      await takeServiceConfigSnapshot(serviceInfo.value, formValue.value)
+      message.success(t('common.success_msg'))
+      emits('success')
+      handleCancel()
+    } catch (e) {
+      console.log(e)
+    } finally {
+      loading.value = false
+    }
+  }
+
+  const handleCancel = () => {
+    formValue.value = {}
+    open.value = false
+  }
+
+  defineExpose({
+    handleOpen
+  })
+</script>
+
+<template>
+  <div class="set-source">
+    <a-modal
+      v-model:open="open"
+      width="50%"
+      :centered="true"
+      :mask="false"
+      :title="$t('service.capture_snapshot')"
+      :mask-closable="false"
+      :destroy-on-close="true"
+      :confirm-loading="loading"
+      @ok="handleOk"
+      @cancel="handleCancel"
+    >
+      <auto-form ref="autoFormRef" v-model:form-value="formValue" 
:show-button="false" :form-items="formItems" />
+    </a-modal>
+  </div>
+</template>
+
+<style lang="scss" scoped></style>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
new file mode 100644
index 00000000..3e220a38
--- /dev/null
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
@@ -0,0 +1,184 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { computed, ref, shallowRef } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { message, Modal, TableColumnType } from 'ant-design-vue'
+  import useBaseTable from '@/composables/use-base-table'
+  import {
+    deleteServiceConfigSnapshot,
+    getServiceConfigSnapshotsList,
+    recoveryServiceConfigSnapshot
+  } from '@/api/service'
+  import type { GroupItem } from '@/components/common/button-group/types'
+  import type { ServiceConfigSnapshot, ServiceParams, SnapshotRecovery } from 
'@/api/service/types'
+
+  type OperationType = 'Restore' | 'Remove'
+
+  const { t } = useI18n()
+  const open = ref(false)
+  const serviceInfo = shallowRef<ServiceParams>()
+
+  const columns = computed((): TableColumnType[] => [
+    {
+      title: t('service.snapshot_name'),
+      dataIndex: 'name',
+      key: 'name',
+      ellipsis: true
+    },
+    {
+      title: t('service.snapshot_notes'),
+      dataIndex: 'desc',
+      key: 'desc',
+      ellipsis: true
+    },
+    {
+      title: t('common.create_time'),
+      dataIndex: 'createTime',
+      key: 'createTime',
+      ellipsis: true
+    },
+    {
+      title: t('common.operation'),
+      dataIndex: 'operation',
+      width: '160px',
+      key: 'operation',
+      ellipsis: true
+    }
+  ])
+
+  const { loading, dataSource, resetState } = 
useBaseTable<ServiceConfigSnapshot>({
+    columns: columns.value,
+    rows: [],
+    pagination: false
+  })
+
+  const operations = computed((): GroupItem<OperationType>[] => [
+    {
+      text: 'restore',
+      action: 'Restore',
+      clickEvent: (item, args) => handleTableOperation(item!, args)
+    },
+    {
+      text: 'remove',
+      action: 'Remove',
+      danger: true,
+      clickEvent: (item, args) => handleTableOperation(item!, args)
+    }
+  ])
+
+  const operationMap = computed(() => ({
+    Restore: {
+      modalTitle: t('common.restore_msg'),
+      api: recoveryServiceConfigSnapshot,
+      successMsg: t('common.update_success')
+    },
+    Remove: {
+      modalTitle: t('common.delete_msg'),
+      api: deleteServiceConfigSnapshot,
+      successMsg: t('common.delete_success')
+    }
+  }))
+
+  const handleTableOperation = (item: GroupItem<OperationType>, payLoad: 
ServiceConfigSnapshot) => {
+    const currOperation = operationMap.value[item.action!]
+    Modal.confirm({
+      title: currOperation.modalTitle,
+      async onOk() {
+        try {
+          const data = await currOperation.api({ ...serviceInfo.value, 
snapshotId: payLoad.id } as SnapshotRecovery)
+          if (data) {
+            message.success(currOperation.successMsg)
+            resetState()
+            await getSnapshotList()
+          }
+        } catch (error) {
+          console.log('error :>> ', error)
+        }
+      }
+    })
+  }
+
+  const handleOpen = (data: ServiceParams) => {
+    serviceInfo.value = data
+    open.value = true
+    getSnapshotList()
+  }
+
+  const getSnapshotList = async () => {
+    if (!serviceInfo.value) {
+      return
+    }
+    try {
+      loading.value = true
+      dataSource.value = await getServiceConfigSnapshotsList(serviceInfo.value)
+    } catch (error) {
+      console.log('error :>> ', error)
+    } finally {
+      loading.value = false
+    }
+  }
+
+  const handleOk = () => {
+    handleCancel()
+  }
+
+  const handleCancel = () => {
+    open.value = false
+  }
+
+  defineExpose({
+    handleOpen
+  })
+</script>
+
+<template>
+  <div>
+    <a-modal
+      v-model:open="open"
+      width="800px"
+      :centered="true"
+      :mask="false"
+      :title="$t('service.snapshot_management')"
+      :mask-closable="false"
+      :destroy-on-close="true"
+      @ok="handleOk"
+      @cancel="handleCancel"
+    >
+      <a-table :loading="loading" :scroll="{ y: 340 }" 
:data-source="dataSource" :columns="columns">
+        <template #bodyCell="{ record, column }">
+          <template v-if="column.key === 'operation'">
+            <button-group
+              i18n="common"
+              :text-compact="true"
+              :space="24"
+              :groups="operations"
+              :args="record"
+              group-shape="default"
+              group-type="link"
+            />
+          </template>
+        </template>
+      </a-table>
+    </a-modal>
+  </div>
+</template>
+
+<style lang="scss" scoped></style>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/configs.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/configs.vue
new file mode 100644
index 00000000..66c9b094
--- /dev/null
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/configs.vue
@@ -0,0 +1,244 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { onActivated, inject, useAttrs, shallowRef, ref, onDeactivated, 
ComputedRef } from 'vue'
+  import { debounce } from 'lodash'
+  import { useI18n } from 'vue-i18n'
+  import { Empty, message } from 'ant-design-vue'
+  import { updateServiceConfigs } from '@/api/service'
+  import CaptureSnapshot from './components/capture-snapshot.vue'
+  import SnapshotManagement from './components/snapshot-management.vue'
+  import type { ServiceConfig, ServiceVO } from '@/api/service/types'
+
+  interface ServieceInfo {
+    cluster: string
+    id: number
+    service: string
+    serviceId: number
+  }
+
+  const { routeParams }: { routeParams: ComputedRef<ServieceInfo> } = 
inject('service') as any
+  const attrs = useAttrs() as unknown as ServiceVO
+  const { t } = useI18n()
+  const getServiceDetail = inject('getServiceDetail') as () => any
+  const searchStr = ref('')
+  const loading = ref(false)
+  const activeKey = ref<number[]>([])
+  const configs = ref<ServiceConfig[]>([])
+  const filterConfigs = ref<ServiceConfig[]>([])
+  const captureRef = ref<InstanceType<typeof CaptureSnapshot>>()
+  const snapshotRef = ref<InstanceType<typeof SnapshotManagement>>()
+  const debouncedOnSearch = ref()
+  const layout = shallowRef({
+    labelCol: {
+      xs: { span: 24 },
+      sm: { span: 8 },
+      md: { span: 8 },
+      lg: { span: 6 }
+    },
+    wrapperCol: {
+      xs: { span: 24 },
+      sm: { span: 16 },
+      md: { span: 16 },
+      lg: { span: 18 }
+    }
+  })
+
+  const createNewConfigItem = () => {
+    return {
+      name: '',
+      displayName: '',
+      value: '',
+      isManual: true
+    }
+  }
+
+  const manualAddConfig = (config: ServiceConfig) => {
+    config.properties?.push(createNewConfigItem())
+  }
+
+  const filterConfigurations = () => {
+    if (!searchStr.value) {
+      filterConfigs.value = configs.value
+    }
+    const lowerSearchTerm = searchStr.value.toLowerCase()
+    filterConfigs.value = configs.value.filter((config) => {
+      return config.properties?.some((property) => {
+        return (
+          (property.displayName || '').toLowerCase().includes(lowerSearchTerm) 
||
+          property.name.toLowerCase().includes(lowerSearchTerm) ||
+          (property.value && 
property.value.toString().toLowerCase().includes(lowerSearchTerm))
+        )
+      })
+    })
+  }
+
+  const saveConfigs = async () => {
+    try {
+      const { serviceId, id: clusterId } = routeParams.value
+      loading.value = true
+      const data = await updateServiceConfigs({ id: serviceId, clusterId }, 
[...configs.value])
+      if (data) {
+        message.success(t('common.update_success'))
+        getServiceDetail()
+      }
+    } catch (error) {
+      console.log('error :>> ', error)
+    } finally {
+      loading.value = false
+    }
+  }
+
+  const onCaptureSnapshot = () => {
+    const { serviceId, id: clusterId } = routeParams.value
+    captureRef.value?.handleOpen({ id: serviceId, clusterId })
+  }
+
+  const openSnapshotManagement = () => {
+    const { serviceId, id: clusterId } = routeParams.value
+    snapshotRef.value?.handleOpen({ id: serviceId, clusterId })
+  }
+
+  onActivated(() => {
+    configs.value = attrs.configs as ServiceConfig[]
+    filterConfigs.value = [...configs.value]
+    activeKey.value = filterConfigs.value.map((v) => v.id!)
+    debouncedOnSearch.value = debounce(filterConfigurations, 300)
+  })
+
+  onDeactivated(() => {
+    debouncedOnSearch.value.cancel()
+  })
+</script>
+
+<template>
+  <div class="service-configurator">
+    <header>
+      <div class="header-title">{{ $t('common.configs') }}</div>
+      <div class="list-operation">
+        <a-space>
+          <a-button type="primary" @click="onCaptureSnapshot">{{ 
$t('service.capture_snapshot') }}</a-button>
+          <a-button @click="openSnapshotManagement">{{ 
$t('service.snapshot_management') }}</a-button>
+        </a-space>
+        <a-input
+          v-model:value="searchStr"
+          :placeholder="$t('service.please_enter_search_keyword')"
+          @input="debouncedOnSearch"
+        />
+      </div>
+    </header>
+    <section>
+      <a-empty v-if="filterConfigs.length === 0" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
+      <a-form v-else :label-wrap="true" :disabled="loading">
+        <a-collapse v-model:active-key="activeKey" :bordered="false" 
:ghost="true">
+          <a-collapse-panel v-for="config in filterConfigs" :key="config.id">
+            <template #extra>
+              <a-button type="text" shape="circle" 
@click.stop="manualAddConfig(config)">
+                <template #icon>
+                  <svg-icon name="plus_dark" />
+                </template>
+              </a-button>
+            </template>
+            <template #header>
+              <span>{{ config.name }}</span>
+            </template>
+            <a-row v-for="(item, idx) in config.properties" :key="idx" 
:gutter="[16, 0]" :wrap="true">
+              <a-col v-bind="layout.labelCol">
+                <a-form-item>
+                  <a-textarea v-if="item.isManual" v-model:value="item.name" 
:auto-size="{ minRows: 1, maxRows: 5 }" />
+                  <span v-else style="overflow-wrap: break-word" 
:title="item.displayName ?? item.name">
+                    {{ item.displayName ?? item.name }}
+                  </span>
+                </a-form-item>
+              </a-col>
+              <a-col v-bind="layout.wrapperCol">
+                <a-form-item>
+                  <a-textarea v-model:value="item.value" :auto-size="{ 
minRows: 1, maxRows: 5 }" />
+                </a-form-item>
+              </a-col>
+            </a-row>
+          </a-collapse-panel>
+        </a-collapse>
+      </a-form>
+    </section>
+    <div style="text-align: end">
+      <a-button type="primary" :loading="loading" @click="saveConfigs">
+        {{ $t('common.save') }}
+      </a-button>
+    </div>
+    <capture-snapshot ref="captureRef" @success="getServiceDetail" />
+    <snapshot-management ref="snapshotRef" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+  .highlight {
+    background-color: rgb(255, 192, 105);
+    padding: 0px;
+  }
+
+  .service-configurator {
+    display: grid;
+    gap: 16px;
+    .list-operation {
+      display: flex;
+      justify-content: space-between;
+      gap: 16px;
+      .ant-input {
+        flex: 0 1 160px;
+      }
+    }
+    :deep(.ant-collapse-header) {
+      display: flex;
+      align-items: center;
+      background-color: $color-fill-quaternary;
+      border-bottom: 1px solid $color-border-secondary;
+    }
+
+    :deep(.ant-collapse-content-box) {
+      padding-inline: 24px !important;
+    }
+
+    .ant-form {
+      max-height: 500px;
+      overflow: auto;
+    }
+
+    .list-title {
+      display: flex;
+      height: 32px;
+      align-items: center;
+      justify-content: space-between;
+      font-weight: 500;
+      border-bottom: 1px solid $color-border;
+      padding-bottom: 16px;
+      margin-bottom: 16px;
+
+      .ant-input {
+        flex: 0 1 160px;
+      }
+    }
+
+    .divider {
+      height: 100%;
+      margin-inline: 16px;
+    }
+  }
+</style>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
new file mode 100644
index 00000000..92491d5f
--- /dev/null
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
@@ -0,0 +1,129 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { computed, onMounted, provide, ref, shallowRef } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { storeToRefs } from 'pinia'
+  // import { execCommand } from '@/api/command'
+  // import { Command } from '@/api/command/types'
+  // import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { useServiceStore } from '@/store/service'
+  import { useRoute } from 'vue-router'
+  import Overview from './overview.vue'
+  import Components from './components.vue'
+  import Configs from './configs.vue'
+  import type { TabItem } from '@/components/common/main-card/types'
+  import type { GroupItem } from '@/components/common/button-group/types'
+  import type { ServiceVO } from '@/api/service/types'
+
+  interface ServieceInfo {
+    cluster: string
+    id: number
+    service: string
+    serviceId: number
+  }
+
+  const { t } = useI18n()
+  const route = useRoute()
+  const serviceStore = useServiceStore()
+  const { loading } = storeToRefs(serviceStore)
+  const activeKey = ref('1')
+  const serviceDetail = shallowRef<ServiceVO>()
+  const routeParams = computed(() => route.params as unknown as ServieceInfo)
+  const tabs = computed((): TabItem[] => [
+    {
+      key: '1',
+      title: t('common.overview')
+    },
+    {
+      key: '2',
+      title: t('common.component')
+    },
+    {
+      key: '3',
+      title: t('common.configs')
+    }
+  ])
+  const actionGroup = computed<GroupItem[]>(() => [
+    {
+      shape: 'default',
+      type: 'primary',
+      text: t('common.operation'),
+      dropdownMenu: [
+        {
+          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 getCompName = computed(() => {
+    const components = [Overview, Components, Configs]
+    return components[parseInt(activeKey.value) - 1]
+  })
+
+  const getServiceDetail = async () => {
+    try {
+      loading.value = true
+      const { id: clusterId, serviceId } = routeParams.value
+      serviceDetail.value = await serviceStore.getServiceDetail(clusterId, 
serviceId)
+    } catch (error) {
+      console.log('error :>> ', error)
+    } finally {
+      loading.value = false
+    }
+  }
+
+  provide('getServiceDetail', getServiceDetail)
+  provide('service', { routeParams })
+
+  onMounted(() => {
+    getServiceDetail()
+  })
+</script>
+
+<template>
+  <a-spin :spinning="loading">
+    <header-card
+      :title="serviceDetail?.displayName || serviceDetail?.name"
+      :avatar="serviceDetail?.name"
+      :desc="serviceDetail?.desc"
+      :action-groups="actionGroup"
+    />
+    <main-card v-model:active-key="activeKey" :tabs="tabs">
+      <template #tab-item>
+        <keep-alive>
+          <component :is="getCompName" v-bind="serviceDetail"></component>
+        </keep-alive>
+      </template>
+    </main-card>
+  </a-spin>
+</template>
+
+<style lang="scss" scoped></style>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/overview.vue
similarity index 53%
copy from bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
copy to 
bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/overview.vue
index 689847ae..ee322efc 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/overview.vue
@@ -18,18 +18,12 @@
 -->
 
 <script setup lang="ts">
-  import { computed, ref, shallowRef, toRefs, watch } from 'vue'
+  import { computed, ref, shallowRef, useAttrs } from 'vue'
   import { useI18n } from 'vue-i18n'
-  import { Empty, MenuProps } from 'ant-design-vue'
-  import { formatFromByte } from '@/utils/storage.ts'
-  import { usePngImage } from '@/utils/tools'
-  import { CommonStatus, CommonStatusTexts } from '@/enums/state.ts'
-  import CategoryChart from 
'@/pages/cluster-manage/cluster/components/category-chart.vue'
-  import GaugeChart from 
'@/pages/cluster-manage/cluster/components/gauge-chart.vue'
-  import { getComponentsByHost } from '@/api/hosts'
-  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 { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import GaugeChart from '../components/gauge-chart.vue'
+  import CategoryChart from '../components/category-chart.vue'
+  import type { ServiceVO, ServiceStatusType } from '@/api/service/types'
 
   type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
   type TimeRangeItem = {
@@ -37,28 +31,22 @@
     time: string
   }
 
-  interface Props {
-    hostInfo: HostVO
-  }
-
-  const props = defineProps<Props>()
-  const { hostInfo } = toRefs(props)
   const { t } = useI18n()
-  const currExecuteComponent = shallowRef<ComponentVO>({})
+  const attrs = useAttrs()
   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 componentsFromCurrentHost = shallowRef<Map<string, ComponentVO[]>>(new 
Map())
-  const needFormatFormByte = computed(() => ['totalMemorySize', 'totalDisk'])
+  const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
+    1: 'healthy',
+    2: 'unhealthy',
+    3: 'unknown'
+  })
+  const serviceDetail = computed(() => attrs as unknown as ServiceVO)
+  const serviceKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ServiceVO)[])
   const noChartData = computed(() => Object.values(chartData.value).every((v) 
=> v.length === 0))
   const timeRanges = computed((): TimeRangeItem[] => [
     {
@@ -86,77 +74,21 @@
       time: ''
     }
   ])
-  const baseConfig = computed((): Partial<Record<keyof HostVO, string>> => {
+  const baseConfig = computed((): Partial<Record<keyof ServiceVO, string>> => {
     return {
-      status: t('overview.host_status'),
-      hostname: t('overview.host_name'),
-      desc: t('overview.host_desc'),
-      clusterDisplayName: t('common.cluster'),
-      componentNum: t('overview.component_count'),
-      os: t('overview.os'),
-      ipv4: t('overview.ip_v4'),
-      ipv6: t('overview.ip_v6'),
-      arch: t('overview.arch'),
-      availableProcessors: t('overview.core_count'),
-      totalMemorySize: t('overview.memory'),
-      totalDisk: t('overview.disk_size')
+      status: t('overview.service_status'),
+      displayName: t('overview.service_name'),
+      version: t('overview.service_version'),
+      stack: t('common.stack'),
+      restartFlag: t('service.required_restart'),
+      metrics: t('overview.metrics'),
+      kerberos: t('overview.kerberos')
     }
   })
-  const unitOfBaseConfig = computed(
-    (): Partial<Record<keyof HostVO, string>> => ({
-      componentNum: t('overview.unit_component'),
-      availableProcessors: t('overview.unit_core')
-    })
-  )
-  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 handleHostOperate: MenuProps['onClick'] = (item) => {
-    console.log('item :>> ', item.key)
-    console.log(currExecuteComponent.value)
-  }
 
   const handleTimeRange = (time: TimeRangeItem) => {
     currTimeRange.value = time.text
   }
-
-  const recordClickComp = (comp: ComponentVO) => {
-    currExecuteComponent.value = comp
-  }
-
-  watch(
-    () => hostInfo.value,
-    async (val) => {
-      if (val.id) {
-        try {
-          const data = await getComponentsByHost({ id: val.id })
-          componentsFromCurrentHost.value = data.reduce((pre, val) => {
-            if (!pre.has(val.stack!)) {
-              pre.set(val.stack!, [val])
-            } else {
-              ;(pre.get(val.stack!) as ComponentVO[]).push(val)
-            }
-            return pre
-          }, new Map<string, ComponentVO[]>())
-        } catch (error) {
-          console.log(error)
-        }
-      }
-    }
-  )
 </script>
 
 <template>
@@ -177,35 +109,37 @@
                 </template>
                 <div class="desc-sub-item-wrp">
                   <div class="desc-sub-item">
-                    <template v-for="base in detailKeys" :key="base">
+                    <template v-for="key in serviceKeys" :key="key">
                       <div class="desc-sub-item-desc">
                         <a-typography-text
                           class="desc-sub-item-desc-column"
                           type="secondary"
-                          :content="baseConfig[base]"
+                          :content="baseConfig[`${key}`]"
                         />
                         <a-tag
-                          v-if="base === 'status'"
+                          v-if="key === 'status'"
                           class="reset-tag"
-                          :color="CommonStatus[statusColors[hostInfo[base] as 
ClusterStatusType]]"
+                          
:color="CommonStatus[statusColors[serviceDetail[key]]]"
                         >
-                          <status-dot 
:color="CommonStatus[statusColors[hostInfo[base] as ClusterStatusType]]" />
-                          {{ hostInfo[base] && 
$t(`common.${statusColors[hostInfo[base] as ClusterStatusType]}`) }}
+                          <status-dot 
:color="CommonStatus[statusColors[serviceDetail[key]]]" />
+                          {{ serviceDetail[key] && 
$t(`common.${statusColors[serviceDetail[key]]}`) }}
                         </a-tag>
-                        <a-typography-text v-else 
class="desc-sub-item-desc-column">
-                          <span 
v-if="Object.keys(unitOfBaseConfig).includes(base as string)">
-                            {{ `${hostInfo[base]} ${unitOfBaseConfig[base]}` }}
-                          </span>
-                          <span v-else>
-                            {{
-                              `${
-                                needFormatFormByte.includes(base as string)
-                                  ? formatFromByte(hostInfo[base])
-                                  : hostInfo[base] || '--'
-                              }`
-                            }}
-                          </span>
-                        </a-typography-text>
+                        <a-typography-text
+                          v-else-if="key === 'stack'"
+                          class="desc-sub-item-desc-column"
+                          :content="serviceDetail[key]?.toLowerCase()"
+                        />
+                        <a-typography-text
+                          v-else-if="key === 'restartFlag'"
+                          class="desc-sub-item-desc-column"
+                          :content="serviceDetail[key] ? $t('common.yes') : 
$t('common.no')"
+                        />
+                        <a-typography-text
+                          v-else-if="['kerberos', 'metrics'].includes(key)"
+                          class="desc-sub-item-desc-column"
+                          :content="$t('common.disabled')"
+                        />
+                        <a-typography-text v-else 
class="desc-sub-item-desc-column" :content="serviceDetail[key]" />
                       </div>
                     </template>
                   </div>
@@ -214,46 +148,6 @@
             </a-descriptions>
           </div>
         </div>
-
-        <template v-if="componentsFromCurrentHost.size === 0">
-          <div class="component-info">
-            <div class="box-title">
-              <a-typography-text strong 
:content="$t('overview.component_info')" />
-            </div>
-            <div class="box-empty">
-              <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
-            </div>
-          </div>
-        </template>
-        <a-descriptions v-else layout="vertical" bordered :column="1">
-          <template #title>
-            <a-typography-text strong :content="$t('overview.component_info')" 
/>
-          </template>
-          <a-descriptions-item v-for="stack in 
componentsFromCurrentHost.keys()" :key="stack">
-            <template #label>
-              <div class="desc-sub-label">
-                <a-typography-text strong :content="stack.split('-')[0] + ' 
Stack'" />
-                <a-typography-text type="secondary" 
:content="`${stack.toLowerCase()}`" />
-              </div>
-            </template>
-            <div v-for="comp in componentsFromCurrentHost.get(stack)" 
:key="comp.id" class="component-item">
-              <a-avatar v-if="comp.serviceName" 
:src="usePngImage(comp.serviceName.toLowerCase())" :size="22" />
-              <a-typography-text 
:content="`${comp.serviceDisplayName}/${comp.displayName}`" />
-              <a-dropdown :trigger="['click']">
-                <a-button type="text" shape="circle" size="small" 
@click="recordClickComp(comp)">
-                  <svg-icon name="more" style="margin: 0" />
-                </a-button>
-                <template #overlay>
-                  <a-menu @click="handleHostOperate">
-                    <a-menu-item v-for="operate in componentOperates" 
:key="operate.action">
-                      <span>{{ operate.text }}</span>
-                    </a-menu-item>
-                  </a-menu>
-                </template>
-              </a-dropdown>
-            </div>
-          </a-descriptions-item>
-        </a-descriptions>
       </a-col>
       <a-col :xs="24" :sm="24" :md="24" :lg="14" :xl="17">
         <div class="box-title">
@@ -352,15 +246,6 @@
     }
   }
 
-  .component-item {
-    display: grid;
-    grid-template-columns: auto 1fr auto;
-    gap: $space-md;
-    align-items: center;
-    padding: 12px 16px;
-    border-top: 1px solid #f0f0f0;
-  }
-
   .chart-item-wrp {
     border: 1px solid $color-border;
     margin-right: -1px;
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 8e0c606e..0f513c3e 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -24,14 +24,16 @@
   import { useI18n } from 'vue-i18n'
   import { useServiceStore } from '@/store/service'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { useRouter } from 'vue-router'
   import FilterForm from '@/components/common/filter-form/index.vue'
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { FilterFormItem } from '@/components/common/filter-form/types'
-  import type { ServiceListParams, ServiceStatusType } from 
'@/api/service/types'
+  import type { ServiceListParams, ServiceStatusType, ServiceVO } from 
'@/api/service/types'
   import type { ClusterVO } from '@/api/cluster/types'
 
   const { t } = useI18n()
   const attrs = useAttrs() as ClusterVO
+  const router = useRouter()
   const serviceStore = useServiceStore()
   const { services, loading } = toRefs(serviceStore)
   const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
@@ -117,6 +119,13 @@
     attrs.id != undefined && serviceStore.getServices(attrs.id, filters)
   }
 
+  const viewServiceDetail = (payload: ServiceVO) => {
+    router.push({
+      name: 'ServiceDetail',
+      params: { service: payload.name, serviceId: payload.id }
+    })
+  }
+
   onActivated(() => {
     getServices()
   })
@@ -127,7 +136,13 @@
     <filter-form :filter-items="filterFormItems" @filter="getServices" />
     <a-empty v-if="services.length == 0" style="width: 100%" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
     <div v-else class="service-item-wrp">
-      <a-card v-for="item in services" :key="item.id" :hoverable="true" 
class="service-item">
+      <a-card
+        v-for="item in services"
+        :key="item.id"
+        :hoverable="true"
+        class="service-item"
+        @click="viewServiceDetail(item)"
+      >
         <div class="header">
           <div class="header-base-wrp">
             <a-avatar v-if="item.name" 
:src="usePngImage(item.name.toLowerCase())" :size="42" class="header-icon" />
@@ -150,7 +165,7 @@
             <span class="small">{{ `${item.restartFlag ? $t('common.required') 
: $t('common.not_required')}` }}</span>
           </div>
         </div>
-        <div class="item-content">
+        <div class="item-content" @click.stop>
           <button-group :auto="true" :space="0" :groups="actionGroups">
             <template #icon="{ item: groupItem }">
               <svg-icon :name="groupItem.icon || ''" />
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 caf4eda8..1b2269a4 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
@@ -126,7 +126,7 @@
       ]
     },
     {
-      title: t('common.action'),
+      title: t('common.operation'),
       key: 'operation',
       width: '160px',
       fixed: 'right'
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
index a55797ac..7b161954 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
@@ -99,7 +99,7 @@
       ]
     },
     {
-      title: t('common.action'),
+      title: t('common.operation'),
       width: '180px',
       key: 'operation',
       fixed: 'right'
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 689847ae..e1c2f6be 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -89,7 +89,7 @@
   const baseConfig = computed((): Partial<Record<keyof HostVO, string>> => {
     return {
       status: t('overview.host_status'),
-      hostname: t('overview.host_name'),
+      hostname: t('overview.hostname'),
       desc: t('overview.host_desc'),
       clusterDisplayName: t('common.cluster'),
       componentNum: t('overview.component_count'),
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 915374f4..1b5c0f75 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
@@ -23,13 +23,15 @@
   import { useI18n } from 'vue-i18n'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import { useServiceStore } from '@/store/service'
-  import type { ServiceListParams, ServiceStatusType } from 
'@/api/service/types'
+  import { useRouter } from 'vue-router'
+  import type { ServiceListParams, ServiceStatusType, ServiceVO } from 
'@/api/service/types'
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { FilterFormItem } from '@/components/common/filter-form/types'
   // import { execCommand } from '@/api/command';
   import type { Command } from '@/api/command/types'
 
   const { t } = useI18n()
+  const router = useRouter()
   const serviceStore = useServiceStore()
   const { services, loading } = toRefs(serviceStore)
 
@@ -109,6 +111,13 @@
     await serviceStore.getServices(0, filters)
   }
 
+  const viewServiceDetail = (payload: ServiceVO) => {
+    router.push({
+      name: 'InfraServiceDetail',
+      params: { id: 0, service: payload.name, serviceId: payload.id }
+    })
+  }
+
   onMounted(async () => {
     await getServices()
   })
@@ -134,7 +143,13 @@
       <filter-form :filter-items="filterFormItems" @filter="getServices" />
       <a-empty v-if="services.length == 0" style="width: 100%" />
       <template v-else>
-        <a-card v-for="item in services" :key="item.name" :hoverable="true" 
class="service-item">
+        <a-card
+          v-for="item in services"
+          :key="item.name"
+          :hoverable="true"
+          class="service-item"
+          @click="viewServiceDetail(item)"
+        >
           <div class="header">
             <div class="header-base-wrp">
               <a-avatar v-if="item.name" 
:src="usePngImage(item.name.toLowerCase())" :size="42" class="header-icon" />
@@ -157,7 +172,7 @@
               <span class="small">{{ `${item.restartFlag ? 
$t('common.required') : $t('common.not_required')}` }}</span>
             </div>
           </div>
-          <div class="item-content">
+          <div class="item-content" @click.stop>
             <button-group :auto="true" :space="0" :args="item" 
:groups="actionGroups">
               <template #icon="{ item: groupItem }">
                 <svg-icon :name="groupItem.icon || ''" />
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts 
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index 031bd82b..d226b313 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -70,6 +70,14 @@ const routes: RouteRecordRaw[] = [
             meta: {
               hidden: true
             }
+          },
+          {
+            name: 'ServiceDetail',
+            path: 
`${RouteExceptions.DYNAMIC_ROUTE_MATCH}/service-detail/:service/:serviceId`,
+            component: () => 
import('@/pages/cluster-manage/cluster/service-management/index.vue'),
+            meta: {
+              hidden: true
+            }
           }
         ]
       },
@@ -99,6 +107,15 @@ const routes: RouteRecordRaw[] = [
               hidden: true,
               activeMenu: '/cluster-manage/infrastructures'
             }
+          },
+          {
+            name: 'InfraServiceDetail',
+            path: 
'create-infra-service/service-detail/:id/:service/:serviceId',
+            component: () => 
import('@/pages/cluster-manage/cluster/service-management/index.vue'),
+            meta: {
+              hidden: true,
+              activeMenu: '/cluster-manage/infrastructures'
+            }
           }
         ]
       },
diff --git a/bigtop-manager-ui/src/store/menu/index.ts 
b/bigtop-manager-ui/src/store/menu/index.ts
index eef4e6f7..6a31508d 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -85,6 +85,9 @@ export const useMenuStore = defineStore(
       if (route.name === 'CreateService') {
         activeMenu = route.path.split('/').slice(0, -2).join('/')
       }
+      if (route.name === 'ServiceDetail') {
+        activeMenu = route.path.split('/').slice(0, -3).join('/')
+      }
       const matchedNames = [RouteExceptions.SPECIAL_ROUTE_NAME, 
RouteExceptions.DEFAULT_ROUTE_NAME] as string[]
       headerSelectedKey.value = route.matched[0].path
 

Reply via email to