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 ade59c2f BIGTOP-4371: Add cluster management page (#179)
ade59c2f is described below

commit ade59c2fe3f75581c6a6b20228e47c8d2668c4ad
Author: Fdefined <[email protected]>
AuthorDate: Wed Feb 19 20:41:42 2025 +0800

    BIGTOP-4371: Add cluster management page (#179)
---
 bigtop-manager-ui/src/api/cluster/index.ts         |  13 +-
 bigtop-manager-ui/src/api/cluster/types.ts         |  12 +-
 bigtop-manager-ui/src/api/command/types.ts         |  12 +-
 bigtop-manager-ui/src/api/hosts/index.ts           |   7 +-
 bigtop-manager-ui/src/api/hosts/types.ts           |  20 +-
 bigtop-manager-ui/src/api/job/index.ts             |  70 ++--
 bigtop-manager-ui/src/api/job/types.ts             |  20 +-
 bigtop-manager-ui/src/api/request-util.ts          |  11 +-
 bigtop-manager-ui/src/api/service/index.ts         |  70 ++--
 bigtop-manager-ui/src/api/service/types.ts         |   8 +-
 bigtop-manager-ui/src/api/sse/index.ts             |  37 --
 bigtop-manager-ui/src/api/types.ts                 |   7 +
 bigtop-manager-ui/src/api/user/types.ts            |   1 -
 .../images/svg/bottom-activated.svg}               |  40 +-
 bigtop-manager-ui/src/assets/images/svg/copy.svg   |  29 ++
 .../index.vue => assets/images/svg/download.svg}   |  46 +--
 .../src/components/common/button-group/index.vue   |   2 +-
 .../src/components/common/filter-form/index.vue    | 120 ++++--
 .../src/components/common/header-card/index.vue    |   4 +-
 .../src/components/common/main-card/index.vue      |   5 +-
 .../src/components/common/status-dot/index.vue     |  12 +-
 .../src/components/common/svg-icon/index.vue       |   1 +
 .../src/components/job/custom-progress.vue         |  93 +++++
 bigtop-manager-ui/src/components/job/index.vue     | 237 ++++++++++++
 .../src/components/log-view/index.vue              | 251 +++++++++++++
 .../src/composables/use-base-table.ts              |  30 +-
 bigtop-manager-ui/src/enums/state.ts               |  39 +-
 bigtop-manager-ui/src/layouts/sider.vue            |   7 +-
 bigtop-manager-ui/src/locales/en_US/cluster.ts     |  22 +-
 bigtop-manager-ui/src/locales/en_US/common.ts      |  32 +-
 bigtop-manager-ui/src/locales/en_US/host.ts        |   6 +-
 bigtop-manager-ui/src/locales/en_US/index.ts       |   6 +-
 bigtop-manager-ui/src/locales/en_US/job.ts         |   4 +-
 bigtop-manager-ui/src/locales/en_US/llm-config.ts  |   4 +-
 .../locales/{zh_CN/job.ts => en_US/overview.ts}    |  20 +-
 .../{api/sse/types.ts => locales/en_US/service.ts} |  11 +-
 bigtop-manager-ui/src/locales/en_US/user.ts        |   1 +
 bigtop-manager-ui/src/locales/zh_CN/common.ts      |  18 +-
 bigtop-manager-ui/src/locales/zh_CN/host.ts        |   8 +-
 bigtop-manager-ui/src/locales/zh_CN/index.ts       |   6 +-
 bigtop-manager-ui/src/locales/zh_CN/job.ts         |   4 +-
 .../src/locales/zh_CN/{job.ts => overview.ts}      |  20 +-
 .../src/locales/zh_CN/{job.ts => service.ts}       |   4 +-
 bigtop-manager-ui/src/locales/zh_CN/user.ts        |   1 +
 .../cluster/components/check-workflow.vue          |  38 +-
 .../cluster/components/cluster-base.vue            |   4 +-
 .../cluster/components/component-info.vue          |   6 +-
 .../cluster-manage/cluster/components/mock.ts      | 187 ----------
 .../src/pages/cluster-manage/cluster/create.vue    |  13 +-
 .../src/pages/cluster-manage/cluster/host.vue      | 146 +++++---
 .../src/pages/cluster-manage/cluster/index.vue     |  95 +++--
 .../src/pages/cluster-manage/cluster/job.vue       |  94 -----
 .../src/pages/cluster-manage/cluster/overview.vue  | 409 ++++++++++++---------
 .../src/pages/cluster-manage/cluster/service.vue   | 116 +++---
 .../src/pages/cluster-manage/cluster/user.vue      |  67 ++--
 .../src/pages/cluster-manage/hosts/create.vue      |  17 +-
 bigtop-manager-ui/src/store/cluster/index.ts       |  44 ++-
 bigtop-manager-ui/src/store/service/index.ts       |  65 ++++
 bigtop-manager-ui/src/styles/index.scss            |   6 +
 bigtop-manager-ui/src/utils/tools.ts               |  20 +
 .../tests/__composables__/use-table.test.ts        |   1 -
 61 files changed, 1693 insertions(+), 1006 deletions(-)

diff --git a/bigtop-manager-ui/src/api/cluster/index.ts 
b/bigtop-manager-ui/src/api/cluster/index.ts
index ed38562f..ac5206c8 100644
--- a/bigtop-manager-ui/src/api/cluster/index.ts
+++ b/bigtop-manager-ui/src/api/cluster/index.ts
@@ -18,9 +18,10 @@
  */
 
 import request from '@/api/request.ts'
-import type { ClusterVO, UpdateClusterParam } from './types'
+import type { PageVO, ListParams } from '@/api/types'
+import type { ClusterVO, ServiceUserVO, UpdateClusterParam } from './types'
 
-export const getCluster = (id: number): Promise<ClusterVO[]> => {
+export const getCluster = (id: number): Promise<ClusterVO> => {
   return request({
     method: 'get',
     url: `/clusters/${id}`
@@ -41,3 +42,11 @@ export const getClusterList = (): Promise<ClusterVO[]> => {
     url: '/clusters'
   })
 }
+
+export const getUserListOfService = (id: number, params: ListParams): 
Promise<PageVO<ServiceUserVO[]>> => {
+  return request({
+    method: 'get',
+    url: `/clusters/${id}/services/users`,
+    params
+  })
+}
diff --git a/bigtop-manager-ui/src/api/cluster/types.ts 
b/bigtop-manager-ui/src/api/cluster/types.ts
index b9f8beb5..4ddb4769 100644
--- a/bigtop-manager-ui/src/api/cluster/types.ts
+++ b/bigtop-manager-ui/src/api/cluster/types.ts
@@ -19,6 +19,8 @@
 
 export type UpdateClusterParam = { name: string; desc: string }
 
+export type ClusterStatusType = 1 | 2 | 3
+
 export interface ClusterVO {
   createUser?: string
   desc?: string
@@ -26,7 +28,7 @@ export interface ClusterVO {
   id?: number
   name?: string
   rootDir?: string
-  status?: number
+  status?: ClusterStatusType
   totalDisk?: number
   totalHost?: number
   totalMemory?: number
@@ -35,3 +37,11 @@ export interface ClusterVO {
   type?: number
   userGroup?: string
 }
+
+export interface ServiceUserVO {
+  desc?: string
+  displayName?: string
+  user?: string
+  userGroup?: string
+  [property: string]: any
+}
diff --git a/bigtop-manager-ui/src/api/command/types.ts 
b/bigtop-manager-ui/src/api/command/types.ts
index 30f59990..ca24cdbe 100644
--- a/bigtop-manager-ui/src/api/command/types.ts
+++ b/bigtop-manager-ui/src/api/command/types.ts
@@ -20,8 +20,8 @@
 export interface CommandRequest {
   clusterCommand?: ClusterCommandReq
   clusterId?: number
-  command: Command
-  commandLevel: CommandLevel
+  command: keyof typeof Command
+  commandLevel: keyof typeof CommandLevel
   componentCommands?: ComponentCommandReq[]
   customCommand?: string
   hostCommands?: HostCommandReq[]
@@ -70,10 +70,10 @@ export enum Command {
 }
 
 export enum CommandLevel {
-  Cluster = 'cluster',
-  Component = 'component',
-  Host = 'host',
-  Service = 'service'
+  cluster = 'cluster',
+  component = 'component',
+  host = 'host',
+  service = 'service'
 }
 
 export interface ComponentCommandReq {
diff --git a/bigtop-manager-ui/src/api/hosts/index.ts 
b/bigtop-manager-ui/src/api/hosts/index.ts
index f970167e..0f401364 100644
--- a/bigtop-manager-ui/src/api/hosts/index.ts
+++ b/bigtop-manager-ui/src/api/hosts/index.ts
@@ -18,12 +18,13 @@
  */
 
 import request from '@/api/request.ts'
-import { HostParams, HostVO, HostVOList, InstalledStatusVO } from 
'@/api/hosts/types.ts'
+import type { HostListParams, HostParams, HostVO, HostVOList, 
InstalledStatusVO } from '@/api/hosts/types.ts'
 
-export const getHosts = (): Promise<HostVOList> => {
+export const getHosts = (params?: HostListParams): Promise<HostVOList> => {
   return request({
     method: 'get',
-    url: '/hosts'
+    url: '/hosts',
+    params
   })
 }
 
diff --git a/bigtop-manager-ui/src/api/hosts/types.ts 
b/bigtop-manager-ui/src/api/hosts/types.ts
index 519a3c29..06e67b0a 100644
--- a/bigtop-manager-ui/src/api/hosts/types.ts
+++ b/bigtop-manager-ui/src/api/hosts/types.ts
@@ -54,6 +54,17 @@ export interface HostVO {
   [property: string]: any
 }
 
+export interface HostListParams {
+  clusterId?: number
+  hostname?: string
+  ipv4?: string
+  orderBy?: string
+  pageNum?: number
+  pageSize?: number
+  sort?: string
+  status?: number
+}
+
 export interface HostParams {
   agentDir?: string
   authType?: number | string // '1-password,2-key,3-no_auth',
@@ -77,8 +88,15 @@ export interface InstalledStatusVO {
 }
 
 export enum Status {
-  Failed = 'FAILED',
   Installing = 'INSTALLING',
   Success = 'SUCCESS',
+  Failed = 'FAILED',
   Unknown = 'UNKNOW'
 }
+
+export enum BaseStatus {
+  'INSTALLING',
+  'SUCCESS',
+  'FAILED',
+  'UNKNOW'
+}
diff --git a/bigtop-manager-ui/src/api/job/index.ts 
b/bigtop-manager-ui/src/api/job/index.ts
index c7aaa052..ebfc4f35 100644
--- a/bigtop-manager-ui/src/api/job/index.ts
+++ b/bigtop-manager-ui/src/api/job/index.ts
@@ -16,47 +16,57 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import request from '@/api/request.ts'
-import type { JobParams, JobList, TaskLogParams, TaskParams, JobVO, StageVO, 
TaskVO } from './types'
+import type {
+  JobParams,
+  JobList,
+  TaskLogParams,
+  JobVO,
+  JobListParams,
+  TaskListParams,
+  StageListParams,
+  StageList,
+  TaskList,
+  LogsRes
+} from './types'
+import type { ListParams } from '../types'
+import { get, post } from '../request-util'
+import axios, { type AxiosProgressEvent, type CancelTokenSource } from 'axios'
+import request from '../request'
 
-export const retryJob = (pathParams: JobParams): Promise<JobVO> => {
-  return request({
-    method: 'post',
-    url: `/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/retry`
-  })
+export const retryJob = (pathParams: JobParams) => {
+  return 
post<JobVO>(`/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/retry`)
 }
 
-export const getJobList = (clusterId: number): Promise<JobList> => {
-  return request({
-    method: 'get',
-    url: `/clusters/${clusterId}/jobs`
-  })
+export const getJobDetails = (pathParams: JobParams) => {
+  return 
get<JobVO>(`/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}`)
 }
 
-export const getJobDetails = (pathParams: JobParams): Promise<JobVO> => {
-  return request({
-    method: 'get',
-    url: `/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}`
-  })
+export const getJobList = (pathParams: JobListParams, params: ListParams) => {
+  return get<JobList>(`/clusters/${pathParams.clusterId}/jobs`, params)
 }
 
-export const getStageList = (pathParams: JobParams): Promise<StageVO[]> => {
-  return request({
-    method: 'get',
-    url: `/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/stages`
-  })
+export const getStageList = (pathParams: StageListParams, params: ListParams) 
=> {
+  return 
get<StageList>(`/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/stages`,
 params)
 }
 
-export const getTaskList = (pathParams: TaskParams): Promise<TaskVO[]> => {
-  return request({
-    method: 'get',
-    url: 
`/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/stages/${pathParams.stageId}/tasks`
-  })
+export const getTaskList = (pathParams: TaskListParams, params: ListParams) => 
{
+  return get<TaskList>(
+    
`/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/stages/${pathParams.stageId}/tasks`,
+    params
+  )
 }
 
-export const getTaskLog = (pathParams: TaskLogParams): Promise<string[]> => {
-  return request({
+export const getTaskLog = (pathParams: TaskLogParams, func: Function): LogsRes 
=> {
+  const source: CancelTokenSource = axios.CancelToken.source()
+
+  const promise = request({
     method: 'get',
-    url: 
`/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/stages/${pathParams.stageId}/tasks/${pathParams.taskId}/log`
+    url: 
`/clusters/${pathParams.clusterId}/jobs/${pathParams.jobId}/stages/${pathParams.stageId}/tasks/${pathParams.taskId}/log`,
+    responseType: 'stream',
+    timeout: 0,
+    cancelToken: source.token,
+    onDownloadProgress: (progressEvent: AxiosProgressEvent) => 
func(progressEvent)
   })
+
+  return { promise, cancel: source.cancel }
 }
diff --git a/bigtop-manager-ui/src/api/job/types.ts 
b/bigtop-manager-ui/src/api/job/types.ts
index 86e54555..cbfd2764 100644
--- a/bigtop-manager-ui/src/api/job/types.ts
+++ b/bigtop-manager-ui/src/api/job/types.ts
@@ -18,22 +18,21 @@
  */
 
 import type { PageVO } from '@/api/types'
+import { JobState } from '@/enums/state'
 
 export type JobList = PageVO<JobVO>
 export type StageList = PageVO<StageVO>
+export type TaskList = PageVO<TaskVO>
+
 export type JobParams = { clusterId: number; jobId: number }
 export type TaskParams = JobParams & { stageId: number }
 export type TaskLogParams = TaskParams & { taskId: number }
 
-export type StateType = keyof typeof State
+export type StateType = keyof typeof JobState
 
-export enum State {
-  'Pending' = 'pending',
-  'Processing' = 'processing',
-  'Successful' = 'successful',
-  'Failed' = 'failed',
-  'Canceled' = 'canceled'
-}
+export type JobListParams = { clusterId: number }
+export type StageListParams = JobParams
+export type TaskListParams = TaskParams
 
 export interface JobVO {
   createTime?: string
@@ -65,3 +64,8 @@ export interface TaskVO {
   updateTime?: string
   [property: string]: any
 }
+
+export interface LogsRes {
+  promise: Promise<any>
+  cancel: () => void
+}
diff --git a/bigtop-manager-ui/src/api/request-util.ts 
b/bigtop-manager-ui/src/api/request-util.ts
index 7a2da40e..a2086b7b 100644
--- a/bigtop-manager-ui/src/api/request-util.ts
+++ b/bigtop-manager-ui/src/api/request-util.ts
@@ -16,23 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import type { ResponseEntity } from './types'
 import request from './request'
 import { AxiosRequestConfig } from 'axios'
 
-const get = <T>(url: string, config?: AxiosRequestConfig): 
Promise<ResponseEntity<T>> => {
-  return request.get(url, config)
+const get = <T, U = any>(url: string, params?: U, config?: 
AxiosRequestConfig): Promise<T> => {
+  return request.get(url, { ...config, params })
 }
 
-const post = <T, U = any>(url: string, data?: U, config?: AxiosRequestConfig): 
Promise<ResponseEntity<T>> => {
+const post = <T, U = any>(url: string, data?: U, config?: AxiosRequestConfig): 
Promise<T> => {
   return request.post(url, data, config)
 }
 
-const put = <T, U = any>(url: string, data: U, config?: AxiosRequestConfig): 
Promise<ResponseEntity<T>> => {
+const put = <T, U = any>(url: string, data: U, config?: AxiosRequestConfig): 
Promise<T> => {
   return request.put(url, data, config)
 }
 
-const del = <T>(url: string, config?: AxiosRequestConfig): 
Promise<ResponseEntity<T>> => {
+const del = <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
   return request.delete(url, config)
 }
 
diff --git a/bigtop-manager-ui/src/api/service/index.ts 
b/bigtop-manager-ui/src/api/service/index.ts
index 6642f094..507061e7 100644
--- a/bigtop-manager-ui/src/api/service/index.ts
+++ b/bigtop-manager-ui/src/api/service/index.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import type { ListParams } from '../types'
 import type {
   ServiceParams,
   ServiceVO,
@@ -24,66 +25,43 @@ import type {
   ServiceConfigSnapshot,
   ServiceList,
   SnapshotData,
-  SnapshotRecovery
+  SnapshotRecovery,
+  ServiceListParams
 } from './types'
-import request from '@/api/request.ts'
+import { get, post, del } from '@/api/request-util'
 
-export const getServiceList = (clusterId: number): Promise<ServiceList> => {
-  return request({
-    method: 'get',
-    url: `/clusters/${clusterId}/services`
-  })
+export const getServiceList = (clusterId: number, params?: ListParams & 
ServiceListParams) => {
+  return get<ServiceList>(`/clusters/${clusterId}/services`, params)
 }
 
-export const getService = (params: ServiceParams): Promise<ServiceVO> => {
-  return request({
-    method: 'get',
-    url: `/clusters/${params.clusterId}/services/${params.id}`
-  })
+export const getService = (pathParams: ServiceParams) => {
+  return 
get<ServiceVO>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}`)
 }
 
-export const getServiceConfigs = (params: ServiceParams): 
Promise<ServiceConfig[]> => {
-  return request({
-    method: 'get',
-    url: `/clusters/${params.clusterId}/services/${params.id}/configs`
-  })
+export const getServiceConfigs = (pathParams: ServiceParams) => {
+  return 
get<ServiceConfig[]>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/configs`)
 }
 
-export const updateServiceConfigs = (params: ServiceParams, data: 
ServiceConfig): Promise<ServiceConfig[]> => {
-  return request({
-    method: 'post',
-    url: `/clusters/${params.clusterId}/services/${params.id}/configs`,
-    data
-  })
+export const updateServiceConfigs = (pathParams: ServiceParams, data: 
ServiceConfig) => {
+  return 
post<ServiceConfig[]>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/configs`,
 data)
 }
 
-export const getServiceConfigSnapshotsList = (params: ServiceParams): 
Promise<ServiceConfigSnapshot[]> => {
-  return request({
-    method: 'get',
-    url: `/clusters/${params.clusterId}/services/${params.id}/config-snapshots`
-  })
+export const getServiceConfigSnapshotsList = (pathParams: ServiceParams) => {
+  return 
get<ServiceConfigSnapshot[]>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/config-snapshots`)
 }
 
-export const takeServiceConfigSnapshot = (
-  params: ServiceParams,
-  data: SnapshotData
-): Promise<ServiceConfigSnapshot> => {
-  return request({
-    method: 'post',
-    url: 
`/clusters/${params.clusterId}/services/${params.id}/config-snapshots`,
+export const takeServiceConfigSnapshot = (pathParams: ServiceParams, data: 
SnapshotData) => {
+  return post<ServiceConfigSnapshot>(
+    
`/clusters/${pathParams.clusterId}/services/${pathParams.id}/config-snapshots`,
     data
-  })
+  )
 }
 
-export const recoveryServiceConfigSnapshot = (params: SnapshotRecovery): 
Promise<ServiceConfig[]> => {
-  return request({
-    method: 'post',
-    url: 
`/clusters/${params.clusterId}/services/${params.id}/config-snapshots/${params.snapshotId}`
-  })
+export const recoveryServiceConfigSnapshot = (pathParams: SnapshotRecovery): 
Promise<ServiceConfig[]> => {
+  return post<ServiceConfig[]>(
+    
`/clusters/${pathParams.clusterId}/services/${pathParams.id}/config-snapshots/${pathParams.snapshotId}`
+  )
 }
-export const deleteServiceConfigSnapshot = (params: SnapshotRecovery): 
Promise<boolean> => {
-  return request({
-    method: 'delete',
-    url: 
`/clusters/${params.clusterId}/services/${params.id}/config-snapshots/${params.snapshotId}`
-  })
+export const deleteServiceConfigSnapshot = (pathParams: SnapshotRecovery): 
Promise<boolean> => {
+  return 
del(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/config-snapshots/${pathParams.snapshotId}`)
 }
diff --git a/bigtop-manager-ui/src/api/service/types.ts 
b/bigtop-manager-ui/src/api/service/types.ts
index 2afefea7..9ecd25a8 100644
--- a/bigtop-manager-ui/src/api/service/types.ts
+++ b/bigtop-manager-ui/src/api/service/types.ts
@@ -24,6 +24,12 @@ 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 ServiceStatusType = 1 | 2 | 3
+export type ServiceListParams = {
+  name?: string
+  restartFlag?: boolean
+  status?: ServiceStatusType
+}
 
 export interface ServiceVO {
   components?: ComponentVO[]
@@ -35,7 +41,7 @@ export interface ServiceVO {
   requiredServices?: string[]
   restartFlag?: boolean
   stack?: string
-  status?: number
+  status: ServiceStatusType
   user?: string
   version?: string
 }
diff --git a/bigtop-manager-ui/src/api/sse/index.ts 
b/bigtop-manager-ui/src/api/sse/index.ts
deleted file mode 100644
index 6b288b3b..00000000
--- a/bigtop-manager-ui/src/api/sse/index.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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
- *
- *    https://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.
- */
-
-import axios, { type AxiosProgressEvent, type CancelTokenSource } from 'axios'
-import request from '@/api/request.ts'
-import type { LogsRes } from './types'
-
-export const getLogs = (clusterId: number, id: number, func: Function): 
LogsRes => {
-  const source: CancelTokenSource = axios.CancelToken.source()
-
-  const promise = request({
-    method: 'get',
-    url: `/sse/clusters/${clusterId}/tasks/${id}/log`,
-    responseType: 'stream',
-    timeout: 0,
-    cancelToken: source.token,
-    onDownloadProgress: (progressEvent: AxiosProgressEvent) => 
func(progressEvent)
-  })
-
-  return { promise, cancel: source.cancel }
-}
diff --git a/bigtop-manager-ui/src/api/types.ts 
b/bigtop-manager-ui/src/api/types.ts
index 007cbb3a..91d65f12 100644
--- a/bigtop-manager-ui/src/api/types.ts
+++ b/bigtop-manager-ui/src/api/types.ts
@@ -27,3 +27,10 @@ export interface PageVO<T = any> {
   total: number
   content: T[]
 }
+
+export interface ListParams {
+  orderBy?: string
+  pageNum?: number
+  pageSize?: number
+  sort?: string
+}
diff --git a/bigtop-manager-ui/src/api/user/types.ts 
b/bigtop-manager-ui/src/api/user/types.ts
index 9692e5cf..5ce2bcdc 100644
--- a/bigtop-manager-ui/src/api/user/types.ts
+++ b/bigtop-manager-ui/src/api/user/types.ts
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 export interface UserVO {
   id: number
   username: string
diff --git a/bigtop-manager-ui/src/components/common/svg-icon/index.vue 
b/bigtop-manager-ui/src/assets/images/svg/bottom-activated.svg
similarity index 57%
copy from bigtop-manager-ui/src/components/common/svg-icon/index.vue
copy to bigtop-manager-ui/src/assets/images/svg/bottom-activated.svg
index c18cc3f3..46a1a50d 100644
--- a/bigtop-manager-ui/src/components/common/svg-icon/index.vue
+++ b/bigtop-manager-ui/src/assets/images/svg/bottom-activated.svg
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one
   ~ or more contributor license agreements.  See the NOTICE file
@@ -16,36 +17,9 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
 -->
-
-<script setup lang="ts">
-  import { computed } from 'vue'
-
-  interface SvgIconProps {
-    prefix?: string
-    name: string
-    color?: string
-  }
-
-  const props = withDefaults(defineProps<SvgIconProps>(), {
-    prefix: 'icon',
-    color: '#000'
-  })
-
-  const symbolId = computed(() => `#${props.prefix}-${props.name}`)
-</script>
-
-<template>
-  <svg class="svg-icon" aria-hidden="true">
-    <use :xlink:href="symbolId" :fill="color" />
-  </svg>
-</template>
-
-<style lang="scss" scoped>
-  .svg-icon {
-    height: 1.2em;
-    width: 1.2em;
-    margin: 0 6px;
-    vertical-align: -0.25em;
-    overflow: hidden;
-  }
-</style>
+<svg xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; width="9.33349609375"
+  height="5.33331298828125" viewBox="0 0 9.33349609375 5.33331298828125" 
fill="none">
+  <path stroke="rgba(22, 119, 255, 1)" stroke-width="1.3333333333333333" 
stroke-linejoin="round" stroke-linecap="round"
+    d="M8.66675 0.666656L4.66675 4.66666L0.666748 0.666656">
+  </path>
+</svg>
\ No newline at end of file
diff --git a/bigtop-manager-ui/src/assets/images/svg/copy.svg 
b/bigtop-manager-ui/src/assets/images/svg/copy.svg
new file mode 100644
index 00000000..bee76261
--- /dev/null
+++ b/bigtop-manager-ui/src/assets/images/svg/copy.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+-->
+<svg xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; width="16" height="16"
+  viewBox="0 0 16 16" fill="none">
+  <path stroke="rgba(22, 119, 255, 1)" stroke-width="1.3333333333333333" 
stroke-linejoin="round" stroke-linecap="round"
+    d="M4.33301 4.14395L4.33301 2.60425C4.33301 2.08648 4.75274 1.66675 
5.27051 1.66675L13.3955 1.66675C13.9133 1.66675 14.333 2.08648 14.333 
2.60425L14.333 10.7292C14.333 11.247 13.9133 11.6667 13.3955 11.6667L11.8384 
11.6667">
+  </path>
+  <path
+    d="M10.7295 4.33325L2.60449 4.33325C2.08673 4.33325 1.66699 4.75299 
1.66699 5.27075L1.66699 13.3958C1.66699 13.9135 2.08673 14.3333 2.60449 
14.3333L10.7295 14.3333C11.2473 14.3333 11.667 13.9135 11.667 13.3958L11.667 
5.27075C11.667 4.75299 11.2473 4.33325 10.7295 4.33325Z"
+    stroke="rgba(22, 119, 255, 1)" stroke-width="1.3333333333333333" 
stroke-linejoin="round">
+  </path>
+</svg>
\ No newline at end of file
diff --git a/bigtop-manager-ui/src/components/common/svg-icon/index.vue 
b/bigtop-manager-ui/src/assets/images/svg/download.svg
similarity index 54%
copy from bigtop-manager-ui/src/components/common/svg-icon/index.vue
copy to bigtop-manager-ui/src/assets/images/svg/download.svg
index c18cc3f3..eff45651 100644
--- a/bigtop-manager-ui/src/components/common/svg-icon/index.vue
+++ b/bigtop-manager-ui/src/assets/images/svg/download.svg
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one
   ~ or more contributor license agreements.  See the NOTICE file
@@ -16,36 +17,15 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
 -->
-
-<script setup lang="ts">
-  import { computed } from 'vue'
-
-  interface SvgIconProps {
-    prefix?: string
-    name: string
-    color?: string
-  }
-
-  const props = withDefaults(defineProps<SvgIconProps>(), {
-    prefix: 'icon',
-    color: '#000'
-  })
-
-  const symbolId = computed(() => `#${props.prefix}-${props.name}`)
-</script>
-
-<template>
-  <svg class="svg-icon" aria-hidden="true">
-    <use :xlink:href="symbolId" :fill="color" />
-  </svg>
-</template>
-
-<style lang="scss" scoped>
-  .svg-icon {
-    height: 1.2em;
-    width: 1.2em;
-    margin: 0 6px;
-    vertical-align: -0.25em;
-    overflow: hidden;
-  }
-</style>
+<svg xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; width="16" height="16"
+  viewBox="0 0 16 16" fill="none">
+  <path stroke="rgba(22, 119, 255, 1)" stroke-width="1.3333333333333333" 
stroke-linejoin="round" stroke-linecap="round"
+    d="M2 8.00277L2 14L14 14L14 8">
+  </path>
+  <path stroke="rgba(22, 119, 255, 1)" stroke-width="1.3333333333333333" 
stroke-linejoin="round" stroke-linecap="round"
+    d="M11 7.66675L8 10.6667L5 7.66675">
+  </path>
+  <path stroke="rgba(22, 119, 255, 1)" stroke-width="1.3333333333333333" 
stroke-linejoin="round" stroke-linecap="round"
+    d="M7.99707 2L7.99707 10.6667">
+  </path>
+</svg>
\ No newline at end of file
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 75b3f581..b2dd417f 100644
--- a/bigtop-manager-ui/src/components/common/button-group/index.vue
+++ b/bigtop-manager-ui/src/components/common/button-group/index.vue
@@ -68,7 +68,7 @@
           </span>
         </a-button>
         <template #overlay>
-          <a-menu @click="item.dropdownMenuClickEvent">
+          <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>
diff --git a/bigtop-manager-ui/src/components/common/filter-form/index.vue 
b/bigtop-manager-ui/src/components/common/filter-form/index.vue
index 7562ea37..05c56bad 100644
--- a/bigtop-manager-ui/src/components/common/filter-form/index.vue
+++ b/bigtop-manager-ui/src/components/common/filter-form/index.vue
@@ -18,8 +18,10 @@
 -->
 
 <script setup lang="ts">
+  import { MenuProps } from 'ant-design-vue'
+  import { ref, toRefs, computed, shallowRef, toRaw } from 'vue'
+  import { isEqual, cloneDeep } from 'lodash'
   import type { FilterFormItem } from './types'
-  import { ref, toRefs } from 'vue'
 
   interface FilterFormPops {
     filterItems: FilterFormItem[]
@@ -29,7 +31,17 @@
   const emits = defineEmits(['filter'])
   const { filterItems } = toRefs(props)
 
-  const formatFilterFormItems = ref(
+  const tempFilterParams = shallowRef({})
+  const filterParams = ref(
+    filterItems.value.reduce(
+      (pre, value) => {
+        return Object.assign(pre, { [`${value.key}`]: undefined })
+      },
+      {} as Record<string, any>
+    )
+  )
+
+  const formatFilterFormItems = computed(() =>
     filterItems.value.map((v) => {
       const formatData = {
         ...v,
@@ -43,57 +55,83 @@
     })
   )
 
-  const filterParams = ref(
-    filterItems.value.map((v) => ({
-      [`${v.key}`]: ''
-    }))
-  )
-
-  const checkSelected = ({ item, itemIdx }: any) => {
-    return filterParams.value[itemIdx][item.key] === ''
+  const openChange = (open: boolean) => {
+    if (open) {
+      tempFilterParams.value = cloneDeep(toRaw(filterParams.value))
+    } else {
+      !isEqual(tempFilterParams.value, filterParams.value) && 
confirmFilterParams()
+    }
   }
 
-  const resetFilter = ({ item, itemIdx }: any) => {
-    filterParams.value[itemIdx][item.key] = ''
-    confirmFilterParams()
+  const confirmFilterParams = () => {
+    emits('filter', filterParams.value)
   }
 
-  const onSelect = ({ item, key }: any, payload: any) => {
-    filterParams.value[payload.itemIdx][item.id] = key
-    confirmFilterParams()
+  const onSelect: MenuProps['onSelect'] = ({ item, key }) => {
+    filterParams.value[`${item.id}`] = key
   }
 
-  const confirmFilterParams = () => {
-    const filters = filterParams.value.reduce((pre, val) => {
-      Object.assign(pre, val)
-      return pre
-    }, {} as any)
-    emits('filter', filters)
+  const resetFilter = (item: any) => {
+    filterParams.value[item.key] = undefined
   }
 </script>
 
 <template>
   <div class="filter-form">
-    <template v-for="(item, itemIdx) in formatFilterFormItems" :key="item">
-      <a-dropdown>
+    <template v-for="item in formatFilterFormItems" :key="item">
+      <a-dropdown :trigger="['click']" @open-change="openChange">
         <span @click.prevent>
           <div class="filter-form-label">
-            <span :style="{ color: !checkSelected({ item, itemIdx }) ? 
'var(--color-primary)' : 'initial' }">
+            <span :style="{ color: filterParams[`${item.key}`] != undefined ? 
'var(--color-primary)' : 'initial' }">
               {{ item.label }}
             </span>
-            <svg-icon name="bottom" style="padding: 6px 4px; margin-left: 8px" 
/>
+            <svg-icon
+              :name="filterParams[`${item.key}`] === undefined ? 'bottom' : 
'bottom-activated'"
+              style="padding: 6px 4px; margin-left: 8px"
+            />
           </div>
         </span>
         <template #overlay>
           <template v-if="item.type === 'status'">
-            <a-menu :selectable="true" :items="item.options" 
@select="onSelect($event, { item, itemIdx })"> </a-menu>
+            <div>
+              <a-menu :selected-keys="[filterParams[item.key]]" 
:selectable="true" @select="onSelect">
+                <a-menu-item
+                  v-for="menuItem in item.options"
+                  :id="item.key"
+                  :key="menuItem.key"
+                  :title="menuItem.label"
+                  @click.stop
+                >
+                  <a-radio :checked="filterParams[item.key] === menuItem.key">
+                    <span>{{ menuItem.label }}</span>
+                  </a-radio>
+                </a-menu-item>
+                <a-space class="status-option">
+                  <a-button
+                    :disabled="filterParams[`${item.key}`] === undefined"
+                    size="small"
+                    type="link"
+                    @click.stop="resetFilter(item)"
+                  >
+                    {{ $t('common.reset') }}
+                  </a-button>
+                  <a-button size="small" type="primary" 
@click="confirmFilterParams">
+                    {{ $t('common.ok') }}
+                  </a-button>
+                </a-space>
+              </a-menu>
+            </div>
           </template>
           <template v-else-if="item.type === 'search'">
             <div class="search" @click.stop>
-              <a-input v-model:value="filterParams[itemIdx][item.key]" 
:placeholder="`搜索${item.label}`" @click.stop />
-              <a-space @click.stop>
-                <a-button size="small" @click="resetFilter({ item, itemIdx 
})">重置</a-button>
-                <a-button size="small" type="primary" 
@click="confirmFilterParams">搜索</a-button>
+              <a-input
+                v-model:value="filterParams[item.key]"
+                
:placeholder="`${$t('common.enter_error')}${item.label.toLowerCase()}`"
+                @click.stop
+              />
+              <a-space class="search-option">
+                <a-button size="small" @click.stop="resetFilter(item)">{{ 
$t('common.reset') }}</a-button>
+                <a-button size="small" type="primary" 
@click="confirmFilterParams">{{ $t('common.search') }}</a-button>
               </a-space>
             </div>
           </template>
@@ -113,6 +151,17 @@
     min-height: 32px;
   }
 
+  .status-option {
+    width: 100%;
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    border-top: 1px solid #f0f0f0;
+    padding: 8px 4px 4px 4px;
+    button {
+      width: 100%;
+    }
+  }
+
   .search {
     background-color: $color-bg-base;
     box-shadow: $box-shadow-drawer-up;
@@ -122,12 +171,21 @@
     flex-direction: column;
     gap: $space-sm;
     align-items: flex-end;
+    &-option {
+      width: 100%;
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      button {
+        width: 100%;
+      }
+    }
   }
 
   .filter-form {
     width: 100%;
     display: flex;
     gap: $space-sm;
+    margin-bottom: $space-md;
     &-label {
       display: inline-block;
       cursor: pointer;
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 d11d81bb..d6fd6274 100644
--- a/bigtop-manager-ui/src/components/common/header-card/index.vue
+++ b/bigtop-manager-ui/src/components/common/header-card/index.vue
@@ -41,8 +41,8 @@
     showStatus: true,
     avatar: '',
     title: '',
-    desc: '暂无描述',
-    status: 'success',
+    desc: '',
+    status: '',
     actionGroups: () => {
       return []
     }
diff --git a/bigtop-manager-ui/src/components/common/main-card/index.vue 
b/bigtop-manager-ui/src/components/common/main-card/index.vue
index f5efb7a7..8fa3a275 100644
--- a/bigtop-manager-ui/src/components/common/main-card/index.vue
+++ b/bigtop-manager-ui/src/components/common/main-card/index.vue
@@ -44,13 +44,12 @@
   <div class="main-card">
     <slot>
       <a-tabs :active-key="activeKey" :tab-bar-gutter="0" 
@change="handleChange">
-        <a-tab-pane v-for="tab in tabs" :key="tab.key" :tab="tab.title">
-          <slot name="tab-item"></slot>
-        </a-tab-pane>
+        <a-tab-pane v-for="tab in tabs" :key="tab.key" :tab="tab.title"> 
</a-tab-pane>
         <template #renderTabBar="{ DefaultTabBar, ...props }">
           <component :is="DefaultTabBar" v-bind="props" />
         </template>
       </a-tabs>
+      <slot name="tab-item"></slot>
     </slot>
   </div>
 </template>
diff --git a/bigtop-manager-ui/src/components/common/status-dot/index.vue 
b/bigtop-manager-ui/src/components/common/status-dot/index.vue
index 96c401a3..5fa4860c 100644
--- a/bigtop-manager-ui/src/components/common/status-dot/index.vue
+++ b/bigtop-manager-ui/src/components/common/status-dot/index.vue
@@ -18,21 +18,15 @@
 -->
 
 <script setup lang="ts">
+  import { StatusType, StatusColors } from '@/enums/state'
   import { computed } from 'vue'
 
   interface StatusDot {
     size?: number
-    color?: keyof typeof Colors
+    color?: StatusType
     content?: string
   }
 
-  enum Colors {
-    success = 'var(--color-success)',
-    error = 'var(--color-error)',
-    warning = 'var(--color-warning)',
-    default = 'var(--color-primary)'
-  }
-
   const props = withDefaults(defineProps<StatusDot>(), {
     color: 'default',
     size: 8,
@@ -43,7 +37,7 @@
     width: props.size + 'px',
     height: props.size + 'px',
     fontSize: fontSize.value + 'px',
-    backgroundColor: Colors[props.color]
+    backgroundColor: StatusColors[props.color]
   }))
 </script>
 
diff --git a/bigtop-manager-ui/src/components/common/svg-icon/index.vue 
b/bigtop-manager-ui/src/components/common/svg-icon/index.vue
index c18cc3f3..db973d4a 100644
--- a/bigtop-manager-ui/src/components/common/svg-icon/index.vue
+++ b/bigtop-manager-ui/src/components/common/svg-icon/index.vue
@@ -47,5 +47,6 @@
     margin: 0 6px;
     vertical-align: -0.25em;
     overflow: hidden;
+    flex-shrink: 0;
   }
 </style>
diff --git a/bigtop-manager-ui/src/components/job/custom-progress.vue 
b/bigtop-manager-ui/src/components/job/custom-progress.vue
new file mode 100644
index 00000000..897a8fc3
--- /dev/null
+++ b/bigtop-manager-ui/src/components/job/custom-progress.vue
@@ -0,0 +1,93 @@
+<!--
+  ~ 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 lang="ts" setup>
+  import { computed } from 'vue'
+  import { JobState } from '@/enums/state'
+  import {
+    MinusCircleFilled as Canceled,
+    CheckCircleFilled as Successful,
+    CloseCircleFilled as Failed
+  } from '@ant-design/icons-vue'
+
+  interface ProgressProps {
+    state: keyof typeof JobState
+    progressData: number
+  }
+
+  const props = withDefaults(defineProps<ProgressProps>(), {})
+
+  const progressConfig = computed(() => {
+    if (props.state === 'Pending') {
+      return {
+        progress: 0,
+        status: 'normal'
+      }
+    } else if (props.state === 'Successful') {
+      return {
+        progress: 100,
+        status: 'success',
+        icon: Successful
+      }
+    } else if (props.state === 'Processing') {
+      return {
+        progress: props.progressData,
+        status: 'active'
+      }
+    } else if (props.state === 'Canceled') {
+      return {
+        progress: 100,
+        status: 'normal',
+        icon: Canceled
+      }
+    } else if (props.state === 'Failed') {
+      return {
+        progress: 100,
+        status: 'exception',
+        icon: Failed
+      }
+    } else {
+      return {
+        progress: 0,
+        status: 'status'
+      }
+    }
+  })
+</script>
+
+<template>
+  <div>
+    <a-progress
+      :percent="progressConfig.progress"
+      :status="progressConfig.status"
+      :stroke-color="JobState[props.state]"
+    >
+      <template #format="percent">
+        <span v-if="['Processing', 'Pending'].includes(props.state)"> {{ 
percent }} % </span>
+        <component
+          :is="progressConfig.icon"
+          v-else-if="progressConfig.icon"
+          :style="{ color: JobState[props.state] }"
+        />
+      </template>
+    </a-progress>
+  </div>
+</template>
+
+<style lang="scss" scoped></style>
diff --git a/bigtop-manager-ui/src/components/job/index.vue 
b/bigtop-manager-ui/src/components/job/index.vue
new file mode 100644
index 00000000..f318fe34
--- /dev/null
+++ b/bigtop-manager-ui/src/components/job/index.vue
@@ -0,0 +1,237 @@
+<!--
+  ~ 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, ComputedRef, onActivated, onDeactivated, reactive, ref, 
shallowRef } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { useRoute } from 'vue-router'
+  import { TableColumnType, TableProps } from 'ant-design-vue'
+  import { getJobList, getStageList, getTaskList } from '@/api/job'
+  import useBaseTable from '@/composables/use-base-table'
+  import LogsView, { type LogViewProps } from '@/components/log-view/index.vue'
+  import CustomProgress from './custom-progress.vue'
+  import type { JobVO, StageVO, StateType, TaskListParams, TaskVO } from 
'@/api/job/types'
+
+  interface BreadcrumbItem {
+    id: string
+    name: ComputedRef<string> | string
+  }
+  const POLLING_INTERVAL = 3000
+  const { t } = useI18n()
+  const route = useRoute()
+  const pollingIntervalId = ref<any>(null)
+  const breadcrumbs = ref<BreadcrumbItem[]>([
+    {
+      name: computed(() => t('job.job_list')),
+      id: `clusterId-${route.params.id}`
+    }
+  ])
+  const status = shallowRef<Record<StateType, string>>({
+    Pending: 'installing',
+    Processing: 'processing',
+    Failed: 'failed',
+    Canceled: 'canceled',
+    Successful: 'success'
+  })
+  const apiMap = shallowRef([
+    {
+      key: 'clusterId',
+      api: getJobList
+    },
+    {
+      key: 'jobId',
+      api: getStageList
+    },
+    {
+      key: 'stageId',
+      api: getTaskList
+    }
+  ])
+  const logsViewState = reactive<LogViewProps>({
+    open: false
+  })
+  const breadcrumbLen = computed(() => breadcrumbs.value.length)
+  const currBreadcrumb = computed(() => breadcrumbs.value.at(-1))
+  const columns = computed((): TableColumnType[] => [
+    {
+      title: '#',
+      width: '48px',
+      key: 'index',
+      customRender: ({ index }) => `${index + 1}`
+    },
+    {
+      title: t('common.name'),
+      key: 'name',
+      dataIndex: 'name',
+      ellipsis: true
+    },
+    {
+      title: t('common.status'),
+      key: 'state',
+      dataIndex: 'state'
+    },
+    {
+      title: t('common.create_time'),
+      dataIndex: 'createTime',
+      ellipsis: true
+    },
+    {
+      title: t('common.update_time'),
+      dataIndex: 'updateTime',
+      ellipsis: true
+    }
+  ])
+  const apiParams = computed(
+    (): TaskListParams =>
+      breadcrumbs.value.reduce((pre, val, index) => {
+        return Object.assign(pre, {
+          [`${apiMap.value[index].key}`]: 
val.id.replace(`${apiMap.value[index].key}-`, '')
+        })
+      }, {} as TaskListParams)
+  )
+
+  const { dataSource, loading, filtersParams, paginationProps, onChange } = 
useBaseTable<JobVO | StageVO | TaskVO>({
+    columns: columns.value
+  })
+
+  const clickBreadcrumb = (breadcrumb: BreadcrumbItem) => {
+    if (breadcrumb.id !== currBreadcrumb.value?.id) {
+      const index = breadcrumbs.value.findIndex((v) => v.id === breadcrumb.id)
+      breadcrumbs.value.splice(index + 1, breadcrumbLen.value - index - 1)
+      stopPolling()
+      startPolling()
+    }
+  }
+
+  const updateBreadcrumbs = (data: BreadcrumbItem) => {
+    if (breadcrumbLen.value + 1 > apiMap.value.length) {
+      // task log open
+      logsViewState.open = true
+      logsViewState.subTitle = data.name as string
+      logsViewState.payLoad = {
+        ...apiParams.value,
+        taskId: parseInt(data.id)
+      }
+      return
+    }
+    const currId = `${apiMap.value[breadcrumbLen.value].key}-${data.id}`
+    const index = breadcrumbs.value.findIndex((v) => v.id === currId)
+    if (index === -1) {
+      breadcrumbs.value.push({
+        id: currId,
+        name: data.name as string
+      })
+      stopPolling()
+      startPolling()
+    }
+  }
+
+  const getListData = async (isFirstCall = false) => {
+    if (!paginationProps.value) {
+      loading.value = false
+      return
+    }
+    try {
+      if (isFirstCall) {
+        loading.value = true
+      }
+      const { api } = apiMap.value[breadcrumbs.value.length - 1]
+      const res = await api(apiParams.value, filtersParams.value)
+      dataSource.value = res.content
+      paginationProps.value.total = res.total
+    } catch (error) {
+      console.log('error :>> ', error)
+      stopPolling()
+    } finally {
+      loading.value = false
+    }
+  }
+
+  const tableChange: TableProps['onChange'] = (...args) => {
+    onChange(...args)
+    stopPolling()
+    startPolling()
+  }
+
+  const startPolling = () => {
+    getListData(true)
+    pollingIntervalId.value = setInterval(() => {
+      getListData()
+    }, POLLING_INTERVAL)
+  }
+
+  const stopPolling = () => {
+    if (pollingIntervalId.value) {
+      clearInterval(pollingIntervalId.value)
+      pollingIntervalId.value = null
+    }
+  }
+
+  onActivated(() => {
+    startPolling()
+  })
+
+  onDeactivated(() => {
+    stopPolling()
+  })
+</script>
+
+<template>
+  <div class="job">
+    <header>
+      <a-breadcrumb>
+        <a-breadcrumb-item
+          v-for="breadcrumb in breadcrumbs"
+          :key="breadcrumb.id"
+          class="header-title"
+          @click="clickBreadcrumb(breadcrumb)"
+        >
+          <a v-if="currBreadcrumb?.id != breadcrumb.id">{{ breadcrumb.name 
}}</a>
+          <span v-else>{{ breadcrumb.name }}</span>
+        </a-breadcrumb-item>
+      </a-breadcrumb>
+    </header>
+    <a-table
+      :loading="loading"
+      :data-source="dataSource"
+      :columns="columns"
+      :pagination="paginationProps"
+      @change="tableChange"
+    >
+      <template #bodyCell="{ record, text, column }">
+        <template v-if="column.key === 'name'">
+          <a-typography-link underline @click="updateBreadcrumbs(record)">
+            <span :title="record.name">{{ record.name }}</span>
+          </a-typography-link>
+        </template>
+        <template v-if="column.key === 'state'">
+          <custom-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>
+        </template>
+      </template>
+    </a-table>
+    <logs-view
+      v-model:open="logsViewState.open"
+      :pay-load="logsViewState.payLoad"
+      :sub-title="logsViewState.subTitle"
+    />
+  </div>
+</template>
+
+<style lang="scss" scoped></style>
diff --git a/bigtop-manager-ui/src/components/log-view/index.vue 
b/bigtop-manager-ui/src/components/log-view/index.vue
new file mode 100644
index 00000000..78e3b671
--- /dev/null
+++ b/bigtop-manager-ui/src/components/log-view/index.vue
@@ -0,0 +1,251 @@
+<!--
+  ~ 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, watch } from 'vue'
+  import { copyText, scrollToBottom } from '@/utils/tools'
+  import { message } from 'ant-design-vue'
+  import { useI18n } from 'vue-i18n'
+  import { getTaskLog } from '@/api/job'
+  import type { AxiosProgressEvent, Canceler } from 'axios'
+
+  type EventListener = (val: any) => void
+
+  interface Option {
+    icon: 'copy' | 'download'
+    text: string
+    on: Record<string, EventListener>
+  }
+
+  interface PayLoad {
+    clusterId: number
+    jobId: number
+    stageId: number
+    taskId: number
+  }
+
+  export interface LogViewProps {
+    open: boolean
+    payLoad?: PayLoad
+    subTitle?: string
+  }
+
+  const props = defineProps<LogViewProps>()
+  const emits = defineEmits(['update:open'])
+
+  const { t } = useI18n()
+  const title = ref('task_log')
+  const loading = ref(false)
+  const viewRef = ref<HTMLElement | null>(null)
+  const parsedLog = ref<string | null>(null)
+  const logMessage = ref<string>('')
+  const canceler = ref<Canceler>()
+  const options = computed((): Option[] => {
+    return [
+      {
+        icon: 'copy',
+        text: 'common.copy',
+        on: {
+          click: () => copyLogs()
+        }
+      },
+      {
+        icon: 'download',
+        text: 'common.download',
+        on: {
+          click: () => downloadLogs()
+        }
+      }
+    ]
+  })
+
+  watch(logMessage, (val) => {
+    parsedLog.value = val
+      .split('\n\n')
+      .map((s) => {
+        return s.substring(5)
+      })
+      .join('\n')
+  })
+
+  watch(
+    () => props.open,
+    (val) => {
+      val ? getLogMessage() : cancelSseConnect()
+      !val && (logMessage.value = '')
+    }
+  )
+
+  const getLogMessage = async () => {
+    if (!props.payLoad) {
+      return
+    }
+    try {
+      loading.value = true
+      const { cancel, promise } = getTaskLog({ ...props.payLoad }, 
getLogProgress)
+      canceler.value = cancel
+      promise.then(onLogComplete)
+    } catch (error) {
+      console.log('error :>> ', error)
+      loading.value = false
+    }
+  }
+
+  const onLogComplete = (res: string | undefined) => {
+    loading.value = res == undefined
+    cancelSseConnect()
+  }
+
+  const getLogProgress = ({ event }: AxiosProgressEvent) => {
+    logMessage.value = event.target.responseText
+    scrollToBottom(viewRef.value)
+  }
+
+  const cancelSseConnect = () => {
+    canceler.value && canceler.value()
+  }
+
+  const copyLogs = () => {
+    if (!parsedLog.value) {
+      return
+    }
+    copyText(parsedLog.value)
+      .then(() => {
+        message.success(`${t('common.copied')}`)
+      })
+      .catch((err: Error) => {
+        message.error(`${t('common.copy_fail')}`)
+        console.log('err :>> ', err)
+      })
+  }
+
+  const downloadLogs = () => {
+    const blob = new Blob([parsedLog.value as BlobPart], { type: 'text/plain' 
})
+    const url = URL.createObjectURL(blob)
+    const a = document.createElement('a')
+    a.href = url
+    a.download = 'log.txt'
+    document.body.appendChild(a)
+    a.click()
+    document.body.removeChild(a)
+    URL.revokeObjectURL(url)
+  }
+
+  const handleOk = () => {
+    emits('update:open', false)
+  }
+</script>
+
+<template>
+  <a-modal
+    width="60%"
+    style="min-width: 400px"
+    :mask="false"
+    :open="open"
+    :title="$t(`job.${title}`)"
+    @ok="handleOk"
+    @cancel="handleOk"
+  >
+    <template #closeIcon>
+      <svg-icon style="margin: 0" name="close" />
+    </template>
+    <template #footer>
+      <div class="log-load">
+        <div>
+          <span v-show="loading" id="log-loading">{{ $t('job.log_loading') 
}}</span>
+        </div>
+        <a-button key="submit" type="primary" @click="handleOk">
+          {{ $t('common.confirm') }}
+        </a-button>
+      </div>
+    </template>
+    <section>
+      <span class="sub-title">{{ props.subTitle || '' }}</span>
+      <a-space :size="16" class="options">
+        <div v-for="option in options" :key="option.icon" v-on="option.on">
+          <svg-icon :name="option.icon" />
+          <a-typography-link underline :content="$t(option.text)" />
+        </div>
+      </a-space>
+    </section>
+    <article ref="viewRef">
+      <pre>{{ parsedLog }}</pre>
+    </article>
+  </a-modal>
+</template>
+
+<style lang="scss" scoped>
+  .log-load {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  section {
+    display: flex;
+    align-content: flex-end;
+    justify-content: space-between;
+    margin-bottom: $space-sm;
+    color: rgba(0, 0, 0, 0.45);
+  }
+  article {
+    height: 500px;
+    overflow: auto;
+    pre {
+      font-size: 12px;
+      padding: 8px 16px;
+      line-height: 32px;
+      min-height: 500px;
+      overflow-y: hidden;
+      background-color: rgba(0, 0, 0, 0.88);
+      height: auto;
+      color: #fff;
+      margin: 0;
+    }
+  }
+
+  @keyframes loading-dots {
+    0% {
+      content: '';
+    }
+    16% {
+      content: '.';
+    }
+    32% {
+      content: '..';
+    }
+    48% {
+      content: '...';
+    }
+    80% {
+      content: '....';
+    }
+    96% {
+      content: '.....';
+    }
+    100% {
+      content: '......';
+    }
+  }
+
+  #log-loading::after {
+    content: '';
+    display: inline-block;
+    animation: loading-dots 1.5s infinite;
+  }
+</style>
diff --git a/bigtop-manager-ui/src/composables/use-base-table.ts 
b/bigtop-manager-ui/src/composables/use-base-table.ts
index 2d359b4b..5bb04cbf 100644
--- a/bigtop-manager-ui/src/composables/use-base-table.ts
+++ b/bigtop-manager-ui/src/composables/use-base-table.ts
@@ -18,14 +18,16 @@
  */
 
 import { ref, onUnmounted } from 'vue'
-import type { TablePaginationConfig, TableColumnType } from 'ant-design-vue'
 import { useI18n } from 'vue-i18n'
+import { processData } from '@/utils/tools'
+import type { PaginationProps, TableColumnType, TableProps } from 
'ant-design-vue'
+import type { FilterValue } from 'ant-design-vue/es/table/interface'
 
-type PaginationProps = TablePaginationConfig | false | undefined
+type PaginationType = PaginationProps | false | undefined
 export interface UseBaseTableProps<T = any> {
   columns: TableColumnType[]
   rows?: T[]
-  pagination?: PaginationProps
+  pagination?: PaginationType
 }
 const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
   const { columns, rows, pagination } = props
@@ -33,7 +35,7 @@ const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
   const loading = ref(false)
   const dataSource = ref<T[]>(rows || [])
   const columnsProp = ref<TableColumnType[]>(columns)
-  const paginationProps = ref<PaginationProps>({
+  const paginationProps = ref<PaginationType>({
     current: 1,
     pageSize: 10,
     total: dataSource.value.length,
@@ -42,19 +44,30 @@ const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
     pageSizeOptions: ['10', '20', '30', '40', '50'],
     showTotal: (total) => `${t('common.total', [total])}`
   })
+  const filtersParams = ref<Record<string, FilterValue | null | number | 
undefined>>({
+    pageNum: paginationProps.value ? paginationProps.value.current : undefined,
+    pageSize: paginationProps.value ? paginationProps.value.pageSize : 
undefined
+  })
 
-  // merge pagination config
+  // Merge pagination config
   if (pagination === undefined && paginationProps.value) {
-    paginationProps.value = Object.assign(paginationProps.value, pagination)
+    Object.assign(paginationProps.value, pagination)
   } else {
     paginationProps.value = false
   }
 
-  const onChange = (pagination: TablePaginationConfig) => {
+  const onChange: TableProps['onChange'] = (pagination, filters) => {
+    // Collect params of filters
+    Object.assign(filtersParams.value, processData(filters))
     if (!paginationProps.value) {
       return
     }
-    paginationProps.value = Object.assign(paginationProps.value, pagination)
+    Object.assign(paginationProps.value, pagination)
+    // Update value of params about pagination
+    Object.assign(filtersParams.value, {
+      pageNum: pagination.current,
+      pageSize: pagination.pageSize
+    })
   }
 
   const resetState = () => {
@@ -80,6 +93,7 @@ const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
     dataSource,
     loading,
     paginationProps,
+    filtersParams,
     onChange,
     resetState
   }
diff --git a/bigtop-manager-ui/src/enums/state.ts 
b/bigtop-manager-ui/src/enums/state.ts
index 003b044c..5f4cee82 100644
--- a/bigtop-manager-ui/src/enums/state.ts
+++ b/bigtop-manager-ui/src/enums/state.ts
@@ -17,26 +17,31 @@
  * under the License.
  */
 
-export type MaintainState = 'Uninstalled' | 'Installed' | 'Maintained' | 
'Started' | 'Stopped'
+export type StatusType = keyof typeof StatusColors
 
-export enum State {
-  Pending = '#1677ff',
-  Processing = '#1677fe',
-  Successful = '#52c41a',
-  Failed = '#ff4d4f',
-  Canceled = '#80868b'
+export enum StatusColors {
+  success = 'var(--color-success)',
+  error = 'var(--color-error)',
+  warning = 'var(--color-warning)',
+  default = 'var(--color-primary)'
 }
 
-export enum CommonState {
-  normal = '#52c41a',
-  abnormal = '#ff4d4f',
-  maintained = '#d9d9d9'
+export enum CommonStatus {
+  healthy = 'success',
+  unhealthy = 'error',
+  unknow = 'warning'
 }
 
-export enum CurrState {
-  Installed,
-  Started,
-  Maintained,
-  Uninstalled,
-  Stopped
+export enum CommonStatusTexts {
+  'healthy',
+  'unhealthy',
+  'unknow'
+}
+
+export enum JobState {
+  'Pending' = 'pending',
+  'Processing' = 'processing',
+  'Successful' = 'successful',
+  'Failed' = 'failed',
+  'Canceled' = 'canceled'
 }
diff --git a/bigtop-manager-ui/src/layouts/sider.vue 
b/bigtop-manager-ui/src/layouts/sider.vue
index 363f10d5..bdb988b0 100644
--- a/bigtop-manager-ui/src/layouts/sider.vue
+++ b/bigtop-manager-ui/src/layouts/sider.vue
@@ -23,6 +23,7 @@
   import { RouteExceptions } from '@/enums'
   import { useMenuStore } from '@/store/menu'
   import type { MenuItem } from '@/store/menu/types'
+  import type { ClusterStatusType } from '@/api/cluster/types'
 
   interface Props {
     siderMenuSelectedKey: string
@@ -34,13 +35,11 @@
     siderMenus: () => []
   })
 
-  type ClusterType = 1 | 2 | 3
-
   const { siderMenuSelectedKey, siderMenus } = toRefs(props)
   const router = useRouter()
   const menuStore = useMenuStore()
   const emits = defineEmits(['onSiderClick'])
-  const clusterStatus = ref<Record<ClusterType, string>>({
+  const clusterStatus = ref<Record<ClusterStatusType, string>>({
     1: 'success',
     2: 'error',
     3: 'warning'
@@ -83,7 +82,7 @@
               <div
                 style="height: 10px; margin-inline: 7px; display: flex; 
justify-content: center; align-items: flex-end"
               >
-                <status-dot :size="8" :color="clusterStatus[child.status as 
ClusterType] as any" />
+                <status-dot :size="8" :color="clusterStatus[child.status as 
ClusterStatusType] as any" />
               </div>
             </template>
             <div>
diff --git a/bigtop-manager-ui/src/locales/en_US/cluster.ts 
b/bigtop-manager-ui/src/locales/en_US/cluster.ts
index 879cfa11..a05ff116 100644
--- a/bigtop-manager-ui/src/locales/en_US/cluster.ts
+++ b/bigtop-manager-ui/src/locales/en_US/cluster.ts
@@ -17,23 +17,23 @@
  * under the License.
  */
 export default {
-  base_info: 'Basic information',
-  cluster_config: 'Cluster configuration',
+  base_info: 'Basic Information',
+  cluster_config: 'Cluster Configuration',
   cluster_management: 'Cluster',
   component_info: 'Stack',
   host_config: 'Hosts',
   create: 'Create',
   name: 'Name',
-  display_name: 'Display name',
+  display_name: 'Display Name',
   description: 'Description',
-  root_directory: 'Root directory',
-  user_group: 'User group',
-  config_source: 'Configuration source',
-  install_dependencies: 'Install dependencies',
-  add_host: 'Add host',
-  edit_host: 'Edit host',
-  view_log: 'View log',
+  root_directory: 'Root Directory',
+  user_group: 'User Group',
+  config_source: 'Configuration Source',
+  install_dependencies: 'Install Dependencies',
+  add_host: 'Add Host',
+  edit_host: 'Edit Host',
+  view_log: 'View Log',
   source: 'Source',
-  show_hosts_resolved: 'Hostnames after resolved',
+  show_hosts_resolved: 'Resolved Hostnames',
   cluster_unavailable_message: 'Sorry, you need to create cluster to use this 
feature.'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/common.ts 
b/bigtop-manager-ui/src/locales/en_US/common.ts
index 47650f45..86a78908 100644
--- a/bigtop-manager-ui/src/locales/en_US/common.ts
+++ b/bigtop-manager-ui/src/locales/en_US/common.ts
@@ -33,7 +33,7 @@ export default {
   rename: 'Rename',
   delete: 'Delete',
   remove: 'Remove',
-  bulk_remove: 'Bulk remove',
+  bulk_remove: 'Bulk Remove',
   disable: 'Disable',
   enable: 'Enable',
   submit: 'Submit',
@@ -45,28 +45,28 @@ export default {
   installing: 'Installing...',
   finish: 'Finish',
   more: 'More',
-  view_all: 'View all',
+  view_all: 'View All',
   select_tips: 'Please select',
   name: 'Name',
   version: 'Version',
   desc: 'Description',
   os: 'OS',
   arch: 'Arch',
-  base_url: 'Base URL',
+  base_url: 'Address',
+  cluster: 'Cluster',
   host: 'Host',
   service: 'Service',
   stack: 'Stack',
   stage: 'Stage',
   search: 'Search',
   reset: 'Reset',
-  restart: 'Restart {0}',
   required: 'Required',
-  not_required: 'Not required',
+  not_required: 'Not Required',
   progress: 'Progress',
   status_change_success: 'Status changed successfully',
-  test_success: 'Test successful !',
-  created: 'Created successfully !',
-  update_success: 'Update successfully !',
+  test_success: 'Test successful!',
+  created: 'Created!',
+  update_success: 'Updated!',
   update_fail: 'Update fail',
   error_unknown: 'Unknown error',
   error_network: 'Network error',
@@ -77,9 +77,10 @@ export default {
   update_time: 'Update Time',
   notification: 'Notification',
   copy: 'Copy',
-  copy_success: 'Copy successfully !',
+  copied: 'Copied!',
+  download: 'Download',
   copy_fail: 'Copy failed!',
-  delete_success: 'Delete successfully !',
+  delete_success: 'Deleted!',
   delete_fail: 'Delete failed!',
   delete_confirm_title: 'Delete Confirm',
   delete_confirm_content: 'Delete will not be recoverable, are you sure you 
want to delete {0} ?',
@@ -96,5 +97,14 @@ export default {
   file_size_error: 'File size cannot exceed 10KB!',
   upload_success: 'File uploaded successfully!',
   upload_failed: 'File upload failed!',
-  password_not_match: 'Passwords do not match.'
+  password_not_match: 'Passwords do not match.',
+  overview: 'Overview',
+  user: 'User',
+  job: 'Job',
+  start: 'Start {0}',
+  restart: 'Restart {0}',
+  stop: 'Stop {0}',
+  add: 'Add {0}',
+  more_operations: 'More Operations',
+  ok: 'Ok'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/host.ts 
b/bigtop-manager-ui/src/locales/en_US/host.ts
index 9eb41395..22ae699b 100644
--- a/bigtop-manager-ui/src/locales/en_US/host.ts
+++ b/bigtop-manager-ui/src/locales/en_US/host.ts
@@ -33,10 +33,14 @@ export default {
   auth_method: 'Authentication Method',
   password_auth: 'Password',
   key_auth: 'Key',
+  ip_address: 'IP Address',
+  component_count: 'Component Count',
   no_auth: 'No Authentication',
   agent_path: 'Agent Path',
   ssh_port: 'SSH Port',
   grpc_port: 'GRPC Port',
   description: 'Description',
-  key_password_not_match: 'Key Passwords do not match.'
+  key_password_not_match: 'Key Passwords do not match.',
+  default_agent_path: 'Agent application installed path, default to /opt',
+  default_grpc_port: 'Agent application grpc port, default to 8835'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/index.ts 
b/bigtop-manager-ui/src/locales/en_US/index.ts
index 5f6166be..bc44b73b 100644
--- a/bigtop-manager-ui/src/locales/en_US/index.ts
+++ b/bigtop-manager-ui/src/locales/en_US/index.ts
@@ -26,6 +26,8 @@ import aiAssistant from '@/locales/en_US/ai-assistant.ts'
 import cluster from '@/locales/en_US/cluster.ts'
 import host from '@/locales/en_US/host.ts'
 import job from '@/locales/en_US/job.ts'
+import overview from '@/locales/en_US/overview'
+import service from '@/locales/en_US/service'
 
 export default {
   common,
@@ -36,5 +38,7 @@ export default {
   aiAssistant,
   cluster,
   host,
-  job
+  job,
+  overview,
+  service
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/job.ts 
b/bigtop-manager-ui/src/locales/en_US/job.ts
index 95caf7b8..3d015fda 100644
--- a/bigtop-manager-ui/src/locales/en_US/job.ts
+++ b/bigtop-manager-ui/src/locales/en_US/job.ts
@@ -18,5 +18,7 @@
  */
 
 export default {
-  job_list: 'Job List'
+  job_list: 'Job List',
+  task_log: 'Task Log',
+  log_loading: 'Loading logs'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/llm-config.ts 
b/bigtop-manager-ui/src/locales/en_US/llm-config.ts
index c23e35f1..f128d116 100644
--- a/bigtop-manager-ui/src/locales/en_US/llm-config.ts
+++ b/bigtop-manager-ui/src/locales/en_US/llm-config.ts
@@ -17,8 +17,8 @@
  * under the License.
  */
 export default {
-  add_authorization: 'Add authorization',
-  edit_authorization: 'Edit authorization',
+  add_authorization: 'Add Authorization',
+  edit_authorization: 'Edit Authorization',
   test: 'Test',
   unavailable: 'Unavailable',
   active: 'Active',
diff --git a/bigtop-manager-ui/src/locales/zh_CN/job.ts 
b/bigtop-manager-ui/src/locales/en_US/overview.ts
similarity index 62%
copy from bigtop-manager-ui/src/locales/zh_CN/job.ts
copy to bigtop-manager-ui/src/locales/en_US/overview.ts
index 06cd7807..fde5bedc 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/job.ts
+++ b/bigtop-manager-ui/src/locales/en_US/overview.ts
@@ -16,7 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 export default {
-  job_list: '作业列表'
+  basic_info: 'Basic Information',
+  service_info: 'Service Information',
+  detail: 'Details',
+  cluster_status: 'Status',
+  cluster_name: 'Name',
+  cluster_desc: 'Description',
+  host_count: 'Host Count',
+  service_count: 'Service Count',
+  memory: 'Memory',
+  chart: 'Chart',
+  core_count: 'Processor Count',
+  disk_size: 'Disk Size',
+  creator: 'Creator',
+  memory_usage: 'Memory Usage',
+  cpu_usage: 'Cpu Usage',
+  unit_host: 'Hosts',
+  unit_service: 'Services',
+  unit_processor: 'Processors'
 }
diff --git a/bigtop-manager-ui/src/api/sse/types.ts 
b/bigtop-manager-ui/src/locales/en_US/service.ts
similarity index 83%
rename from bigtop-manager-ui/src/api/sse/types.ts
rename to bigtop-manager-ui/src/locales/en_US/service.ts
index 8e169f04..26d0b328 100644
--- a/bigtop-manager-ui/src/api/sse/types.ts
+++ b/bigtop-manager-ui/src/locales/en_US/service.ts
@@ -16,12 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-export interface LogsRes {
-  promise: Promise<any>
-  cancel: () => void
-}
-export interface ChatMessagesRes {
-  promise: Promise<any>
-  cancel: () => void
+export default {
+  name: 'Service Name',
+  required_restart: 'Restart'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/user.ts 
b/bigtop-manager-ui/src/locales/en_US/user.ts
index dab2f04a..29f36bd5 100644
--- a/bigtop-manager-ui/src/locales/en_US/user.ts
+++ b/bigtop-manager-ui/src/locales/en_US/user.ts
@@ -22,6 +22,7 @@ export default {
   settings: 'Settings',
   logout: 'Log Out',
   username: 'Username',
+  user_group: 'User Group',
   nickname: 'Nickname',
   user_list: 'User List',
   set_nickname_valid: 'Nickname should not be empty'
diff --git a/bigtop-manager-ui/src/locales/zh_CN/common.ts 
b/bigtop-manager-ui/src/locales/zh_CN/common.ts
index 30123e19..3005772f 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/common.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/common.ts
@@ -53,13 +53,12 @@ export default {
   os: '系统',
   arch: '架构',
   base_url: '地址',
+  cluster: '集群',
   host: '主机',
   service: '服务',
   stack: '组件栈',
   stage: '阶段',
   search: '搜索',
-  reset: '重置',
-  restart: '重启{0}',
   required: '需重启',
   not_required: '无需重启',
   progress: '进度',
@@ -77,7 +76,8 @@ export default {
   update_time: '更新时间',
   notification: '通知',
   copy: '复制',
-  copy_success: '复制成功',
+  copied: '已复制',
+  download: '下载',
   copy_fail: '复制失败',
   delete_success: '删除成功',
   delete_fail: '删除失败',
@@ -96,5 +96,15 @@ export default {
   file_size_error: '文件大小不能超过 10KB!',
   upload_success: '文件上传成功!',
   upload_failed: '文件上传失败!',
-  password_not_match: '密码不一致。'
+  password_not_match: '密码不一致。',
+  overview: '概览',
+  user: '用户',
+  job: '作业',
+  start: '启动{0}',
+  restart: '重启{0}',
+  stop: '停止{0}',
+  add: '添加{0}',
+  reset: '重置',
+  more_operations: '其他操作',
+  ok: '确认'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/host.ts 
b/bigtop-manager-ui/src/locales/zh_CN/host.ts
index 30692cfa..51b4d662 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/host.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/host.ts
@@ -33,10 +33,14 @@ export default {
   auth_method: '认证方式',
   password_auth: '密码',
   key_auth: '密钥',
+  ip_address: 'IP 地址',
+  component_count: '组件数',
   no_auth: '无认证',
   agent_path: 'Agent路径',
   ssh_port: 'SSH端口',
-  grpc_port: 'gRPC端口',
+  grpc_port: 'GRPC端口',
   description: '备注',
-  key_password_not_match: '密钥口令不一致。'
+  key_password_not_match: '密钥口令不一致。',
+  default_agent_path: '代理应用程序安装路径,默认为/opt',
+  default_grpc_port: '代理应用程序gRPC端口,默认为8835'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/index.ts 
b/bigtop-manager-ui/src/locales/zh_CN/index.ts
index cbb01b6e..5aa15877 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/index.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/index.ts
@@ -26,6 +26,8 @@ import aiAssistant from '@/locales/zh_CN/ai-assistant.ts'
 import cluster from '@/locales/zh_CN/cluster.ts'
 import host from '@/locales/zh_CN/host.ts'
 import job from '@/locales/zh_CN/job.ts'
+import overview from '@/locales/zh_CN/overview.ts'
+import service from '@/locales/zh_CN/service.ts'
 
 export default {
   common,
@@ -36,5 +38,7 @@ export default {
   aiAssistant,
   cluster,
   host,
-  job
+  job,
+  overview,
+  service
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/job.ts 
b/bigtop-manager-ui/src/locales/zh_CN/job.ts
index 06cd7807..a76d0677 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/job.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/job.ts
@@ -18,5 +18,7 @@
  */
 
 export default {
-  job_list: '作业列表'
+  job_list: '作业列表',
+  task_log: '任务日志',
+  log_loading: '获取日志中'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/job.ts 
b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
similarity index 62%
copy from bigtop-manager-ui/src/locales/zh_CN/job.ts
copy to bigtop-manager-ui/src/locales/zh_CN/overview.ts
index 06cd7807..ced7f376 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/job.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
@@ -16,7 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 export default {
-  job_list: '作业列表'
+  basic_info: '基本信息',
+  service_info: '服务信息',
+  detail: '详情',
+  cluster_status: '集群状态',
+  cluster_name: '集群名',
+  cluster_desc: '集群备注',
+  host_count: '主机数',
+  service_count: '服务数',
+  memory: '内存',
+  chart: '图表',
+  core_count: '核心数',
+  disk_size: '磁盘大小',
+  creator: '创建人',
+  memory_usage: '内存使用率',
+  cpu_usage: 'CPU使用率',
+  unit_host: '个主机',
+  unit_service: '个服务',
+  unit_processor: '个核心'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/job.ts 
b/bigtop-manager-ui/src/locales/zh_CN/service.ts
similarity index 93%
copy from bigtop-manager-ui/src/locales/zh_CN/job.ts
copy to bigtop-manager-ui/src/locales/zh_CN/service.ts
index 06cd7807..e2f6ad20 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/job.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/service.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 export default {
-  job_list: '作业列表'
+  name: '服务名',
+  required_restart: '需要重启'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/user.ts 
b/bigtop-manager-ui/src/locales/zh_CN/user.ts
index 808f7971..1fe47abd 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/user.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/user.ts
@@ -22,6 +22,7 @@ export default {
   settings: '个人设置',
   logout: '退出登录',
   username: '用户名',
+  user_group: '用户组',
   nickname: '昵称',
   user_list: '用户列表',
   set_nickname_valid: '昵称不能为空'
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
index 847ef120..8bb2b193 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
@@ -18,10 +18,11 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onMounted, ref, shallowRef, toRefs } from 'vue'
-  import type { JobVO, StateType } from '@/api/job/types'
+  import { computed, onMounted, reactive, ref, shallowRef, toRefs } from 'vue'
   import { getJobDetails, retryJob } from '@/api/job'
   import { CommandVO } from '@/api/command/types'
+  import LogsView, { type LogViewProps } from '@/components/log-view/index.vue'
+  import type { JobVO, StageVO, StateType, TaskVO } from '@/api/job/types'
 
   const props = defineProps<{ stepData: CommandVO }>()
   const emits = defineEmits(['updateData'])
@@ -29,6 +30,9 @@
   const activeKey = ref<number[]>([])
   const jobDetail = ref<JobVO>({})
   const spinning = ref(false)
+  const logsViewState = reactive<LogViewProps>({
+    open: false
+  })
   const status = shallowRef<Record<StateType, string>>({
     Pending: 'installing',
     Processing: 'processing',
@@ -92,6 +96,23 @@
     }, interval)
   }
 
+  const viewLogs = (stage: StageVO, task: TaskVO) => {
+    const { id: jobId } = stepData.value
+    const { id: stageId } = stage
+    const { id: taskId } = task
+    if (jobId === undefined || stageId === undefined || taskId === undefined) {
+      return
+    }
+    logsViewState.payLoad = {
+      clusterId: 0,
+      jobId,
+      stageId,
+      taskId
+    }
+    logsViewState.open = true
+    logsViewState.subTitle = task.name
+  }
+
   onMounted(() => {
     pollJobDetails(getJobInstanceDetails)
   })
@@ -118,13 +139,22 @@
             <span>{{ task.name }}</span>
             <a-space :size="16">
               <svg-icon :name="task.state && status[task.state]"></svg-icon>
-              <a-button v-if="task.state && !['Canceled', 
'Pending'].includes(task.state)" type="link">
+              <a-button
+                v-if="task.state && !['Canceled', 
'Pending'].includes(task.state)"
+                type="link"
+                @click="viewLogs(stage, task)"
+              >
                 {{ $t('cluster.view_log') }}
               </a-button>
             </a-space>
           </div>
         </a-collapse-panel>
       </a-collapse>
+      <logs-view
+        v-model:open="logsViewState.open"
+        :pay-load="logsViewState.payLoad"
+        :sub-title="logsViewState.subTitle"
+      />
     </div>
   </a-spin>
 </template>
@@ -153,8 +183,8 @@
     }
   }
   .stage-item {
+    margin-right: 68px;
     @include flexbox($justify: space-between, $align: center);
-    padding-right: 65px;
   }
   .task-item {
     height: 45px;
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/cluster-base.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/cluster-base.vue
index 82a9d1e2..2db924d8 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/cluster-base.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/cluster-base.vue
@@ -18,11 +18,11 @@
 -->
 
 <script setup lang="ts">
-  import type { FormItemState } from '@/components/common/auto-form/types'
   import { computed, ref, shallowRef, watch } from 'vue'
   import { useI18n } from 'vue-i18n'
-  import type { ClusterCommandReq } from '@/api/command/types'
   import { pick } from '@/utils/tools'
+  import type { ClusterCommandReq } from '@/api/command/types'
+  import type { FormItemState } from '@/components/common/auto-form/types'
 
   type ClusterCommandReqKey = keyof ClusterCommandReq
 
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/component-info.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/component-info.vue
index 0f8fe394..3e6cd684 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/component-info.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/component-info.vue
@@ -19,13 +19,13 @@
 
 <script setup lang="ts">
   import { computed, ref, shallowRef, watchEffect } from 'vue'
-  import type { TableColumnType } from 'ant-design-vue'
-  import useBaseTable from '@/composables/use-base-table'
-  import SetSource from './set-source.vue'
   import { useStackStore } from '@/store/stack'
   import { storeToRefs } from 'pinia'
   import { ServiceVO } from '@/api/service/types'
   import { useI18n } from 'vue-i18n'
+  import useBaseTable from '@/composables/use-base-table'
+  import SetSource from './set-source.vue'
+  import type { TableColumnType } from 'ant-design-vue'
 
   const { t } = useI18n()
   const stackStore = useStackStore()
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/mock.ts 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/mock.ts
deleted file mode 100644
index 4ccb391a..00000000
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/mock.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * 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
- *
- *    https://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.
- */
-
-export function generateTestData(count = 100) {
-  const names = ['componentA', 'componentB', 'componentC', 'componentD', 
'componentE']
-  const versions = ['1.0.0', '2.3.1', '3.2.4', '4.0.0', '5.1.2']
-  const compStacks = [
-    'React, Redux, Webpack',
-    'Vue, Vuex, Vite',
-    'Angular, RxJS, NgRx',
-    'Node.js, Express, MongoDB',
-    'Svelte, Rollup, Firebase'
-  ]
-  const descriptions = [
-    '这是一个用于状态管理的组件',
-    '这是一个用于数据展示的组件',
-    '这是一个用于实时数据处理的组件',
-    '这是一个后端服务组件',
-    '这是一个前端开发框架'
-  ]
-
-  const testData = Array.from({ length: count }, (_, i) => ({
-    key: (i + 1).toString(),
-    name: names[Math.floor(Math.random() * names.length)],
-    version: versions[Math.floor(Math.random() * versions.length)],
-    compStack: compStacks[Math.floor(Math.random() * compStacks.length)],
-    descrip: descriptions[Math.floor(Math.random() * descriptions.length)]
-  }))
-
-  return testData
-}
-
-export function generateTableHostData(numRows = 100) {
-  const hostnames = ['Server-A', 'Server-B', 'Server-C', 'Server-D', 
'Server-E']
-  const nodeNames = ['host-A', 'host-B', 'host-C', 'host-D']
-  const ipAddresses = ['192.168.1.1', '192.168.1.2', '192.168.1.3', 
'192.168.1.4', '192.168.1.5']
-  const systems = 'Rocky Linux 8.8'
-  const architecture = 'x86_64'
-  const remarks = ['Main server', 'Backup server', 'Database server', 'Web 
server', 'Test server']
-  const statuses = ['installing', 'success', 'error', 'unknow']
-
-  const tableData = []
-
-  for (let i = 0; i < numRows; i++) {
-    tableData.push({
-      key: i + 1,
-      name: hostnames[Math.floor(Math.random() * hostnames.length)],
-      system: `${systems}-${i}`,
-      architecture: `${architecture}-${i}`,
-      componentCount: `${i} components`,
-      nodeName: nodeNames[Math.floor(Math.random() * nodeNames.length)],
-      address: ipAddresses[Math.floor(Math.random() * ipAddresses.length)],
-      remark: remarks[Math.floor(Math.random() * remarks.length)],
-      status: statuses[Math.floor(Math.random() * statuses.length)]
-    })
-  }
-
-  return tableData
-}
-
-export function getCheckWorkflows(stageCount: number, maxTasksPerStage: 
number, maxStatus: number): Array<any> {
-  const data = []
-
-  for (let i = 0; i < stageCount; i++) {
-    const taskCount = Math.floor(Math.random() * maxTasksPerStage) + 1
-    const tasks = []
-
-    for (let j = 0; j < taskCount; j++) {
-      tasks.push({
-        id: j,
-        name: `task${j + 1}`,
-        status: Math.floor(Math.random() * maxStatus) + 1
-      })
-    }
-
-    data.push({
-      id: i,
-      name: `stage${i + 1}`,
-      status: Math.floor(Math.random() * maxStatus) + 1,
-      tasks
-    })
-  }
-
-  return data
-}
-
-export enum StatusColors {
-  success = 'success',
-  error = 'error',
-  unknow = 'warning'
-}
-
-export enum StatusTexts {
-  success = 'healthy',
-  error = 'unhealthy',
-  unknow = 'unknown'
-}
-
-export type ServiceStatus = keyof typeof StatusColors
-
-export interface ServiceItem {
-  key: string | number
-  serviceName: string
-  version: string
-  restart: boolean
-  status: ServiceStatus
-}
-
-export function getServices(): ServiceItem[] {
-  const statusList = ['success', 'error', 'unknow']
-  const serviceNames = [
-    'GraFana',
-    'Flink',
-    'Kafka',
-    'ZooKeeper',
-    'Hadoop',
-    'Hbase',
-    'Hive',
-    'MySQL',
-    'Spark',
-    'Solr',
-    'Seatunnel',
-    'Tez',
-    'Prometheus'
-  ]
-  return Array.from({ length: serviceNames.length }, (_, i) => ({
-    key: i,
-    serviceName: serviceNames[i],
-    version: `${serviceNames[i].toLowerCase()}1.0.${i}`,
-    restart: Math.floor(Math.random() * 2) == 0,
-    status: statusList[Math.floor(Math.random() * statusList.length)] as 
ServiceStatus
-  }))
-}
-
-export interface UserListItem {
-  serviceName: string
-  userName: string
-  userGroup: string
-  descrip: string
-}
-
-export function getUserList(count: number = 20): UserListItem[] {
-  return Array.from({ length: count }, (_, i) => ({
-    key: i,
-    serviceName: `serviceName-${i}`,
-    userName: `user-${i}`,
-    userGroup: `userGroup-${i}`,
-    descrip: 'descrip-descrip-descrip'
-  }))
-}
-
-type JobStatus = 'success' | 'exception' | 'normal' | 'active'
-export interface JobListItem {
-  name: string
-  status: JobStatus
-  progress: number
-  createTime: string
-  updateTime: string
-}
-
-export function getJobList(count: number = 20): JobListItem[] {
-  const status = ['success', 'exception', 'normal', 'active']
-  return Array.from({ length: count }, (_, i) => ({
-    key: i,
-    name: `name-${i}`,
-    progress: Math.floor(Math.random() * 100),
-    status: status[Math.floor(Math.random() * status.length)] as JobStatus,
-    createTime: '2024-09-19 11:11:11',
-    updateTime: '2024-09-19 11:11:11'
-  }))
-}
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
index 3fbee40d..a9428be5 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
@@ -29,14 +29,7 @@
   import ComponentInfo from './components/component-info.vue'
   import HostConfig from './components/host-config.vue'
   import CheckWorkflow from './components/check-workflow.vue'
-  import {
-    ClusterCommandReq,
-    Command,
-    CommandLevel,
-    HostReq,
-    type CommandRequest,
-    type CommandVO
-  } from '@/api/command/types'
+  import { ClusterCommandReq, HostReq, type CommandRequest, type CommandVO } 
from '@/api/command/types'
 
   const { t } = useI18n()
   const menuStore = useMenuStore()
@@ -45,8 +38,8 @@
   const installing = ref(false)
   const stepData = ref<[Partial<ClusterCommandReq>, any, HostReq[], 
CommandVO]>([{}, {}, [], {}])
   const commandRequest = ref<CommandRequest>({
-    command: Command.Add,
-    commandLevel: CommandLevel.Cluster
+    command: 'Add',
+    commandLevel: 'cluster'
   })
   const installStatus = shallowRef<InstalledStatusVO[]>([])
   const components = shallowRef<any[]>([ClusterBase, ComponentInfo, 
HostConfig, CheckWorkflow])
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 f39243de..4637b96b 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -18,95 +18,98 @@
 -->
 
 <script setup lang="ts">
-  import { TableColumnType } from 'ant-design-vue'
-  import { computed, reactive, ref } from 'vue'
-  import { generateTableHostData } from './components/mock'
+  import { TableColumnType, TableProps } from 'ant-design-vue'
+  import { computed, onActivated, reactive, ref, useAttrs } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { getHosts } from '@/api/hosts'
   import useBaseTable from '@/composables/use-base-table'
   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/hosts/types'
+  import type { ClusterVO } from '@/api/cluster/types'
 
   type Key = string | number
   interface TableState {
     selectedRowKeys: Key[]
     searchText: string
-    searchedColumn: string
+    searchedColumn: keyof HostVO
   }
 
+  const { t } = useI18n()
+  const attrs = useAttrs() as ClusterVO
   const searchInputRef = ref()
-  const data = ref<any[]>(generateTableHostData(50))
+  const hostStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOW'])
   const state = reactive<TableState>({
     searchText: '',
     searchedColumn: '',
     selectedRowKeys: []
   })
 
-  const columns = computed((): TableColumnType[] => [
+  const columns = computed((): TableColumnType<HostVO>[] => [
     {
-      title: '节点',
-      dataIndex: 'nodeName',
-      key: 'nodeName',
+      title: t('host.hostname'),
+      dataIndex: 'hostname',
+      key: 'hostname',
       ellipsis: true,
       customFilterDropdown: true,
-      onFilter: (value, record) => isContain(record.nodeName, value as string),
       onFilterDropdownOpenChange: (visible) => 
onFilterDropdownOpenChange(visible)
     },
     {
-      title: 'IP地址',
-      dataIndex: 'address',
-      key: 'address',
+      title: t('host.ip_address'),
+      dataIndex: 'ipv4',
+      key: 'ipv4',
       ellipsis: true,
       customFilterDropdown: true,
-      onFilter: (value, record) => isContain(record.address, value as string),
       onFilterDropdownOpenChange: (visible) => 
onFilterDropdownOpenChange(visible)
     },
     {
-      title: '系统',
-      dataIndex: 'system',
+      title: t('common.os'),
+      dataIndex: 'os',
       ellipsis: true
     },
     {
-      title: '架构',
-      dataIndex: 'architecture',
+      title: t('common.arch'),
+      dataIndex: 'arch',
       ellipsis: true
     },
     {
-      title: '组件数',
-      dataIndex: 'componentCount',
+      title: t('host.component_count'),
+      dataIndex: 'componentNum',
       ellipsis: true
     },
     {
-      title: '状态',
+      title: t('common.status'),
       dataIndex: 'status',
       key: 'status',
       width: '160px',
       ellipsis: true,
+      filterMultiple: false,
       filters: [
         {
-          text: '正常',
-          value: 'success'
+          text: t('common.success'),
+          value: 1
         },
         {
-          text: '异常',
-          value: 'error'
+          text: t('common.failed'),
+          value: 2
         },
         {
-          text: '未知',
-          value: 'unknow'
+          text: t('common.unknow'),
+          value: 3
         }
-      ],
-      onFilter: (value, record) => record.status.indexOf(value) === 0
+      ]
     },
     {
-      title: '操作',
+      title: t('common.action'),
       key: 'operation',
       width: '160px',
       fixed: 'right'
     }
   ])
 
-  const { loading, paginationProps, onChange } = useBaseTable({
+  const { loading, dataSource, filtersParams, paginationProps, onChange } = 
useBaseTable<HostVO>({
     columns: columns.value,
-    rows: data.value
+    rows: []
   })
 
   const operations = computed((): GroupItem[] => [
@@ -121,10 +124,6 @@
     }
   ])
 
-  const isContain = (source: string, target: string) => {
-    return source.toString().toLowerCase().includes(target.toLowerCase())
-  }
-
   const onFilterDropdownOpenChange = (visible: boolean) => {
     if (visible) {
       setTimeout(() => {
@@ -140,7 +139,7 @@
   const handleSearch = (selectedKeys: Key[], confirm: (param?: 
FilterConfirmProps) => void, dataIndex: string) => {
     confirm()
     state.searchText = selectedKeys[0] as string
-    state.searchedColumn = dataIndex as string
+    state.searchedColumn = dataIndex
   }
 
   const handleReset = (clearFilters: (param?: FilterResetProps) => void) => {
@@ -159,12 +158,42 @@
       console.log('row :>> ', row)
     }
   }
+
+  const getHostList = async (isReset = false) => {
+    loading.value = true
+    if (attrs.id == undefined || !paginationProps.value) {
+      loading.value = false
+      return
+    }
+    if (isReset) {
+      paginationProps.value.current = 1
+    }
+    try {
+      const res = await getHosts({ ...filtersParams.value, clusterId: attrs.id 
})
+      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'] = (pagination, filters, ...args) 
=> {
+    onChange(pagination, filters, ...args)
+    getHostList()
+  }
+
+  onActivated(() => {
+    getHostList()
+  })
 </script>
 
 <template>
   <div class="host">
     <header>
-      <div class="host-title">{{ $t('host.host_list') }}</div>
+      <div class="header-title">{{ $t('host.host_list') }}</div>
       <a-space :size="16">
         <a-button type="primary" danger @click="handleDelete">{{ 
$t('common.bulk_remove') }}</a-button>
         <a-button type="primary">{{ $t('cluster.add_host') }}</a-button>
@@ -172,11 +201,11 @@
     </header>
     <a-table
       :loading="loading"
-      :data-source="data"
+      :data-source="dataSource"
       :columns="columns"
       :pagination="paginationProps"
       :row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: 
onSelectChange }"
-      @change="onChange"
+      @change="tableChange"
     >
       <template #customFilterDropdown="{ setSelectedKeys, selectedKeys, 
confirm, clearFilters, column }">
         <div class="search">
@@ -187,8 +216,10 @@
             @change="(e: any) => setSelectedKeys(e.target?.value ? 
[e.target?.value] : [])"
             @press-enter="handleSearch(selectedKeys, confirm, 
column.dataIndex)"
           />
-          <div class="search-btn">
-            <a-button size="small" @click="handleReset(clearFilters)"> {{ 
$t('common.reset') }} </a-button>
+          <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>
@@ -200,11 +231,12 @@
         <svg-icon v-else :name="filtered ? 'filter_activated' : 'filter'" />
       </template>
       <template #bodyCell="{ record, column }">
-        <template v-if="column.key === 'nodeName'">
-          <a-typography-link underline> {{ record.nodeName }} 
</a-typography-link>
+        <template v-if="column.key === 'hostname'">
+          <a-typography-link underline> {{ record.hostname }} 
</a-typography-link>
         </template>
         <template v-if="column.key === 'status'">
-          <svg-icon :name="record.status" />
+          <svg-icon style="margin-left: 0" 
:name="hostStatus[record.status].toLowerCase()" />
+          <span>{{ $t(`common.${hostStatus[record.status].toLowerCase()}`) 
}}</span>
         </template>
         <template v-if="column.key === 'operation'">
           <button-group
@@ -215,7 +247,7 @@
             :args="record"
             group-shape="default"
             group-type="link"
-          ></button-group>
+          />
         </template>
       </template>
     </a-table>
@@ -223,23 +255,21 @@
 </template>
 
 <style lang="scss" scoped>
-  .host {
-    header {
-      margin-bottom: $space-md;
-    }
-    &-title {
-      font-size: 14px;
-      font-weight: 500;
-      line-height: 22px;
-      margin-bottom: $space-md;
-    }
+  header {
+    margin-bottom: $space-md;
   }
   .search {
     display: grid;
     gap: $space-sm;
     padding: $space-sm;
-    &-btn {
-      @include flexbox($justify: flex-end, $gap: $space-md);
+    &-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/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
index 6017e998..71ab061b 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
@@ -18,65 +18,76 @@
 -->
 
 <script setup lang="ts">
-  import { computed, ref } from 'vue'
-  import { useRoute } from 'vue-router'
-  import type { GroupItem } from '@/components/common/button-group/types'
-  import type { TabItem } from '@/components/common/main-card/types'
+  import { computed, onMounted, ref, shallowRef } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { useClusterStore } from '@/store/cluster'
+  import { storeToRefs } from 'pinia'
+  import { execCommand } from '@/api/command'
+  import { Command } from '@/api/command/types'
+  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import Overview from './overview.vue'
   import Service from './service.vue'
   import Host from './host.vue'
   import User from './user.vue'
-  import Job from './job.vue'
+  import Job from '@/components/job/index.vue'
+  import type { TabItem } from '@/components/common/main-card/types'
+  import type { GroupItem } from '@/components/common/button-group/types'
+  import type { ClusterStatusType } from '@/api/cluster/types'
 
-  const route = useRoute()
-  const title = computed(() => route.params.cluster as string)
-  const desc = ref('我是描述')
+  const { t } = useI18n()
+  const clusterStore = useClusterStore()
+  const { currCluster, loading } = storeToRefs(clusterStore)
   const activeKey = ref('1')
-  const tabs = ref<TabItem[]>([
+  const statusColors = shallowRef<Record<ClusterStatusType, keyof typeof 
CommonStatusTexts>>({
+    1: 'healthy',
+    2: 'unhealthy',
+    3: 'unknow'
+  })
+  const tabs = computed((): TabItem[] => [
     {
       key: '1',
-      title: '概览'
+      title: t('common.overview')
     },
     {
       key: '2',
-      title: '服务'
+      title: t('common.service')
     },
     {
       key: '3',
-      title: '主机'
+      title: t('common.host')
     },
     {
       key: '4',
-      title: '用户'
+      title: t('common.user')
     },
     {
       key: '5',
-      title: '作业'
+      title: t('common.job')
     }
   ])
   const actionGroup = computed<GroupItem[]>(() => [
     {
       shape: 'default',
       type: 'primary',
-      text: '添加服务',
+      text: t('common.add', [t('common.service')]),
       clickEvent: () => addService && addService()
     },
     {
       shape: 'default',
       type: 'default',
-      text: '其他操作',
+      text: t('common.more_operations'),
       dropdownMenu: [
         {
-          action: 'start',
-          text: '启动集群'
+          action: 'Start',
+          text: t('common.start', [t('common.cluster')])
         },
         {
-          action: 'restart',
-          text: '重启集群'
+          action: 'Restart',
+          text: t('common.restart', [t('common.cluster')])
         },
         {
-          action: 'stop',
-          text: '停止集群'
+          action: 'Stop',
+          text: t('common.stop', [t('common.cluster')])
         }
       ],
       dropdownMenuClickEvent: (info) => dropdownMenuClick && 
dropdownMenuClick(info)
@@ -88,22 +99,46 @@
     return componnts[parseInt(activeKey.value) - 1]
   })
 
-  const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = ({ key }) => {
-    console.log('key :>> ', key)
+  const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key 
}) => {
+    try {
+      await execCommand({
+        command: key as keyof typeof Command,
+        clusterId: currCluster.value.id,
+        commandLevel: 'cluster'
+      })
+      clusterStore.loadClusters()
+      clusterStore.getClusterDetail()
+    } catch (error) {
+      console.log('error :>> ', error)
+    }
   }
 
   const addService: GroupItem['clickEvent'] = () => {
     console.log('add :>> ')
   }
+
+  onMounted(() => {
+    clusterStore.getClusterDetail()
+  })
 </script>
 
 <template>
-  <header-card :title="title" avatar="cluster" :desc="desc" 
:action-groups="actionGroup" />
-  <main-card v-model:active-key="activeKey" :tabs="tabs">
-    <template #tab-item>
-      <component :is="getCompName"></component>
-    </template>
-  </main-card>
+  <a-spin :spinning="loading">
+    <header-card
+      :title="currCluster.displayName"
+      avatar="cluster"
+      :status="CommonStatus[statusColors[currCluster.status as 
ClusterStatusType]]"
+      :desc="currCluster.desc"
+      :action-groups="actionGroup"
+    />
+    <main-card v-model:active-key="activeKey" :tabs="tabs">
+      <template #tab-item>
+        <keep-alive>
+          <component :is="getCompName" v-bind="currCluster"></component>
+        </keep-alive>
+      </template>
+    </main-card>
+  </a-spin>
 </template>
 
 <style lang="scss" scoped></style>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/job.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/job.vue
deleted file mode 100644
index 90a1eb21..00000000
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/job.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<!--
-  ~ 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 { type JobListItem, getJobList } from './components/mock'
-  import { ref } from 'vue'
-  import type { TableColumnType } from 'ant-design-vue'
-  import useBaseTable from '@/composables/use-base-table'
-
-  const columns: TableColumnType[] = [
-    {
-      title: '#',
-      width: '48px',
-      key: 'index',
-      customRender: ({ index }) => {
-        return `${index + 1}`
-      }
-    },
-    {
-      title: '名称',
-      key: 'name',
-      dataIndex: 'name',
-      ellipsis: true
-    },
-    {
-      title: '状态',
-      key: 'status',
-      dataIndex: 'status'
-    },
-    {
-      title: '创建时间',
-      dataIndex: 'createTime',
-      ellipsis: true
-    },
-    {
-      title: '更新时间',
-      dataIndex: 'updateTime',
-      ellipsis: true
-    }
-  ]
-  const data = ref<JobListItem[]>(getJobList(50))
-  const { columnsProp, dataSource, loading, paginationProps, onChange } = 
useBaseTable({
-    columns,
-    rows: data.value
-  })
-</script>
-
-<template>
-  <div class="job">
-    <header>
-      <div class="host-title">{{ $t('job.job_list') }}</div>
-    </header>
-    <a-table
-      :loading="loading"
-      :data-source="dataSource"
-      :columns="columnsProp"
-      :pagination="paginationProps"
-      @change="onChange"
-    >
-      <template #bodyCell="{ record, column }">
-        <template v-if="column.key === 'name'">
-          <a-typography-link underline> {{ record.name }} </a-typography-link>
-        </template>
-        <template v-if="column.key === 'status'">
-          <a-progress :percent="record.progress" :status="record.status" />
-        </template>
-      </template>
-    </a-table>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-  .job {
-    header {
-      margin-bottom: $space-md;
-    }
-  }
-</style>
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 c7daf834..3ec9aed4 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
@@ -18,89 +18,46 @@
 -->
 
 <script setup lang="ts">
-  import { ref } from 'vue'
-  import type { MenuProps } from 'ant-design-vue'
+  import { computed, ref, shallowRef, useAttrs } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { storeToRefs } from 'pinia'
+  import { useServiceStore } from '@/store/service'
+  import { formatFromByte } from '@/utils/storage'
+  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import GaugeChart from './components/gauge-chart.vue'
   import CategoryChart from './components/category-chart.vue'
-
-  const baseConfig = {
-    clusterStatus: '集群状态',
-    clusterName: '集群名',
-    clusterDesc: '集群备注',
-    hostCount: '主机数',
-    serviceCount: '服务数',
-    memory: '内存',
-    coreCount: '核心数',
-    diskSize: '磁盘大小',
-    creator: '创建人'
-  } as any
-
-  const baseInfo = {
-    clusterStatus: 'succsss',
-    clusterName: '集群名集群名集群名集群名集群名集群名集群名',
-    clusterDesc: '集群备注',
-    hostCount: '15个主机',
-    serviceCount: '15个服务',
-    memory: '内存',
-    coreCount: '15个核心',
-    diskSize: '磁盘大小',
-    creator: '创建人'
-  } as any
-
-  const serviceStack = [
-    {
-      stackId: 1,
-      stackName: 'Bigtop Stack',
-      stackVersion: 'bigtop-3.3.0',
-      services: [
-        {
-          id: 1,
-          name: 'HDFS'
-        },
-        {
-          id: 2,
-          name: 'Hive'
-        }
-      ]
-    },
-    {
-      stackId: 2,
-      stackName: 'Extra Stack',
-      stackVersion: 'extra-1.0.0',
-      services: [
-        {
-          id: 1,
-          name: 'Doris'
-        },
-        {
-          id: 2,
-          name: 'SeaTunnel'
-        }
-      ]
-    }
-  ]
-
-  const serviceOperates = [
-    {
-      action: 'start',
-      text: '启动服务'
-    },
-    {
-      action: 'restart',
-      text: '重启服务'
-    },
-    {
-      action: 'stop',
-      text: '停止服务'
-    }
-  ]
+  import type { ClusterStatusType, ClusterVO } from '@/api/cluster/types'
+  import type { MenuProps } from 'ant-design-vue'
 
   type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
   type TimeRangeItem = {
     text: TimeRangeText
     time: string
   }
-  const timeRanges: TimeRangeItem[] = [
+
+  const { t } = useI18n()
+  const attrs = useAttrs() as ClusterVO
+  const serviceStore = useServiceStore()
+  const currTimeRange = ref<TimeRangeText>('15m')
+  const chartData = ref({
+    chart1: [],
+    chart2: [],
+    chart3: [],
+    chart4: []
+  })
+  const statusColors = shallowRef<Record<ClusterStatusType, keyof typeof 
CommonStatusTexts>>({
+    1: 'healthy',
+    2: 'unhealthy',
+    3: 'unknow'
+  })
+  const { locateStackWithService } = storeToRefs(serviceStore)
+  const clusterDetail = computed(() => ({
+    ...attrs,
+    totalMemory: formatFromByte(attrs.totalMemory as number),
+    totalDisk: formatFromByte(attrs.totalDisk as number)
+  }))
+  const noChartData = computed(() => Object.values(chartData.value).every((v) 
=> v.length === 0))
+  const timeRanges = computed((): TimeRangeItem[] => [
     {
       text: '1m',
       time: ''
@@ -125,9 +82,43 @@
       text: '30h',
       time: ''
     }
-  ]
-
-  const currTimeRange = ref<TimeRangeText>('15m')
+  ])
+  const baseConfig = computed((): Partial<Record<keyof ClusterVO, string>> => {
+    return {
+      status: t('overview.cluster_status'),
+      displayName: t('overview.cluster_name'),
+      desc: t('overview.cluster_desc'),
+      totalHost: t('overview.host_count'),
+      totalService: t('overview.service_count'),
+      totalMemory: t('overview.memory'),
+      totalProcessor: t('overview.core_count'),
+      totalDisk: t('overview.disk_size'),
+      createUser: t('overview.creator')
+    }
+  })
+  const unitOfBaseConfig = computed((): Partial<Record<keyof ClusterVO, 
string>> => {
+    return {
+      totalHost: t('overview.unit_host'),
+      totalService: t('overview.unit_service'),
+      totalProcessor: t('overview.unit_processor')
+    }
+  })
+  const detailKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ClusterVO)[])
+  const serviceStack = computed(() => locateStackWithService.value)
+  const serviceOperates = computed(() => [
+    {
+      action: 'start',
+      text: t('common.start', [t('common.service')])
+    },
+    {
+      action: 'restart',
+      text: t('common.restart', [t('common.service')])
+    },
+    {
+      action: 'stop',
+      text: t('common.stop', [t('common.service')])
+    }
+  ])
 
   const handleServiceOperate: MenuProps['onClick'] = (item) => {
     console.log('item :>> ', item.key)
@@ -142,66 +133,99 @@
   <div class="dashboard">
     <a-row :gutter="[50, 16]" :wrap="true">
       <a-col :xs="24" :sm="24" :md="24" :lg="10" :xl="7" style="display: flex; 
flex-direction: column; gap: 24px">
-        <!-- base info -->
-        <a-descriptions layout="vertical" bordered>
+        <div class="base-info">
+          <div class="box-title">
+            <a-typography-text strong :content="$t('overview.basic_info')" />
+          </div>
+          <div>
+            <a-descriptions layout="vertical" bordered>
+              <a-descriptions-item>
+                <template #label>
+                  <div class="desc-sub-label">
+                    <a-typography-text strong :content="$t('overview.detail')" 
/>
+                  </div>
+                </template>
+                <div class="desc-sub-item-wrp">
+                  <div class="desc-sub-item">
+                    <template v-for="base in detailKeys" :key="base">
+                      <div class="desc-sub-item-desc">
+                        <a-typography-text
+                          class="desc-sub-item-desc-column"
+                          type="secondary"
+                          :content="baseConfig[base]"
+                        />
+                        <a-tag
+                          v-if="base === 'status'"
+                          class="reset-tag"
+                          
:color="CommonStatus[statusColors[clusterDetail[base] as ClusterStatusType]]"
+                        >
+                          <status-dot 
:color="CommonStatus[statusColors[clusterDetail[base] as ClusterStatusType]]" />
+                          {{
+                            clusterDetail[base] &&
+                            $t(`common.${statusColors[clusterDetail[base] as 
ClusterStatusType]}`)
+                          }}
+                        </a-tag>
+                        <a-typography-text
+                          v-else
+                          class="desc-sub-item-desc-column"
+                          :content="
+                            Object.keys(unitOfBaseConfig).includes(base)
+                              ? `${clusterDetail[base]} 
${unitOfBaseConfig[base]}`
+                              : `${clusterDetail[base]}`
+                          "
+                        />
+                      </div>
+                    </template>
+                  </div>
+                </div>
+              </a-descriptions-item>
+            </a-descriptions>
+          </div>
+        </div>
+        <!-- service info -->
+        <template v-if="serviceStack.length == 0">
+          <div class="service-info">
+            <div class="box-title">
+              <a-typography-text strong :content="$t('overview.service_info')" 
/>
+            </div>
+            <div class="box-empty">
+              <a-empty />
+            </div>
+          </div>
+        </template>
+        <a-descriptions v-else layout="vertical" bordered :column="1">
           <template #title>
-            <a-typography-text strong content="基本信息" />
+            <a-typography-text strong :content="$t('overview.service_info')" />
           </template>
-          <a-descriptions-item>
+          <a-descriptions-item v-for="stack in serviceStack" 
:key="stack.stackName">
             <template #label>
               <div class="desc-sub-label">
-                <a-typography-text strong content="详情" />
+                <a-typography-text strong :content="stack.stackName" />
+                <a-typography-text type="secondary" 
:content="stack.stackVersion" />
               </div>
             </template>
-            <div class="desc-sub-item-wrp">
-              <div class="desc-sub-item">
-                <template v-for="base in Object.keys(baseInfo)" :key="base">
-                  <div class="desc-sub-item-desc">
-                    <a-typography-text type="secondary" 
:content="baseConfig[base]" />
-                    <a-typography-text :content="baseInfo[base]" />
-                  </div>
+            <div v-for="service in stack.services" :key="service.id" 
class="service-item">
+              <a-avatar shape="square" :size="16" />
+              <a-typography-text :content="service.displayName" />
+              <a-dropdown :trigger="['click']">
+                <a-button type="text" shape="circle" size="small">
+                  <svg-icon name="more" style="margin: 0" />
+                </a-button>
+                <template #overlay>
+                  <a-menu @click="handleServiceOperate">
+                    <a-menu-item v-for="operate in serviceOperates" 
:key="operate.action">
+                      <span>{{ operate.text }}</span>
+                    </a-menu-item>
+                  </a-menu>
                 </template>
-              </div>
+              </a-dropdown>
             </div>
           </a-descriptions-item>
         </a-descriptions>
-        <!-- service info -->
-        <a-descriptions layout="vertical" bordered :column="1">
-          <template #title>
-            <a-typography-text strong content="服务信息" />
-          </template>
-          <template v-for="stack in serviceStack" :key="stack.stackId">
-            <a-descriptions-item>
-              <template #label>
-                <div class="desc-sub-label">
-                  <a-typography-text strong :content="stack.stackName" />
-                  <a-typography-text type="secondary" 
:content="stack.stackVersion" />
-                </div>
-              </template>
-              <div v-for="service in stack.services" :key="service.id" 
class="service-item">
-                <a-avatar shape="square" :size="16" />
-                <a-typography-text :content="service.name" />
-
-                <a-dropdown :trigger="['click']">
-                  <a-button type="text" shape="circle" size="small">
-                    <svg-icon name="more" style="margin: 0" />
-                  </a-button>
-                  <template #overlay>
-                    <a-menu @click="handleServiceOperate">
-                      <a-menu-item v-for="operate in serviceOperates" 
:key="operate.action">
-                        <span>{{ operate.text }}</span>
-                      </a-menu-item>
-                    </a-menu>
-                  </template>
-                </a-dropdown>
-              </div>
-            </a-descriptions-item>
-          </template>
-        </a-descriptions>
       </a-col>
       <a-col :xs="24" :sm="24" :md="24" :lg="14" :xl="17">
-        <div class="chart-title">
-          <a-typography-text strong content="图表" />
+        <div class="box-title">
+          <a-typography-text strong :content="$t('overview.chart')" />
           <a-space :size="12">
             <div
               v-for="time in timeRanges"
@@ -215,25 +239,30 @@
             </div>
           </a-space>
         </div>
-        <a-row class="chart-wrp">
+        <template v-if="noChartData">
+          <div class="box-empty">
+            <a-empty />
+          </div>
+        </template>
+        <a-row v-else class="box-content">
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart1" title="内存使用率" />
+              <gauge-chart chart-id="chart1" 
:title="$t('overview.memory_usage')" />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <gauge-chart chart-id="chart2" title="CPU 使用率" />
+              <gauge-chart chart-id="chart2" :title="$t('overview.cpu_usage')" 
/>
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <category-chart chart-id="chart4" title="CPU 使用率" />
+              <category-chart chart-id="chart4" 
:title="$t('overview.cpu_usage')" />
             </div>
           </a-col>
           <a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
             <div class="chart-item-wrp">
-              <category-chart chart-id="chart3" title="内存使用率" />
+              <category-chart chart-id="chart3" 
:title="$t('overview.memory_usage')" />
             </div>
           </a-col>
         </a-row>
@@ -243,68 +272,78 @@
 </template>
 
 <style lang="scss" scoped>
-  .dashboard {
-    .service-item {
-      display: grid;
-      grid-template-columns: auto 1fr auto;
-      gap: $space-md;
-      align-items: center;
-      padding: $space-md;
-      border-bottom: 1px solid #e5e5e5;
-    }
-
-    .chart-title {
-      display: flex;
-      justify-content: space-between;
+  .box {
+    &-title {
+      @include flexbox($justify: space-between);
       margin-bottom: 20px;
     }
 
-    .time-range {
-      padding-inline: 6px;
-      border-radius: 4px;
-      text-align: center;
-      cursor: pointer;
-      user-select: none;
-      outline: none;
-      transition: background-color 0.3s;
-
-      &:hover {
-        color: $color-primary-text-hover;
-      }
-      &-activated {
-        color: $color-primary-text;
-      }
-    }
-
-    .chart-wrp {
+    &-content {
       border-radius: 8px;
       overflow: hidden;
       box-sizing: border-box;
       border: 1px solid $color-border;
     }
 
-    .chart-item-wrp {
+    &-empty {
+      @include flexbox($justify: center, $align: center);
+      min-height: 200px;
+      border-radius: 8px;
+      box-sizing: border-box;
       border: 1px solid $color-border;
-      margin-right: -1px;
-      margin-bottom: -1px;
+    }
+  }
 
-      &:first-child {
-        border-left: 0;
-        border-top: 0;
-      }
+  .time-range {
+    padding-inline: 6px;
+    border-radius: 4px;
+    text-align: center;
+    cursor: pointer;
+    user-select: none;
+    outline: none;
+    transition: background-color 0.3s;
 
-      &:not(:last-child) {
-        border-right: 0;
-      }
+    &:hover {
+      color: $color-primary-text-hover;
+    }
+    &-activated {
+      color: $color-primary-text;
+    }
+  }
 
-      &:nth-child(n + 3):not(:nth-child(4)) {
-        border-bottom: 0;
-        border-left: 0;
-      }
+  .service-item {
+    display: grid;
+    grid-template-columns: auto 1fr auto;
+    gap: $space-md;
+    align-items: center;
+    padding: $space-md;
+    border-bottom: 1px solid #3b2020;
+  }
+
+  .chart-item-wrp {
+    border: 1px solid $color-border;
+    margin-right: -1px;
+    margin-bottom: -1px;
+
+    &:first-child {
+      border-left: 0;
+      border-top: 0;
+    }
+
+    &:not(:last-child) {
+      border-right: 0;
+    }
+
+    &:nth-child(n + 3):not(:nth-child(4)) {
+      border-bottom: 0;
+      border-left: 0;
     }
+  }
 
+  .dashboard {
     :deep(.ant-descriptions-view) {
       overflow: hidden;
+      border-color: $color-border;
       .ant-descriptions-item-label {
         padding: 0;
       }
@@ -314,17 +353,23 @@
     }
 
     .desc-sub-item-wrp {
-      display: flex;
-      gap: $space-md;
+      @include flexbox($gap: $space-md);
       padding: $space-md;
+      :deep(.ant-tag) {
+        width: fit-content;
+        @include flexbox($align: center, $gap: 4px);
+      }
     }
 
     .desc-sub-item {
-      @include flexbox($direction: column, $gap: $space-md);
+      display: grid;
+      grid-template-columns: max-content 1fr;
+      gap: 16px;
       &-desc {
-        display: grid;
-        grid-template-columns: 56px auto;
-        gap: 30px;
+        display: contents;
+        &-column {
+          text-align: left;
+        }
       }
     }
 
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 007a9cb2..f8062f57 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -18,16 +18,26 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onMounted, ref, shallowRef } from 'vue'
-  import { getServices, StatusColors, StatusTexts, type ServiceItem } from 
'./components/mock'
+  import { computed, onActivated, shallowRef, toRefs, useAttrs } from 'vue'
   import { usePngImage } from '@/utils/tools'
   import { useI18n } from 'vue-i18n'
+  import { useServiceStore } from '@/store/service'
+  import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   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 { ClusterVO } from '@/api/cluster/types'
 
   const { t } = useI18n()
-  const data = ref<ServiceItem[]>([])
+  const attrs = useAttrs() as ClusterVO
+  const serviceStore = useServiceStore()
+  const { services, loading } = toRefs(serviceStore)
+  const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
+    1: 'healthy',
+    2: 'unhealthy',
+    3: 'unknow'
+  })
   const actionGroups = shallowRef<GroupItem[]>([
     {
       action: 'start',
@@ -63,19 +73,19 @@
     {
       type: 'search',
       key: 'serviceName',
-      label: '服务名'
+      label: t('service.name')
     },
     {
       type: 'status',
-      key: 'restart',
-      label: '需要重启',
+      key: 'restartFlag',
+      label: t('service.required_restart'),
       options: [
         {
-          label: '需重启',
+          label: t('common.required'),
           value: 1
         },
         {
-          label: '无需重启',
+          label: t('common.not_required'),
           value: 2
         }
       ]
@@ -83,73 +93,71 @@
     {
       type: 'status',
       key: 'status',
-      label: '状态',
+      label: t('common.status'),
       options: [
         {
-          label: t(`common.${StatusTexts.success}`),
-          value: StatusTexts.success
+          label: t(`common.${statusColors.value[1]}`),
+          value: 1
         },
         {
-          label: t(`common.${StatusTexts.error}`),
-          value: StatusTexts.error
+          label: t(`common.${statusColors.value[2]}`),
+          value: 2
         },
         {
-          label: t(`common.${StatusTexts.unknow}`),
-          value: StatusTexts.unknow
+          label: t(`common.${statusColors.value[3]}`),
+          value: 3
         }
       ]
     }
   ])
 
-  const onFilter = (filters: any) => {
-    console.log('filters :>> ', filters)
+  const getServices = (filters?: ServiceListParams) => {
+    attrs.id != undefined && serviceStore.getServices(attrs.id, filters)
   }
 
-  onMounted(() => {
-    data.value = getServices()
+  onActivated(() => {
+    getServices()
   })
 </script>
 
 <template>
-  <div class="service">
-    <filter-form :filter-items="filterFormItems" @filter="onFilter" />
-    <a-card v-for="item in data" :key="item.key" :hoverable="true" 
class="service-item">
-      <div class="header">
-        <div class="header-base-wrp">
-          <a-avatar
-            v-if="item.serviceName"
-            :src="usePngImage(item.serviceName.toLowerCase())"
-            :size="42"
-            class="header-icon"
-          />
-          <div class="header-base-title">
-            <span>{{ `${item.serviceName}` }}</span>
-            <span class="small-gray">{{ item.version }}</span>
+  <a-spin :spinning="loading" class="service">
+    <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.id" :hoverable="true" 
class="service-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" />
+            <div class="header-base-title">
+              <span>{{ `${item.displayName}` }}</span>
+              <span class="small-gray">{{ item.version }}</span>
+            </div>
+            <div class="header-base-status">
+              <a-tag :color="statusColors[item.status]">
+                <div class="header-base-status-inner">
+                  <status-dot :color="CommonStatus[statusColors[item.status]]" 
/>
+                  <span class="small">{{ 
$t(`common.${CommonStatusTexts[item.status]}`) }}</span>
+                </div>
+              </a-tag>
+            </div>
           </div>
-          <div class="header-base-status">
-            <a-tag :color="StatusColors[item.status]">
-              <div class="header-base-status-inner">
-                <status-dot :color="StatusColors[item.status]" />
-                <span class="small">{{ 
$t(`common.${StatusTexts[item.status]}`) }}</span>
-              </div>
-            </a-tag>
+          <div class="header-restart-status">
+            <span class="small-gray">{{ `${$t('common.restart')}` }}</span>
+            <status-dot :color="CommonStatus[statusColors[item.status]]" />
+            <span class="small">{{ `${item.restartFlag ? $t('common.required') 
: $t('common.not_required')}` }}</span>
           </div>
         </div>
-        <div class="header-restart-status">
-          <span class="small-gray">{{ `${$t('common.restart')}` }}</span>
-          <status-dot :color="StatusColors[item.status]" />
-          <span class="small">{{ `${item.restart ? $t('common.required') : 
$t('common.not_required')}` }}</span>
+        <div class="item-content">
+          <button-group :auto="true" :space="0" :groups="actionGroups">
+            <template #icon="{ item: groupItem }">
+              <svg-icon :name="groupItem.icon || ''" />
+            </template>
+          </button-group>
         </div>
-      </div>
-      <div class="item-content">
-        <button-group :auto="true" :space="0" :groups="actionGroups">
-          <template #icon="{ item: groupItem }">
-            <svg-icon :name="groupItem.icon || ''" />
-          </template>
-        </button-group>
-      </div>
-    </a-card>
-  </div>
+      </a-card>
+    </template>
+  </a-spin>
 </template>
 
 <style lang="scss" scoped>
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 2955093d..fc967256 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
@@ -18,12 +18,16 @@
 -->
 
 <script setup lang="ts">
-  import { type UserListItem, getUserList } from './components/mock'
-  import { ref } from 'vue'
-  import type { TableColumnType } from 'ant-design-vue'
+  import { computed, onActivated, useAttrs } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { getUserListOfService } from '@/api/cluster'
   import useBaseTable from '@/composables/use-base-table'
+  import type { TableColumnType } from 'ant-design-vue'
+  import type { ClusterVO, ServiceUserVO } from '@/api/cluster/types'
 
-  const columns: TableColumnType[] = [
+  const { t } = useI18n()
+  const attrs = useAttrs() as ClusterVO
+  const columns = computed((): TableColumnType[] => [
     {
       title: '#',
       width: '48px',
@@ -33,55 +37,68 @@
       }
     },
     {
-      title: '服务名',
-      dataIndex: 'serviceName',
+      title: t('service.name'),
+      dataIndex: 'displayName',
       width: '20%',
       ellipsis: true
     },
     {
-      title: '用户名',
-      dataIndex: 'userName',
+      title: t('user.username'),
+      dataIndex: 'user',
       width: '15%',
       ellipsis: true
     },
     {
-      title: '用户组',
+      title: t('user.user_group'),
       dataIndex: 'userGroup',
       width: '20%',
       ellipsis: true
     },
     {
-      title: '描述',
-      dataIndex: 'descrip',
+      title: t('common.desc'),
+      dataIndex: 'desc',
       ellipsis: true
     }
-  ]
-  const data = ref<UserListItem[]>(getUserList(50))
-  const { columnsProp, dataSource, loading, paginationProps, onChange } = 
useBaseTable({
-    columns,
-    rows: data.value
+  ])
+  const { dataSource, loading, filtersParams, paginationProps, onChange } = 
useBaseTable<ServiceUserVO>({
+    columns: columns.value,
+    rows: []
+  })
+
+  const loadUserListOfService = async () => {
+    if (attrs.id == undefined || !paginationProps.value) {
+      loading.value = false
+      return
+    }
+    try {
+      const data = await getUserListOfService(attrs.id, filtersParams.value)
+      dataSource.value = data.content
+      paginationProps.value.total = data.total
+    } catch (error) {
+      console.log('error :>> ', error)
+    } finally {
+      loading.value = false
+    }
+  }
+
+  onActivated(() => {
+    loadUserListOfService()
   })
 </script>
 
 <template>
   <div class="user">
     <header>
-      <div class="host-title">{{ $t('user.user_list') }}</div>
+      <div class="header-title">{{ $t('user.user_list') }}</div>
     </header>
     <a-table
       :loading="loading"
       :data-source="dataSource"
-      :columns="columnsProp"
+      :columns="columns"
       :pagination="paginationProps"
       @change="onChange"
     ></a-table>
   </div>
 </template>
 
-<style lang="scss" scoped>
-  .user {
-    header {
-      margin-bottom: $space-md;
-    }
-  }
-</style>
+<style lang="scss" scoped></style>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
index 85ed4a0a..31b357be 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
@@ -249,14 +249,13 @@
     {
       type: 'input',
       field: 'agentDir',
-      slot: 'agentDirSlot',
       formItemProps: {
         name: 'agentDir',
         label: t('host.agent_path')
       },
       controlProps: {
         disabled: isEdit.value,
-        placeholder: t('common.enter_error', 
[`${t('host.agent_path')}`.toLowerCase()])
+        placeholder: t('host.default_agent_path')
       }
     },
     {
@@ -273,13 +272,12 @@
     {
       type: 'input',
       field: 'grpcPort',
-      slot: 'grpcPortSlot',
       formItemProps: {
         name: 'grpcPort',
         label: t('host.grpc_port')
       },
       controlProps: {
-        placeholder: t('common.enter_error', 
[`${t('host.grpc_port')}`.toLowerCase()])
+        placeholder: t('host.default_grpc_port')
       }
     },
     {
@@ -506,7 +504,7 @@
             </a-upload>
           </a-form-item>
         </template>
-        <template #agentDirSlot="{ item, state }">
+        <!-- <template #agentDirSlot="{ item, state }">
           <a-form-item>
             <template #label>
               <div class="question">
@@ -518,8 +516,8 @@
             </template>
             <a-input v-bind="item.controlProps" 
v-model:value="state[item.field]" />
           </a-form-item>
-        </template>
-        <template #grpcPortSlot="{ item, state }">
+        </template> -->
+        <!-- <template #grpcPortSlot="{ item, state }">
           <a-form-item>
             <template #label>
               <div class="question">
@@ -531,7 +529,7 @@
             </template>
             <a-input v-bind="item.controlProps" 
v-model:value="state[item.field]" />
           </a-form-item>
-        </template>
+        </template> -->
       </auto-form>
       <template #footer>
         <footer>
@@ -555,7 +553,6 @@
   }
   footer {
     width: 100%;
-    display: flex;
-    justify-content: flex-end;
+    @include flexbox($justify: flex-end);
   }
 </style>
diff --git a/bigtop-manager-ui/src/store/cluster/index.ts 
b/bigtop-manager-ui/src/store/cluster/index.ts
index 5c8459c6..f994d139 100644
--- a/bigtop-manager-ui/src/store/cluster/index.ts
+++ b/bigtop-manager-ui/src/store/cluster/index.ts
@@ -17,49 +17,65 @@
  * under the License.
  */
 
+import { computed, ref } from 'vue'
 import { defineStore } from 'pinia'
-import { ClusterVO } from '@/api/cluster/types.ts'
-import { ref } from 'vue'
-import { getClusterList } from '@/api/cluster'
+import { useRoute } from 'vue-router'
+import { getCluster, getClusterList } from '@/api/cluster'
+import { useServiceStore } from '@/store/service'
+import type { ClusterVO } from '@/api/cluster/types.ts'
 
 export const useClusterStore = defineStore(
   'cluster',
   () => {
+    const route = useRoute()
+    const serviceStore = useServiceStore()
     const clusters = ref<ClusterVO[]>([])
-    const count = ref(0)
+    const loading = ref(false)
+    const currCluster = ref<ClusterVO>({})
+    const clusterId = computed(() => (route.params.id as string) || undefined)
 
     const addCluster = async () => {
-      count.value = count.value + 1
       await loadClusters()
     }
 
     const delCluster = async () => {
-      if (count.value < 0) {
-        count.value = 0
+      await loadClusters()
+    }
+
+    const getClusterDetail = async () => {
+      if (clusterId.value == undefined) {
         return
       }
-      count.value = count.value - 1
-      await loadClusters()
+      try {
+        loading.value = true
+        currCluster.value = await getCluster(parseInt(clusterId.value))
+        currCluster.value.id != undefined && 
serviceStore.getServices(currCluster.value.id)
+      } catch (error) {
+        console.log('error :>> ', error)
+      } finally {
+        loading.value = false
+      }
     }
 
     const loadClusters = async () => {
-      const data = await getClusterList()
-      clusters.value = data as ClusterVO[]
+      clusters.value = await getClusterList()
     }
 
     return {
       clusters,
-      count,
+      loading,
+      currCluster,
       addCluster,
       delCluster,
-      loadClusters
+      loadClusters,
+      getClusterDetail
     }
   },
   {
     persist: false
     // persist: {
     //   storage: sessionStorage,
-    //   paths: ['clusters', 'count']
+    //   paths: ['clusters',]
     // }
   }
 )
diff --git a/bigtop-manager-ui/src/store/service/index.ts 
b/bigtop-manager-ui/src/store/service/index.ts
new file mode 100644
index 00000000..330903f3
--- /dev/null
+++ b/bigtop-manager-ui/src/store/service/index.ts
@@ -0,0 +1,65 @@
+/*
+ * 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
+ *
+ *    https://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.
+ */
+
+import { defineStore, storeToRefs } from 'pinia'
+import { computed, ref } from 'vue'
+import { getServiceList } from '@/api/service'
+import { useStackStore } from '@/store/stack/index'
+import type { ServiceListParams, ServiceVO } from '@/api/service/types'
+
+export const useServiceStore = defineStore(
+  'service',
+  () => {
+    const stackStore = useStackStore()
+    const services = ref<ServiceVO[]>([])
+    const total = ref(0)
+    const loading = ref(false)
+    const { stacks } = storeToRefs(stackStore)
+
+    const serviceNames = computed(() => services.value.map((v) => v.name))
+    const locateStackWithService = computed(() =>
+      stacks.value.filter((item) =>
+        item.services.some((service) => service.name && 
serviceNames.value.includes(service.name))
+      )
+    )
+
+    const getServices = async (clusterId: number, filterParams?: 
ServiceListParams) => {
+      try {
+        loading.value = true
+        const data = await getServiceList(clusterId, { ...filterParams, 
pageNum: 1, pageSize: 50 })
+        services.value = data.content
+        total.value = data.total
+      } catch (error) {
+        console.log('error :>> ', error)
+      } finally {
+        loading.value = false
+      }
+    }
+
+    return {
+      services,
+      loading,
+      getServices,
+      locateStackWithService
+    }
+  },
+  {
+    persist: false
+  }
+)
diff --git a/bigtop-manager-ui/src/styles/index.scss 
b/bigtop-manager-ui/src/styles/index.scss
index f2bf2d1f..6689600f 100644
--- a/bigtop-manager-ui/src/styles/index.scss
+++ b/bigtop-manager-ui/src/styles/index.scss
@@ -54,3 +54,9 @@
 :where(.ant-modal-header) {
   margin-bottom: 16px !important;
 }
+
+
+.header-title {
+  font-weight: 500;
+  margin-bottom: $space-md;
+}
diff --git a/bigtop-manager-ui/src/utils/tools.ts 
b/bigtop-manager-ui/src/utils/tools.ts
index 9092937a..53c8f059 100644
--- a/bigtop-manager-ui/src/utils/tools.ts
+++ b/bigtop-manager-ui/src/utils/tools.ts
@@ -93,3 +93,23 @@ export const pick = <T extends object, K extends keyof 
T>(obj: T, keys: K[]): Pi
     {} as Pick<T, K>
   )
 }
+
+type Data = { [key: string]: any }
+type Result = { [key: string]: any | undefined }
+
+export const processData = (data: Data): Result => {
+  const result: Result = {}
+  if (!data) {
+    return result
+  }
+  for (const [key, value] of Object.entries(data)) {
+    if (value === null) {
+      result[key] = undefined
+    } else if (Array.isArray(value)) {
+      result[key] = value[0] || undefined
+    } else {
+      result[key] = value
+    }
+  }
+  return result
+}
diff --git a/bigtop-manager-ui/tests/__composables__/use-table.test.ts 
b/bigtop-manager-ui/tests/__composables__/use-table.test.ts
index 006a537f..60a76270 100644
--- a/bigtop-manager-ui/tests/__composables__/use-table.test.ts
+++ b/bigtop-manager-ui/tests/__composables__/use-table.test.ts
@@ -71,7 +71,6 @@ describe('useBaseTable', () => {
     }
     
expect(result.paginationProps.value).toEqual(expect.objectContaining(expectedPaginationProps))
     // Test showTotal function separately
-    console.log('result.paginationProps.value.showTotal(10) :>> ', 
result.paginationProps.value.showTotal(10))
     expect(result.paginationProps.value.showTotal(10)).toBe('Total: 10')
   })
 


Reply via email to