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 21739ef3 BIGTOP-4401: Supports adding/deleting components (#201)
21739ef3 is described below

commit 21739ef37ebafe06a8e57b41a8e2a8beba1fb2e3
Author: Fdefined <[email protected]>
AuthorDate: Wed Apr 9 14:50:06 2025 +0800

    BIGTOP-4401: Supports adding/deleting components (#201)
---
 .../src/components/common/button-group/index.vue   |  6 +-
 .../src/components/common/button-group/types.ts    |  4 +-
 .../components/component-assigner.vue              | 44 ++++++++---
 .../components/component-installer.vue             | 13 ++--
 .../components/service-configurator.vue            | 16 +++-
 .../create-service/components/service-selector.vue | 48 +++++++++---
 .../components/use-create-service.ts               | 88 ++++++++++++++++++----
 .../src/components/create-service/create.vue       | 41 +++++++---
 bigtop-manager-ui/src/components/job/index.vue     |  2 +-
 .../cluster/components/check-workflow.vue          |  1 -
 .../cluster/components/host-config.vue             |  2 +-
 .../src/pages/cluster-manage/cluster/host.vue      | 67 ++++++++++++----
 .../cluster/service-management/components.vue      | 23 ++++--
 .../components/snapshot-management.vue             |  2 +-
 .../cluster/service-management/index.vue           |  2 +-
 .../src/pages/cluster-manage/cluster/service.vue   |  2 +-
 .../src/pages/cluster-manage/hosts/create.vue      |  3 +-
 .../src/pages/cluster-manage/hosts/index.vue       |  4 +-
 .../cluster-manage/hosts/install-dependencies.vue  |  2 +-
 .../pages/cluster-manage/infrastructures/index.vue | 14 +++-
 .../src/router/routes/modules/clusters.ts          | 21 +++++-
 bigtop-manager-ui/src/store/menu/index.ts          |  3 +
 bigtop-manager-ui/src/store/stack/index.ts         |  2 +-
 bigtop-manager-ui/src/styles/index.scss            |  2 +-
 24 files changed, 312 insertions(+), 100 deletions(-)

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 43ee306a..7d475c5c 100644
--- a/bigtop-manager-ui/src/components/common/button-group/index.vue
+++ b/bigtop-manager-ui/src/components/common/button-group/index.vue
@@ -34,7 +34,7 @@
   })
 
   const { t } = useI18n()
-  const { groups, groupShape, groupType, space, args } = toRefs(props)
+  const { groups, groupShape, groupType, space, payload } = toRefs(props)
 
   const checkTitle = (item: GroupItem) => {
     if (props.i18n) {
@@ -52,7 +52,7 @@
 <template>
   <a-space :size="space" :wrap="true" :class="{ 'text-compact': 
$props.textCompact, 'btn-auto': $props.auto }">
     <template v-for="(item, _index) in groups" :key="_index">
-      <template v-if="!item.hidden">
+      <template v-if="!(typeof item.hidden === 'function' ? item.hidden(item, 
payload) : item.hidden ?? false)">
         <a-dropdown v-if="item.dropdownMenu">
           <a-button
             :disabled="item.disabled"
@@ -83,7 +83,7 @@
           :shape="item.shape || groupShape || 'default'"
           :type="item.type || groupType || 'default'"
           :title="checkTitle(item)"
-          @click="item.clickEvent ? item.clickEvent(item, args) : () => {}"
+          @click="item.clickEvent ? item.clickEvent(item, payload) : () => {}"
         >
           <template #icon>
             <slot name="icon" :item="item" />
diff --git a/bigtop-manager-ui/src/components/common/button-group/types.ts 
b/bigtop-manager-ui/src/components/common/button-group/types.ts
index 4125e867..b309a171 100644
--- a/bigtop-manager-ui/src/components/common/button-group/types.ts
+++ b/bigtop-manager-ui/src/components/common/button-group/types.ts
@@ -31,7 +31,7 @@ export interface GroupItem<T = any> {
   tip?: string
   text?: string
   action?: T
-  hidden?: boolean
+  hidden?: boolean | ((item?: GroupItem<T>, ...args: any[]) => boolean)
   type?: BtnType
   shape?: ShapeType
   disabled?: boolean
@@ -48,6 +48,6 @@ export interface Props {
   groupType?: BtnType
   groupShape?: ShapeType
   space?: number
-  args?: any
+  payload?: any
   auto?: boolean
 }
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/component-assigner.vue
 
b/bigtop-manager-ui/src/components/create-service/components/component-assigner.vue
index df9574b1..231d354a 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/component-assigner.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/component-assigner.vue
@@ -48,7 +48,16 @@
     searchedColumn: '',
     selectedRowKeys: []
   })
-  const { clusterId, installedStore, allComps, selectedServices, 
updateHostsForComponent } = useCreateService()
+  const {
+    clusterId,
+    installedStore,
+    allComps,
+    allCompsMeta,
+    creationModeType,
+    selectedServices,
+    updateHostsForComponent
+  } = useCreateService()
+
   const serviceList = computed(() =>
     selectedServices.value.map((v) => ({
       ...v,
@@ -56,10 +65,14 @@
     }))
   )
 
+  const currCompInfo = computed(() => 
allComps.value.get(currComp.value.split('/')[1]))
+  const currCompInfoMeta = computed(() => 
allCompsMeta.value.get(currComp.value.split('/')[1]))
+  const installedServices = computed(() => 
installedStore.getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`))
   const hostsOfCurrComp = computed((): HostVO[] => {
     const temp = currComp.value.split('/').at(-1)
-    return allComps.value.has(temp) ? allComps.value.get(temp)?.hosts : []
+    return allComps.value.has(temp!) ? allComps.value.get(temp!)?.hosts ?? [] 
: []
   })
+
   const columns = computed((): TableColumnType<HostVO>[] => [
     {
       title: t('host.hostname'),
@@ -119,20 +132,31 @@
     onChangeCallback: getHostList
   })
 
-  const getCheckboxProps: TableRowSelection['getCheckboxProps'] = (record) => 
({
-    disabled: installedStore
-      .getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
-      .includes(currComp.value.split('/')[0]),
-    name: record.hostname
-  })
+  const validateHostIsCheck = (host: HostVO) => {
+    const notAdd = currCompInfoMeta.value?.hosts.findIndex((v) => v.hostname 
=== host.hostname) == -1
+    if (creationModeType.value === 'component') {
+      return !currCompInfo.value?.uninstall && !notAdd
+    } else {
+      return installedServices.value.includes(currComp.value.split('/')[0]) && 
!notAdd
+    }
+  }
+
+  const getCheckboxProps: TableRowSelection['getCheckboxProps'] = (record) => {
+    return {
+      disabled: validateHostIsCheck(record),
+      name: record.hostname
+    }
+  }
 
   const onSelectChange: TableRowSelection['onChange'] = (selectedRowKeys, 
selectedRows) => {
-    allComps.value.has(currComp.value.split('/').at(-1)) && 
updateHostsForComponent(currComp.value, selectedRows)
+    allComps.value.has(currComp.value.split('/').at(-1)!) && 
updateHostsForComponent(currComp.value, selectedRows)
     state.selectedRowKeys = selectedRowKeys
   }
 
   const resetSelectedRowKeys = (key: string) => {
-    state.selectedRowKeys = allComps.value.has(key) ? 
allComps.value.get(key)?.hosts.map((v: HostVO) => v.hostname) : []
+    state.selectedRowKeys = allComps.value.has(key)
+      ? (allComps.value.get(key)?.hosts.map((v: HostVO) => v.hostname) as 
Key[]) ?? ([] as Key[])
+      : []
   }
 
   const treeSelectedChange = (keyPath: string) => {
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
 
b/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
index 825533af..af08100d 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
@@ -48,14 +48,13 @@
   })
 
   const getJobInstanceDetails = async () => {
-    const { id: jobId } = stepData.value
+    const { id: jobId, clusterId } = stepData.value
     if (jobId === undefined) {
       return true
     }
     try {
-      const data = await getJobDetails({ jobId, clusterId: 0 })
+      const data = await getJobDetails({ jobId, clusterId })
       jobDetail.value = data
-      activeKey.value = data.stages ? data.stages?.map((v) => v.id!) : []
       return ['Successful', 'Failed'].includes(data.state as StateType)
     } catch (error) {
       console.log('error :>> ', error)
@@ -64,12 +63,12 @@
   }
 
   const handleRetryJob = async () => {
-    const { id: jobId } = stepData.value
+    const { id: jobId, clusterId } = stepData.value
     if (jobId === undefined) {
       return true
     }
     try {
-      await retryJob({ jobId, clusterId: 0 })
+      await retryJob({ jobId, clusterId })
       pollJobDetails(getJobInstanceDetails)
     } catch (error) {
       console.log('error :>> ', error)
@@ -96,14 +95,14 @@
   }
 
   const viewLogs = (stage: StageVO, task: TaskVO) => {
-    const { id: jobId } = stepData.value
+    const { id: jobId, clusterId } = stepData.value
     const { id: stageId } = stage
     const { id: taskId } = task
     if (jobId === undefined || stageId === undefined || taskId === undefined) {
       return
     }
     logsViewState.payLoad = {
-      clusterId: 0,
+      clusterId,
       jobId,
       stageId,
       taskId
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/service-configurator.vue
 
b/bigtop-manager-ui/src/components/create-service/components/service-configurator.vue
index 37073c1d..0fb1aa07 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/service-configurator.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/service-configurator.vue
@@ -62,13 +62,21 @@
       lg: { span: 17 }
     }
   })
-  const serviceList = computed(() => selectedServices.value)
 
-  const disabled = computed(() =>
-    installedStore
+  const serviceList = computed(() => selectedServices.value)
+  const disabled = computed(() => {
+    return installedStore
       .getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
       .includes(currService.value.toString().split('/').at(-1)!)
-  )
+  })
+
+  // const disabled = computed(() => {
+  //   return creationModeType.value === 'component'
+  //     ? false
+  //     : installedStore
+  //         .getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
+  //         .includes(currService.value.toString().split('/').at(-1)!)
+  // })
 
   watch(
     () => props.isView,
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
 
b/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
index f8b47788..464242b6 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
@@ -18,18 +18,18 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onActivated, reactive, ref, toRefs, useAttrs } from 'vue'
+  import { computed, onActivated, reactive, ref, toRefs } from 'vue'
   import { usePngImage } from '@/utils/tools'
   import useCreateService from './use-create-service'
   import type { ExpandServiceVO } from '@/store/stack'
-  import { ComponentVO } from '@/api/component/types.ts'
+  import type { ComponentVO } from '@/api/component/types.ts'
+  import type { ServiceVO } from '@/api/service/types'
 
   interface State {
     isAddableData: ExpandServiceVO[]
     selectedData: ExpandServiceVO[]
   }
 
-  const { creationMode } = useAttrs()
   const searchStr = ref('')
   const state = reactive<State>({
     isAddableData: [],
@@ -37,10 +37,13 @@
   })
   const {
     clusterId,
+    routeParams,
     selectedServices,
     servicesOfInfra,
     servicesOfExcludeInfra,
     installedStore,
+    creationMode,
+    creationModeType,
     confirmServiceDependencies,
     setDataByCurrent
   } = useCreateService()
@@ -142,20 +145,41 @@
     }
   }
 
+  const mergeInherentServiceAndInstalledService = (
+    installedServiceMap: Map<string, ExpandServiceVO>,
+    inherentServices: ServiceVO[]
+  ) => {
+    const inherentService = inherentServices.filter((v) => v.name === 
routeParams.value.service)[0]
+    const installedService = { 
...installedServiceMap.get(routeParams.value.service)! }
+    const map = new Map(installedService.components!.map((item) => [item.name, 
item]))
+    inherentService.components!.forEach((item) => {
+      !map.has(item.name) && map.set(item.name, { ...item, hosts: [], 
uninstall: true })
+    })
+    installedService.components = [...map.values()]
+    return installedService
+  }
+
   const addInstalledSymbolForSelectedServices = async (onlyInstalled: boolean) 
=> {
     if (onlyInstalled) {
       const installedServiceMap = await initInstalledServicesDetail()
       const installedServiceNames = 
installedStore.getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
-      const data = creationMode === 'internal' ? servicesOfExcludeInfra.value 
: servicesOfInfra.value
-      data.forEach((v) => {
-        if (installedServiceNames.includes(v.name || '')) {
-          Object.assign(v, installedServiceMap.get(v.name!))
-          v.isInstalled = true
-          state.selectedData.push(v as ExpandServiceVO)
-        } else {
-          state.isAddableData.push(v as ExpandServiceVO)
+      const inherentServices = creationMode.value === 'internal' ? 
servicesOfExcludeInfra.value : servicesOfInfra.value
+
+      if (creationModeType.value === 'component' && 
installedServiceMap.has(routeParams.value.service)) {
+        state.selectedData[0] = 
mergeInherentServiceAndInstalledService(installedServiceMap, inherentServices)
+        state.selectedData[0].isInstalled = true
+        state.isAddableData = []
+      } else {
+        for (const service of inherentServices) {
+          if (installedServiceNames.includes(service.name || '')) {
+            Object.assign(service, installedServiceMap.get(service.name!))
+            service.isInstalled = true
+            state.selectedData.push(service as ExpandServiceVO)
+          } else {
+            state.isAddableData.push(service as ExpandServiceVO)
+          }
         }
-      })
+      }
       setDataByCurrent(state.selectedData)
     } else {
       state.selectedData = [...selectedServices.value]
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
 
b/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
index 021c8fd5..aaaeb25c 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
+++ 
b/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
@@ -29,15 +29,28 @@ import SvgIcon from '@/components/common/svg-icon/index.vue'
 import type { HostVO } from '@/api/hosts/types'
 import type { CommandRequest, CommandVO, ServiceCommandReq } from 
'@/api/command/types'
 import type { ServiceVO } from '@/api/service/types'
+import type { ComponentVO } from '@/api/component/types'
 
 interface ProcessResult {
   success: boolean
   conflictService?: ExpandServiceVO
 }
 
+interface RouteParams {
+  service: string
+  serviceId: number
+  id: number
+  cluster: string
+}
+
+interface CompItem extends ComponentVO {
+  hosts: HostVO[]
+}
+
 const scope = effectScope()
 let isChange = false
 let selectedServices: Ref<ExpandServiceVO[]>
+let selectedServicesMeta: Ref<ExpandServiceVO[]>
 let afterCreateRes: Ref<CommandVO>
 let servicesOfInfra: ComputedRef<ExpandServiceVO[]>
 let servicesOfExcludeInfra: ComputedRef<ExpandServiceVO[] | ServiceVO[]>
@@ -46,7 +59,8 @@ let steps: ComputedRef<string[]>
 const setupStore = () => {
   scope.run(() => {
     selectedServices = ref<ExpandServiceVO[]>([])
-    afterCreateRes = ref<CommandVO>({ id: undefined })
+    selectedServicesMeta = ref<ExpandServiceVO[]>([])
+    afterCreateRes = ref<{ clusterId: number } & CommandVO>({ id: undefined, 
clusterId: 0 })
     servicesOfInfra = computed(() => 
useStackStore().getServicesByExclude(['bigtop', 'extra']) as ExpandServiceVO[])
     servicesOfExcludeInfra = computed(() => 
useStackStore().getServicesByExclude(['infra']))
     steps = computed(() => [
@@ -71,6 +85,36 @@ const useCreateService = () => {
   const { current, stepsLimit, previousStep, nextStep } = useSteps(steps.value)
   const clusterId = computed(() => Number(route.params.id))
   const creationMode = computed(() => route.params.creationMode as 'internal' 
| 'public')
+  const creationModeType = computed(() => route.params.type)
+  const routeParams = computed(() => route.params as unknown as RouteParams)
+
+  const commandRequest = ref<CommandRequest>({
+    command: 'Add',
+    commandLevel: 'service',
+    clusterId: clusterId.value
+  })
+
+  const allComps = computed(() => {
+    return new Map(
+      selectedServices.value.flatMap((s) =>
+        s.components!.map((comp) => [
+          comp.name,
+          { serviceName: s.name, serviceDisplayName: s.displayName, serviceId: 
s.id, ...comp }
+        ])
+      )
+    ) as Map<string, CompItem>
+  })
+
+  const allCompsMeta = computed(() => {
+    return new Map(
+      selectedServicesMeta.value.flatMap((s) =>
+        s.components!.map((comp) => [
+          comp.name,
+          { serviceName: s.name, serviceDisplayName: s.displayName, serviceId: 
s.id, ...comp }
+        ])
+      )
+    ) as Map<string, CompItem>
+  })
 
   watch(
     () => selectedServices.value,
@@ -82,18 +126,9 @@ const useCreateService = () => {
     }
   )
 
-  const commandRequest = ref<CommandRequest>({
-    command: 'Add',
-    commandLevel: 'service',
-    clusterId: clusterId.value
-  })
-
-  const allComps = computed(() => {
-    return new Map(selectedServices.value.flatMap((s) => 
s.components!.map((comp) => [comp.name, comp])))
-  })
-
   const setDataByCurrent = (val: ExpandServiceVO[]) => {
     selectedServices.value = val
+    selectedServicesMeta.value = JSON.parse(JSON.stringify(val))
   }
 
   const updateHostsForComponent = (compName: string, hosts: HostVO[]) => {
@@ -220,6 +255,26 @@ const useCreateService = () => {
     try {
       commandRequest.value.serviceCommands = 
transformServiceData(selectedServices.value)
       afterCreateRes.value = await execCommand(commandRequest.value)
+      Object.assign(afterCreateRes.value, { clusterId })
+      return true
+    } catch (error) {
+      console.log('error :>> ', error)
+      return false
+    }
+  }
+
+  const addComponentForService = async () => {
+    try {
+      commandRequest.value.commandLevel = 'component'
+      commandRequest.value.componentCommands = []
+      for (const [compName, comp] of allComps.value) {
+        commandRequest.value.componentCommands?.push({
+          componentName: compName!,
+          hostnames: comp.hosts.map((v) => v.hostname!)
+        })
+      }
+      afterCreateRes.value = await execCommand(commandRequest.value)
+      Object.assign(afterCreateRes.value, { clusterId })
       return true
     } catch (error) {
       console.log('error :>> ', error)
@@ -233,19 +288,24 @@ const useCreateService = () => {
     current,
     stepsLimit,
     selectedServices,
+    selectedServicesMeta,
     servicesOfExcludeInfra,
     servicesOfInfra,
     installedStore,
     allComps,
     afterCreateRes,
+    scope,
+    creationMode,
+    creationModeType,
+    routeParams,
+    allCompsMeta,
     setDataByCurrent,
+    addComponentForService,
     updateHostsForComponent,
     confirmServiceDependencies,
     createService,
     previousStep,
-    nextStep,
-    scope,
-    creationMode
+    nextStep
   }
 }
 
diff --git a/bigtop-manager-ui/src/components/create-service/create.vue 
b/bigtop-manager-ui/src/components/create-service/create.vue
index 48f7e7cb..e687373b 100644
--- a/bigtop-manager-ui/src/components/create-service/create.vue
+++ b/bigtop-manager-ui/src/components/create-service/create.vue
@@ -35,18 +35,34 @@
     stepsLimit,
     steps,
     allComps,
+    allCompsMeta,
+    creationModeType,
     afterCreateRes,
     selectedServices,
     setDataByCurrent,
     previousStep,
     nextStep,
     createService,
-    confirmServiceDependencies
+    confirmServiceDependencies,
+    addComponentForService
   } = useCreateService()
   const compRef = ref<any>()
   const currComp = computed(() => components.value[current.value])
+  const noUninstallComponent = computed(() => {
+    return (
+      current.value === 1 &&
+      creationModeType.value === 'component' &&
+      Array.from(allComps.value).every(
+        ([compName, { hosts }]) => hosts.length === 
allCompsMeta.value.get(compName)?.hosts?.length
+      )
+    )
+  })
 
   const validateServiceSelection = async () => {
+    if (creationModeType.value === 'component') {
+      return true
+    }
+
     if (selectedServices.value.filter((v) => !v.isInstalled).length === 0) {
       message.error(t('service.service_selection'))
       return false
@@ -57,16 +73,17 @@
 
   const validateDependenciesOfServiceSelection = async () => {
     let selectedServiceNames = new Set(selectedServices.value.map((service) => 
service.name))
-    for (const service of selectedServices.value) {
-      const res = await confirmServiceDependencies(service)
-      if (res.length === 0) {
+    for (const selectedService of selectedServices.value) {
+      const serviceDependencies = await 
confirmServiceDependencies(selectedService)
+      if (serviceDependencies.length === 0) {
         return false
       }
-      const filter = res.filter((service) => 
!selectedServiceNames.has(service.name))
-      filter.forEach((service) => {
-        compRef.value.addInstallItem(service)
-        selectedServiceNames.add(service.name)
-      })
+      for (const service of serviceDependencies) {
+        if (!selectedServiceNames.has(service.name)) {
+          compRef.value.addInstallItem(service)
+          selectedServiceNames.add(service.name)
+        }
+      }
     }
     return true
   }
@@ -86,7 +103,7 @@
     if (current.value < 3) {
       ;(await stepValidators[current.value]()) && nextStep()
     } else if (current.value === 3) {
-      const state = await createService()
+      const state = creationModeType.value === 'component' ? await 
addComponentForService() : await createService()
       if (state) {
         nextStep()
       }
@@ -124,7 +141,7 @@
           :is="currComp"
           ref="compRef"
           :is-view="current === 3"
-          v-bind="{ creationMode: $route.params.creationMode || 'internal' }"
+          v-bind="{ ...$route.params, creationMode: $route.params.creationMode 
|| 'internal' }"
         />
       </keep-alive>
       <component-installer v-if="current == stepsLimit" 
:step-data="afterCreateRes" />
@@ -135,7 +152,7 @@
             {{ $t('common.prev') }}
           </a-button>
           <template v-if="current >= 0 && current <= stepsLimit - 1">
-            <a-button type="primary" @click="proceedToNextStep">
+            <a-button :disabled="noUninstallComponent" type="primary" 
@click="proceedToNextStep">
               {{ $t('common.next') }}
             </a-button>
           </template>
diff --git a/bigtop-manager-ui/src/components/job/index.vue 
b/bigtop-manager-ui/src/components/job/index.vue
index dd849522..2bdb7f88 100644
--- a/bigtop-manager-ui/src/components/job/index.vue
+++ b/bigtop-manager-ui/src/components/job/index.vue
@@ -152,7 +152,7 @@
         loading.value = true
       }
       const { api } = apiMap.value[breadcrumbs.value.length - 1]
-      const res = await api(apiParams.value, { ...filtersParams.value, order: 
'desc' })
+      const res = await api(apiParams.value, { ...filtersParams.value, sort: 
'desc' })
       dataSource.value = res.content
       paginationProps.value.total = res.total
     } catch (error) {
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 c10c0920..7e3e4974 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
@@ -56,7 +56,6 @@
     try {
       const data = await getJobDetails({ jobId, clusterId: 0 })
       jobDetail.value = data
-      activeKey.value = data.stages ? data.stages?.map((v) => v.id!) : []
       emits('updateData', { ...stepData.value, ...data })
       return ['Successful', 'Failed'].includes(data.state as StateType)
     } catch (error) {
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
index 7c0f7266..98b0d0a5 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
@@ -239,7 +239,7 @@
             :text-compact="true"
             :space="24"
             :groups="operations"
-            :args="record"
+            :payload="record"
             group-shape="default"
             group-type="link"
           ></button-group>
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 e0e2ba9f..495becf0 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -18,15 +18,20 @@
 -->
 
 <script setup lang="ts">
-  import { TableColumnType, TableProps } from 'ant-design-vue'
+  import { message, Modal, 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 * as hostApi from '@/api/hosts'
+  import { useRouter } from 'vue-router'
   import useBaseTable from '@/composables/use-base-table'
+  import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
+  import InstallDependencies from 
'@/pages/cluster-manage/hosts/install-dependencies.vue'
   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'
+  import type { HostReq } from '@/api/command/types'
 
   type Key = string | number
   interface TableState {
@@ -36,8 +41,11 @@
   }
 
   const { t } = useI18n()
+  const router = useRouter()
   const attrs = useAttrs() as ClusterVO
   const searchInputRef = ref()
+  const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
+  const installRef = ref<InstanceType<typeof InstallDependencies> | null>(null)
   const hostStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
   const state = reactive<TableState>({
     searchText: '',
@@ -120,7 +128,7 @@
     {
       text: 'remove',
       danger: true,
-      clickEvent: (_item, args) => handleDelete(args)
+      clickEvent: (_item, args) => deleteHost([args.id])
     }
   ])
 
@@ -147,16 +155,47 @@
     state.searchText = ''
   }
 
-  const handleEdit = (row: any) => {
-    console.log('row :>> ', row)
+  const handleEdit = (row: HostVO) => {
+    const formatHost = { ...row, displayName: row.clusterDisplayName, 
clusterId: attrs?.id }
+    hostCreateRef.value?.handleOpen('EDIT', formatHost)
   }
 
-  const handleDelete = (row?: any) => {
-    if (!row) {
-      console.log('selectedRowKeys :>> ', state.selectedRowKeys)
-    } else {
-      console.log('row :>> ', row)
+  const bulkRemove = () => {
+    if (state.selectedRowKeys.length === 0) {
+      message.error(t('common.delete_empty'))
+      return
     }
+    deleteHost(state.selectedRowKeys as number[])
+  }
+
+  const deleteHost = (ids: number[]) => {
+    Modal.confirm({
+      title: ids.length > 1 ? t('common.delete_msgs') : t('common.delete_msg'),
+      async onOk() {
+        try {
+          const data = await hostApi.removeHost({ ids })
+          if (data) {
+            message.success(t('common.delete_success'))
+            await getHostList(true)
+          }
+        } catch (error) {
+          console.log('error :>> ', error)
+        }
+      }
+    })
+  }
+
+  const viewHostDetail = (row: HostVO) => {
+    const { id: hostId } = row
+    router.push({ name: 'HostDetail', query: { hostId, clusterId: attrs.id } })
+  }
+
+  const addHost = () => {
+    hostCreateRef.value?.handleOpen('ADD', { clusterId: attrs.id })
+  }
+
+  const afterSetupHostConfig = async (type: 'ADD' | 'EDIT', item: HostReq) => {
+    type === 'ADD' ? installRef.value?.handleOpen(item) : await 
getHostList(true)
   }
 
   const getHostList = async (isReset = false) => {
@@ -195,8 +234,8 @@
     <header>
       <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>
+        <a-button type="primary" danger @click="bulkRemove">{{ 
$t('common.bulk_remove') }}</a-button>
+        <a-button type="primary" @click="addHost">{{ $t('cluster.add_host') 
}}</a-button>
       </a-space>
     </header>
     <a-table
@@ -232,7 +271,7 @@
       </template>
       <template #bodyCell="{ record, column }">
         <template v-if="column.key === 'hostname'">
-          <a-typography-link underline> {{ record.hostname }} 
</a-typography-link>
+          <a-typography-link underline @click="viewHostDetail(record)"> {{ 
record.hostname }} </a-typography-link>
         </template>
         <template v-if="column.key === 'status'">
           <svg-icon style="margin-left: 0" 
:name="hostStatus[record.status].toLowerCase()" />
@@ -244,13 +283,15 @@
             :text-compact="true"
             :space="24"
             :groups="operations"
-            :args="record"
+            :payload="record"
             group-shape="default"
             group-type="link"
           />
         </template>
       </template>
     </a-table>
+    <host-create ref="hostCreateRef" :api-edit-caller="true" 
@on-ok="afterSetupHostConfig" />
+    <install-dependencies ref="installRef" 
@on-install-success="getHostList(true)" />
   </div>
 </template>
 
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
index 35505783..2d852806 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
@@ -21,7 +21,7 @@
   import { computed, onActivated, onDeactivated, reactive, ref, shallowRef, 
useAttrs } from 'vue'
   import { storeToRefs } from 'pinia'
   import { useI18n } from 'vue-i18n'
-  import { useRoute } from 'vue-router'
+  import { useRoute, useRouter } from 'vue-router'
   import { deleteComponent, getComponents } from '@/api/component'
   import { useStackStore } from '@/store/stack'
   import { useJobProgress } from '@/store/job-progress'
@@ -52,8 +52,9 @@
   const jobProgressStore = useJobProgress()
   const stackStore = useStackStore()
   const route = useRoute()
+  const router = useRouter()
   const attrs = useAttrs()
-  const { stacks } = storeToRefs(stackStore)
+  const { stacks, stackRelationMap } = storeToRefs(stackStore)
   const searchInputRef = ref()
   const pollingIntervalId = ref<any>(null)
   const componentStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
@@ -146,21 +147,23 @@
     {
       text: 'start',
       action: 'Start',
+      hidden: (_, args) => 
stackRelationMap.value?.components[`${args.name}`].category === 'client',
       clickEvent: (item, args) => handleTableOperation(item!, args)
     },
     {
       text: 'stop',
       action: 'Stop',
+      hidden: (_, args) => 
stackRelationMap.value?.components[`${args.name}`].category === 'client',
       clickEvent: (item, args) => handleTableOperation(item!, args)
     },
     {
       text: 'restart',
       action: 'Restart',
+      hidden: (_, args) => 
stackRelationMap.value?.components[`${args.name}`].category === 'client',
       clickEvent: (item, args) => handleTableOperation(item!, args)
     },
     {
       text: 'remove',
-      disabled: true,
       danger: true,
       clickEvent: (_, args) => handleDelete(args)
     }
@@ -312,6 +315,16 @@
     }
   }
 
+  const addComponent = () => {
+    const { cluster: clusterId } = route.params
+    const creationMode = clusterId == '0' ? 'public' : 'internal'
+    const routerName = clusterId == '0' ? 'CreateInfraComponent' : 
'CreateComponent'
+    router.push({
+      name: routerName,
+      params: { ...route.params, creationMode, type: 'component' }
+    })
+  }
+
   onActivated(() => {
     startPolling()
   })
@@ -326,7 +339,7 @@
     <header>
       <div class="header-title">{{ $t('common.component') }}</div>
       <div class="list-operation">
-        <a-button type="primary" :disabled="true">{{ $t('common.add', 
[`${$t('common.component')}`]) }}</a-button>
+        <a-button type="primary" @click="addComponent">{{ $t('common.add', 
[`${$t('common.component')}`]) }}</a-button>
         <button-group :groups="batchOperations" group-shape="default" />
       </div>
     </header>
@@ -380,7 +393,7 @@
             :text-compact="true"
             :space="24"
             :groups="operations"
-            :args="record"
+            :payload="record"
             group-shape="default"
             group-type="link"
           />
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
index 3e220a38..3def8642 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
@@ -170,7 +170,7 @@
               :text-compact="true"
               :space="24"
               :groups="operations"
-              :args="record"
+              :payload="record"
               group-shape="default"
               group-type="link"
             />
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
index 144904cb..d02c5f12 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
@@ -24,10 +24,10 @@
   import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
   import { useRoute } from 'vue-router'
+  import { Command } from '@/api/command/types'
   import Overview from './overview.vue'
   import Components from './components.vue'
   import Configs from './configs.vue'
-  import { Command } from '@/api/command/types'
   import type { TabItem } from '@/components/common/main-card/types'
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { ServiceVO } from '@/api/service/types'
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 03534884..be4e1a71 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -183,7 +183,7 @@
           </div>
         </div>
         <div class="item-content" @click.stop>
-          <button-group :auto="true" :space="0" :args="item" 
:groups="actionGroups">
+          <button-group :auto="true" :space="0" :payload="item" 
:groups="actionGroups">
             <template #icon="{ item: groupItem }">
               <svg-icon :name="groupItem.icon || ''" />
             </template>
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 a1e3d01a..45d67fb2 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
@@ -365,7 +365,8 @@
     if (payload) {
       formValue.value = Object.assign(formValue.value, {
         ...payload,
-        authType: `${payload.authType}`
+        authType: `${payload?.authType ?? 1}`,
+        inputType: `${payload?.inputType ?? 1}`
       })
     } else {
       Object.assign(formValue.value, {
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
index 942ee00a..233cccfb 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
@@ -26,7 +26,7 @@
   import * as hostApi from '@/api/hosts'
   import useBaseTable from '@/composables/use-base-table'
   import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
-  import InstallDependencies from './install-dependencies.vue'
+  import InstallDependencies from 
'@/pages/cluster-manage/hosts/install-dependencies.vue'
   import type { FilterConfirmProps, FilterResetProps, TableRowSelection } from 
'ant-design-vue/es/table/interface'
   import type { HostReq } from '@/api/command/types'
   import type { GroupItem } from '@/components/common/button-group/types'
@@ -339,7 +339,7 @@
             :text-compact="true"
             :space="24"
             :groups="operations"
-            :args="record"
+            :payload="record"
             group-shape="default"
             group-type="link"
           ></button-group>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
index 7b161954..559bdbbf 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
@@ -329,7 +329,7 @@
               :text-compact="true"
               :space="24"
               :groups="operations"
-              :args="record"
+              :payload="record"
               group-shape="default"
               group-type="link"
             ></button-group>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
index cd257dfc..c4f7b4f3 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
@@ -116,7 +116,7 @@
   const viewServiceDetail = (payload: ServiceVO) => {
     router.push({
       name: 'InfraServiceDetail',
-      params: { id: 0, service: payload.name, serviceId: payload.id }
+      params: { id: 0, service: payload.name, cluster: 0, serviceId: 
payload.id }
     })
   }
 
@@ -144,7 +144,7 @@
     <div class="infra-body">
       <filter-form :filter-items="filterFormItems" @filter="getServices" />
       <a-empty v-if="services.length == 0" style="width: 100%" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
-      <template v-else>
+      <div v-else class="service-item-wrp">
         <a-card
           v-for="item in services"
           :key="item.name"
@@ -175,14 +175,14 @@
             </div>
           </div>
           <div class="item-content" @click.stop>
-            <button-group :auto="true" :space="0" :args="item" 
:groups="actionGroups">
+            <button-group :auto="true" :space="0" :payload="item" 
:groups="actionGroups">
               <template #icon="{ item: groupItem }">
                 <svg-icon :name="groupItem.icon || ''" />
               </template>
             </button-group>
           </div>
         </a-card>
-      </template>
+      </div>
     </div>
   </a-spin>
 </template>
@@ -280,4 +280,10 @@
   :deep(.ant-dropdown-link) {
     margin-right: 10px;
   }
+
+  .service-item-wrp {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 19px;
+  }
 </style>
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts 
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index d226b313..45a443e2 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -65,7 +65,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'CreateService',
-            path: 
`${RouteExceptions.DYNAMIC_ROUTE_MATCH}/create-service/:creationMode`,
+            path: 
`${RouteExceptions.DYNAMIC_ROUTE_MATCH}/create-service/:creationMode?`,
             component: () => import('@/components/create-service/create.vue'),
             meta: {
               hidden: true
@@ -78,6 +78,14 @@ const routes: RouteRecordRaw[] = [
             meta: {
               hidden: true
             }
+          },
+          {
+            name: 'CreateComponent',
+            path: 
`${RouteExceptions.DYNAMIC_ROUTE_MATCH}/create-component/:service/:serviceId/:creationMode?/:type`,
+            component: () => import('@/components/create-service/create.vue'),
+            meta: {
+              hidden: true
+            }
           }
         ]
       },
@@ -110,12 +118,21 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'InfraServiceDetail',
-            path: 
'create-infra-service/service-detail/:id/:service/:serviceId',
+            path: 
'create-infra-service/service-detail/:id/:cluster/:service/:serviceId',
             component: () => 
import('@/pages/cluster-manage/cluster/service-management/index.vue'),
             meta: {
               hidden: true,
               activeMenu: '/cluster-manage/infrastructures'
             }
+          },
+          {
+            name: 'CreateInfraComponent',
+            path: 
'/create-infra-service/create-infra-component/:id/:cluster/:service/:serviceId/:creationMode/:type',
+            component: () => import('@/components/create-service/create.vue'),
+            meta: {
+              hidden: true,
+              activeMenu: '/cluster-manage/infrastructures'
+            }
           }
         ]
       },
diff --git a/bigtop-manager-ui/src/store/menu/index.ts 
b/bigtop-manager-ui/src/store/menu/index.ts
index 6a31508d..eec8d342 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -88,6 +88,9 @@ export const useMenuStore = defineStore(
       if (route.name === 'ServiceDetail') {
         activeMenu = route.path.split('/').slice(0, -3).join('/')
       }
+      if (route.name === 'CreateComponent') {
+        activeMenu = route.path.split('/').slice(0, -5).join('/')
+      }
       const matchedNames = [RouteExceptions.SPECIAL_ROUTE_NAME, 
RouteExceptions.DEFAULT_ROUTE_NAME] as string[]
       headerSelectedKey.value = route.matched[0].path
 
diff --git a/bigtop-manager-ui/src/store/stack/index.ts 
b/bigtop-manager-ui/src/store/stack/index.ts
index 358f74ab..99acbaa5 100644
--- a/bigtop-manager-ui/src/store/stack/index.ts
+++ b/bigtop-manager-ui/src/store/stack/index.ts
@@ -25,7 +25,7 @@ import type { ServiceConfig, ServiceVO } from 
'@/api/service/types'
 import type { StackVO } from '@/api/stack/types'
 
 export type ExpandServiceVO = ServiceVO & { order: number }
-interface StackRelationMap {
+export interface StackRelationMap {
   services: {
     displayName: string
     stack: string
diff --git a/bigtop-manager-ui/src/styles/index.scss 
b/bigtop-manager-ui/src/styles/index.scss
index fe88f1d9..12dd0203 100644
--- a/bigtop-manager-ui/src/styles/index.scss
+++ b/bigtop-manager-ui/src/styles/index.scss
@@ -74,7 +74,7 @@
 
 .job-progress-notification{
   &:hover{
-    background-color: $color-bg-text-hover;
+    background-color: $color-bg-layout;
     cursor: pointer;
   }
   .ant-notification-notice-close{


Reply via email to