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 2a52fe95 BIGTOP-4430: Implement cardinality check for 
component-to-host assignments (#219)
2a52fe95 is described below

commit 2a52fe954d6e9c29b842f1e0db2116922f95cace
Author: Fdefined <[email protected]>
AuthorDate: Fri May 30 14:46:00 2025 +0800

    BIGTOP-4430: Implement cardinality check for component-to-host assignments 
(#219)
---
 .../components/component-assigner.vue              |  48 ++--
 .../components/service-configurator.vue            |  15 +-
 .../create-service/components/service-selector.vue | 140 ++++-----
 .../components/use-create-service.ts               | 312 ---------------------
 .../src/components/create-service/create.vue       | 107 ++++---
 .../components/service-management/components.vue   |  45 ++-
 .../src/components/service-management/configs.vue  |  24 +-
 .../src/components/service-management/index.vue    |  17 +-
 .../src/components/service-management/overview.vue |  15 +-
 bigtop-manager-ui/src/layouts/index.vue            |   6 +-
 bigtop-manager-ui/src/layouts/sider.vue            |  15 +-
 bigtop-manager-ui/src/locales/en_US/service.ts     |   8 +-
 bigtop-manager-ui/src/locales/zh_CN/service.ts     |   8 +-
 .../src/pages/cluster-manage/cluster/index.vue     |   2 +-
 .../src/pages/cluster-manage/cluster/service.vue   |   2 +-
 .../src/pages/cluster-manage/hosts/create.vue      |   2 +-
 .../src/pages/cluster-manage/hosts/index.vue       |   4 +-
 .../cluster-manage/hosts/install-dependencies.vue  |   7 +-
 .../src/pages/cluster-manage/hosts/overview.vue    |   8 +-
 .../pages/cluster-manage/infrastructures/index.vue |   5 +-
 .../cluster-manage/infrastructures/service.vue     |   2 -
 bigtop-manager-ui/src/router/guard.ts              |   5 +-
 .../src/router/routes/modules/clusters.ts          |  12 +-
 bigtop-manager-ui/src/store/cluster/index.ts       |  63 ++---
 .../src/store/create-service/index.ts              | 304 ++++++++++++++++++++
 .../src/store/create-service/validation.ts         | 105 +++++++
 bigtop-manager-ui/src/store/installed/index.ts     |  89 ------
 bigtop-manager-ui/src/store/job-progress/index.ts  |   8 +-
 bigtop-manager-ui/src/store/menu/helper.ts         |  24 --
 bigtop-manager-ui/src/store/menu/index.ts          |  21 +-
 bigtop-manager-ui/src/store/service/index.ts       |  61 +++-
 bigtop-manager-ui/src/store/stack/index.ts         |  26 +-
 32 files changed, 774 insertions(+), 736 deletions(-)

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 231d354a..9f8664f5 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
@@ -18,14 +18,16 @@
 -->
 
 <script setup lang="ts">
-  import { useI18n } from 'vue-i18n'
   import { computed, onActivated, reactive, ref, shallowRef } from 'vue'
+  import { useI18n } from 'vue-i18n'
   import { TableColumnType, Empty } from 'ant-design-vue'
-  import { HostVO } from '@/api/hosts/types'
   import { getHosts } from '@/api/hosts'
-  import useCreateService from './use-create-service'
+  import { useCreateServiceStore } from '@/store/create-service'
+  import { useServiceStore } from '@/store/service'
+  import { storeToRefs } from 'pinia'
   import useBaseTable from '@/composables/use-base-table'
   import TreeSelector from './tree-selector.vue'
+  import type { HostVO } from '@/api/hosts/types'
   import type { FilterConfirmProps, FilterResetProps, TableRowSelection } from 
'ant-design-vue/es/table/interface'
   import type { Key } from 'ant-design-vue/es/_util/type'
 
@@ -36,6 +38,10 @@
   }
 
   const { t } = useI18n()
+  const createStore = useCreateServiceStore()
+  const serviceStore = useServiceStore()
+  const { stepContext, selectedServices, allComps, allCompsMeta } = 
storeToRefs(createStore)
+
   const searchInputRef = ref()
   const currComp = ref<string>('')
   const fieldNames = shallowRef({
@@ -48,15 +54,6 @@
     searchedColumn: '',
     selectedRowKeys: []
   })
-  const {
-    clusterId,
-    installedStore,
-    allComps,
-    allCompsMeta,
-    creationModeType,
-    selectedServices,
-    updateHostsForComponent
-  } = useCreateService()
 
   const serviceList = computed(() =>
     selectedServices.value.map((v) => ({
@@ -67,7 +64,9 @@
 
   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 installedServices = computed(() =>
+    
serviceStore.getInstalledNamesOrIdsOfServiceByKey(`${stepContext.value.clusterId}`)
+  )
   const hostsOfCurrComp = computed((): HostVO[] => {
     const temp = currComp.value.split('/').at(-1)
     return allComps.value.has(temp!) ? allComps.value.get(temp!)?.hosts ?? [] 
: []
@@ -115,7 +114,8 @@
       return
     }
     try {
-      const res = await getHosts({ ...filtersParams.value, clusterId: 
clusterId.value })
+      const { clusterId } = stepContext.value
+      const res = await getHosts({ ...filtersParams.value, clusterId })
       dataSource.value = res.content.map((v) => ({ ...v, name: v.id, 
displayName: v.hostname }))
       paginationProps.value.total = res.total
       loading.value = false
@@ -133,8 +133,9 @@
   })
 
   const validateHostIsCheck = (host: HostVO) => {
+    const { type } = stepContext.value
     const notAdd = currCompInfoMeta.value?.hosts.findIndex((v) => v.hostname 
=== host.hostname) == -1
-    if (creationModeType.value === 'component') {
+    if (type === 'component') {
       return !currCompInfo.value?.uninstall && !notAdd
     } else {
       return installedServices.value.includes(currComp.value.split('/')[0]) && 
!notAdd
@@ -149,8 +150,21 @@
   }
 
   const onSelectChange: TableRowSelection['onChange'] = (selectedRowKeys, 
selectedRows) => {
-    allComps.value.has(currComp.value.split('/').at(-1)!) && 
updateHostsForComponent(currComp.value, selectedRows)
-    state.selectedRowKeys = selectedRowKeys
+    const { cardinality, displayName } = currCompInfo.value || {}
+    const newCount = selectedRowKeys.length
+    const compKey = currComp.value.split('/').at(-1)!
+    const isInAllComps = allComps.value.has(compKey)
+    const shouldValidate = !!cardinality
+    const isValid = !shouldValidate || 
createStore.validCardinality(cardinality, newCount, displayName || '')
+
+    const isDeselecting = newCount < state.selectedRowKeys.length
+
+    if (isValid || isDeselecting) {
+      state.selectedRowKeys = selectedRowKeys
+      if (isInAllComps) {
+        createStore.setComponentHosts(currComp.value, selectedRows)
+      }
+    }
   }
 
   const resetSelectedRowKeys = (key: string) => {
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 0fb1aa07..150a876c 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
@@ -19,10 +19,12 @@
 
 <script setup lang="ts">
   import { computed, onActivated, onDeactivated, ref, shallowRef, watch } from 
'vue'
+  import { storeToRefs } from 'pinia'
   import { debounce } from 'lodash'
   import { Empty } from 'ant-design-vue'
   import TreeSelector from './tree-selector.vue'
-  import useCreateService from './use-create-service'
+  import { useCreateServiceStore } from '@/store/create-service'
+  import { useServiceStore } from '@/store/service'
   import type { ServiceConfigReq } from '@/api/command/types'
   import type { ComponentVO } from '@/api/component/types'
   import type { Key } from 'ant-design-vue/es/_util/type'
@@ -36,7 +38,9 @@
     isView: false
   })
 
-  const { clusterId, installedStore, selectedServices } = useCreateService()
+  const createStore = useCreateServiceStore()
+  const serviceStore = useServiceStore()
+  const { stepContext, selectedServices } = storeToRefs(createStore)
   const searchStr = ref('')
   const currService = ref<Key>('')
   const configs = ref<ServiceConfigReq[]>([])
@@ -65,15 +69,16 @@
 
   const serviceList = computed(() => selectedServices.value)
   const disabled = computed(() => {
-    return installedStore
-      .getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
+    const clusterId = stepContext.value.clusterId
+    return serviceStore
+      .getInstalledNamesOrIdsOfServiceByKey(`${clusterId}`)
       .includes(currService.value.toString().split('/').at(-1)!)
   })
 
   // const disabled = computed(() => {
   //   return creationModeType.value === 'component'
   //     ? false
-  //     : installedStore
+  //     : serviceStore
   //         .getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
   //         .includes(currService.value.toString().split('/').at(-1)!)
   // })
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 20060470..65636bf2 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
@@ -20,7 +20,10 @@
 <script setup lang="ts">
   import { computed, onActivated, reactive, ref, shallowRef, toRefs } from 
'vue'
   import { usePngImage } from '@/utils/tools'
-  import useCreateService from './use-create-service'
+  import { useStackStore } from '@/store/stack'
+  import { useServiceStore } from '@/store/service'
+  import { useCreateServiceStore } from '@/store/create-service'
+  import { storeToRefs } from 'pinia'
   import type { ExpandServiceVO } from '@/store/stack'
   import type { ComponentVO } from '@/api/component/types.ts'
   import type { ServiceVO } from '@/api/service/types'
@@ -30,28 +33,20 @@
     selectedData: ExpandServiceVO[]
   }
 
+  const stackStore = useStackStore()
+  const createStore = useCreateServiceStore()
+  const serviceStore = useServiceStore()
   const searchStr = ref('')
+  const spinning = ref(false)
   const licenseOfConflictService = shallowRef(['AGPL-3.0', 'GPLv2'])
   const state = reactive<State>({
     isAddableData: [],
     selectedData: []
   })
-  const {
-    clusterId,
-    routeParams,
-    selectedServices,
-    servicesOfInfra,
-    servicesOfExcludeInfra,
-    installedStore,
-    creationMode,
-    creationModeType,
-    confirmServiceDependencies,
-    setDataByCurrent
-  } = useCreateService()
+  const { stepContext, selectedServices, infraServices, excludeInfraServices } 
= storeToRefs(createStore)
   const { isAddableData } = toRefs(state)
-  const checkSelectedServicesOnlyInstalled = computed(
-    () => selectedServices.value.filter((v: ExpandServiceVO) => 
!v.isInstalled).length === 0
-  )
+  const targetServiceName = computed(() => 
serviceStore.serviceFlatMap[stepContext.value.serviceId].name!)
+  const checkIfInstalled = computed(() => selectedServices.value.filter((v) => 
!v.isInstalled).length === 0)
   const filterAddableData = computed(() =>
     isAddableData.value.filter(
       (v) =>
@@ -92,22 +87,22 @@
   const handleInstallItem = (item: ExpandServiceVO, from: ExpandServiceVO[], 
to: ExpandServiceVO[]) => {
     item.components = item.components?.map((v) => ({ ...v, hosts: [] }))
     moveItem(from, to, item)
-    setDataByCurrent(state.selectedData)
+    createStore.updateSelectedService(state.selectedData)
   }
 
-  const addInstallItem = async (item: ExpandServiceVO) => {
-    const items = await confirmServiceDependencies(item)
+  const modifyInstallItems = async (type: 'add' | 'remove', item: 
ExpandServiceVO) => {
+    const items = await createStore.confirmServiceDependencyAction(type, item)
     if (items.length > 0) {
       items.forEach((i) => {
-        handleInstallItem(i, state.isAddableData, state.selectedData)
+        if (type === 'add') {
+          handleInstallItem(i, state.isAddableData, state.selectedData)
+        } else {
+          handleInstallItem(i, state.selectedData, state.isAddableData)
+        }
       })
     }
   }
 
-  const removeInstallItem = (item: ExpandServiceVO) => {
-    handleInstallItem(item, state.selectedData, state.isAddableData)
-  }
-
   const splitSearchStr = (splitStr: string) => {
     return splitStr.toString().split(new 
RegExp(`(?<=${searchStr.value})|(?=${searchStr.value})`, 'i'))
   }
@@ -119,7 +114,8 @@
         if (!acc[name!]) {
           acc[name!] = {
             ...item,
-            hosts: [{ hostname, name: hostname, displayName: hostname }]
+            hosts: [{ hostname, name: hostname, displayName: hostname }],
+            cardinality: 
stackStore.stackRelationMap?.components[item.name!].cardinality
           }
         } else {
           acc[name!].hosts.push({ hostname, name: hostname, displayName: 
hostname })
@@ -130,7 +126,8 @@
   }
 
   const initInstalledServicesDetail = async () => {
-    const detailRes = await 
installedStore.getInstalledServicesDetailByKey(`${clusterId.value}`)
+    const clusterId = stepContext.value.clusterId
+    const detailRes = await 
serviceStore.getInstalledServicesDetailByKey(`${clusterId}`)
     const detailMap = new Map<string, ExpandServiceVO>()
     if (!detailRes) {
       return detailMap
@@ -146,60 +143,77 @@
     }
   }
 
-  const mergeInherentServiceAndInstalledService = (
+  const buildCompleteServiceInfo = (
     installedServiceMap: Map<string, ExpandServiceVO>,
     inherentServices: ServiceVO[]
   ) => {
-    const inherentService = inherentServices.filter((v) => v.name === 
routeParams.value.service)[0]
-    const installedService = { 
...installedServiceMap.get(routeParams.value.service)! }
-    installedService.license = inherentService.license
-    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 inherent = inherentServices.find((v) => v.name === 
targetServiceName.value)!
+    const installed = { ...installedServiceMap.get(targetServiceName.value)! }
+    const mergedComponentsMap = new Map(
+      (installed.components ?? []).map((component) => [component.name, { 
...component }])
+    )
+    for (const component of inherent.components ?? []) {
+      if (!mergedComponentsMap.has(component.name)) {
+        mergedComponentsMap.set(component.name, { ...component, hosts: [], 
uninstall: true })
+      }
+    }
+    installed.components = [...mergedComponentsMap.values()]
+    installed.license = inherent.license
+
+    return installed as ExpandServiceVO
   }
 
-  const addInstalledSymbolForSelectedServices = async (onlyInstalled: boolean) 
=> {
-    if (onlyInstalled) {
-      const installedServiceMap = await initInstalledServicesDetail()
-      const installedServiceNames = 
installedStore.getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
-      const inherentServices = creationMode.value === 'internal' ? 
servicesOfExcludeInfra.value : servicesOfInfra.value
+  const markSelectedAsInstalled = async (hasInstalled: boolean) => {
+    const { type, creationMode, clusterId } = stepContext.value
+    if (!hasInstalled) {
+      state.selectedData = [...selectedServices.value]
+      return
+    }
+    const installedServiceMap = await initInstalledServicesDetail()
+    const installedServiceNames = 
serviceStore.getInstalledNamesOrIdsOfServiceByKey(`${clusterId}`)
+    const rawServices = creationMode === 'internal' ? 
excludeInfraServices.value : infraServices.value
+    // Clone to prevent mutation of original data
+    const inherentServices = rawServices.map((s) => ({ ...s }))
 
-      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!))
+    if (type === 'component' && 
installedServiceMap.has(targetServiceName.value!)) {
+      const completeService = buildCompleteServiceInfo(installedServiceMap, 
inherentServices)
+      state.selectedData[0] = { ...completeService, isInstalled: true }
+      state.isAddableData = []
+    } else {
+      for (const service of inherentServices) {
+        const name = service.name || ''
+        if (installedServiceNames.includes(name)) {
+          const installed = installedServiceMap.get(name)
+          if (installed) {
+            Object.assign(service, installed)
             service.isInstalled = true
-            state.selectedData.push(service as ExpandServiceVO)
-          } else {
-            state.isAddableData.push(service as ExpandServiceVO)
           }
+          state.selectedData.push(service as ExpandServiceVO)
+        } else {
+          state.isAddableData.push(service as ExpandServiceVO)
         }
       }
-      setDataByCurrent(state.selectedData)
-    } else {
-      state.selectedData = [...selectedServices.value]
     }
+
+    createStore.updateSelectedService(state.selectedData)
+    createStore.setTempData(state.selectedData)
   }
 
   onActivated(async () => {
-    await 
addInstalledSymbolForSelectedServices(checkSelectedServicesOnlyInstalled.value)
+    spinning.value = true
+    markSelectedAsInstalled(checkIfInstalled.value).finally(() => {
+      spinning.value = false
+    })
   })
 
   defineExpose({
-    addInstallItem
+    modifyInstallItems
   })
 </script>
 
 <template>
   <div class="service-selector">
-    <div>
+    <a-spin :spinning="spinning">
       <div class="list-title">
         <div>{{ $t('service.select_service') }}</div>
         <a-input v-model:value="searchStr" 
:placeholder="$t('service.please_enter_search_keyword')" />
@@ -208,7 +222,7 @@
         <template #renderItem="{ item }">
           <a-list-item>
             <template #actions>
-              <a-button type="primary" @click="addInstallItem(item)">{{ 
$t('common.add') }}</a-button>
+              <a-button type="primary" @click="modifyInstallItems('add', 
item)">{{ $t('common.add') }}</a-button>
             </template>
             <a-list-item-meta>
               <template #title>
@@ -248,9 +262,9 @@
           </a-list-item>
         </template>
       </a-list>
-    </div>
+    </a-spin>
     <a-divider type="vertical" class="divider" />
-    <div>
+    <a-spin :spinning="spinning">
       <div class="list-title">
         <div>{{ $t('service.pending_installation_services') }}</div>
       </div>
@@ -258,7 +272,7 @@
         <template #renderItem="{ item }">
           <a-list-item>
             <template #actions>
-              <a-button danger :disabled="item.isInstalled" type="primary" 
@click="removeInstallItem(item)">
+              <a-button danger :disabled="item.isInstalled" type="primary" 
@click="modifyInstallItems('remove', item)">
                 {{ $t('common.remove') }}
               </a-button>
             </template>
@@ -290,7 +304,7 @@
           </a-list-item>
         </template>
       </a-list>
-    </div>
+    </a-spin>
   </div>
 </template>
 
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
deleted file mode 100644
index aaaeb25c..00000000
--- 
a/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
+++ /dev/null
@@ -1,312 +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 { computed, ComputedRef, createVNode, effectScope, Ref, ref, watch } 
from 'vue'
-import { message, Modal } from 'ant-design-vue'
-import { useI18n } from 'vue-i18n'
-import { useRoute } from 'vue-router'
-import { ExpandServiceVO, useStackStore } from '@/store/stack'
-import { useInstalledStore } from '@/store/installed'
-import { execCommand } from '@/api/command'
-import useSteps from '@/composables/use-steps'
-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[]>
-let steps: ComputedRef<string[]>
-
-const setupStore = () => {
-  scope.run(() => {
-    selectedServices = ref<ExpandServiceVO[]>([])
-    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(() => [
-      'service.select_service',
-      'service.assign_component',
-      'service.configure_service',
-      'service.service_overview',
-      'service.install_component'
-    ])
-  })
-}
-
-const useCreateService = () => {
-  if (!isChange) {
-    setupStore()
-    isChange = true
-  }
-  const installedStore = useInstalledStore()
-  const route = useRoute()
-  const { t } = useI18n()
-  const processedServices = ref(new Set())
-  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,
-    () => {
-      processedServices.value = new Set(selectedServices.value.map((v) => 
v.name))
-    },
-    {
-      deep: true
-    }
-  )
-
-  const setDataByCurrent = (val: ExpandServiceVO[]) => {
-    selectedServices.value = val
-    selectedServicesMeta.value = JSON.parse(JSON.stringify(val))
-  }
-
-  const updateHostsForComponent = (compName: string, hosts: HostVO[]) => {
-    const [serviceName, componentName] = compName.split('/')
-    const service = selectedServices.value.find((svc) => svc.name === 
serviceName)
-    if (!service) return false
-    const component = service.components?.find((comp) => comp.name === 
componentName)
-    if (!component) return false
-    component.hosts = hosts
-  }
-
-  const transformServiceData = (services: ExpandServiceVO[]) => {
-    return services.map((service) => ({
-      serviceName: service.name,
-      installed: service.isInstalled === undefined ? false : 
service.isInstalled,
-      componentHosts: (service.components || []).map((component) => ({
-        componentName: component.name,
-        hostnames: (component.hosts || []).map((host: HostVO) => host.hostname)
-      })),
-      configs: service.configs
-    })) as ServiceCommandReq[]
-  }
-
-  // Validate services from infra
-  const validServiceFromInfra = (targetService: ExpandServiceVO, 
requiredServices: string[]) => {
-    const servicesOfInfraNames = servicesOfInfra.value.map((v) => v.name)
-    const installedServicesOfInfra = 
installedStore.getInstalledNamesOrIdsOfServiceByKey('0', 'names')
-    const set = new Set(installedServicesOfInfra)
-    const missServices = requiredServices.reduce((acc, name) => {
-      !set.has(name) && servicesOfInfraNames.includes(name) && acc.push(name)
-      return acc
-    }, [] as string[])
-
-    if (missServices.length === 0) return false
-    message.error(t('service.dependencies_conflict_msg', 
[targetService.displayName!, missServices.join(',')]))
-    return true
-  }
-
-  const processDependencies = async (
-    targetService: ExpandServiceVO,
-    serviceMap: Map<string, ExpandServiceVO>,
-    servicesOfInfra: ExpandServiceVO[],
-    collected: ExpandServiceVO[]
-  ): Promise<ProcessResult> => {
-    const dependencies = targetService.requiredServices || []
-
-    if (creationMode.value === 'internal' && 
validServiceFromInfra(targetService, dependencies)) {
-      return {
-        success: false,
-        conflictService: targetService
-      }
-    }
-
-    for (const serviceName of dependencies) {
-      const dependency = serviceMap.get(serviceName)
-      if (!dependency || processedServices.value.has(dependency.name!)) 
continue
-
-      if (dependency.isInstalled) continue
-
-      const shouldAdd = await confirmRequiredServicesToInstall(targetService, 
dependency)
-      if (!shouldAdd) return { success: false }
-
-      collected.push(dependency)
-      processedServices.value.add(dependency.name!)
-      const result = await processDependencies(dependency, serviceMap, 
servicesOfInfra, collected)
-
-      if (!result.success) {
-        collected.splice(collected.indexOf(dependency), 1)
-        processedServices.value.delete(dependency.name!)
-        return result
-      }
-    }
-    return { success: true }
-  }
-
-  const getServiceMap = (services: ServiceVO[]) => {
-    return new Map(services.map((s) => [s.name as string, s as 
ExpandServiceVO]))
-  }
-
-  const handlePreSelectedServiceDependencies = async (preSelectedService: 
ExpandServiceVO) => {
-    const serviceMap =
-      creationMode.value == 'public'
-        ? getServiceMap(servicesOfInfra.value)
-        : getServiceMap(servicesOfExcludeInfra.value)
-
-    const result: ExpandServiceVO[] = []
-    const dependenciesSuccess = await processDependencies(preSelectedService, 
serviceMap, servicesOfInfra.value, result)
-    if (dependenciesSuccess.success) {
-      result.unshift(preSelectedService)
-      return result
-    }
-    return []
-  }
-
-  const confirmServiceDependencies = async (preSelectedService: 
ExpandServiceVO) => {
-    const { requiredServices } = preSelectedService
-    if (!requiredServices) {
-      return [preSelectedService]
-    }
-    if (creationMode.value === 'internal' && 
validServiceFromInfra(preSelectedService, requiredServices)) {
-      return []
-    } else {
-      return await handlePreSelectedServiceDependencies(preSelectedService)
-    }
-  }
-
-  const confirmRequiredServicesToInstall = (targetService: ExpandServiceVO, 
requiredService: ExpandServiceVO) => {
-    return new Promise((resolve) => {
-      Modal.confirm({
-        content: t('service.dependencies_msg', [targetService.displayName, 
requiredService.displayName]),
-        icon: createVNode(SvgIcon, { name: 'unknown' }),
-        cancelText: t('common.no'),
-        okText: t('common.yes'),
-        onOk: () => resolve(true),
-        onCancel: () => {
-          Modal.destroyAll()
-          return resolve(false)
-        }
-      })
-    })
-  }
-
-  const createService = async () => {
-    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)
-      return false
-    }
-  }
-
-  return {
-    steps,
-    clusterId,
-    current,
-    stepsLimit,
-    selectedServices,
-    selectedServicesMeta,
-    servicesOfExcludeInfra,
-    servicesOfInfra,
-    installedStore,
-    allComps,
-    afterCreateRes,
-    scope,
-    creationMode,
-    creationModeType,
-    routeParams,
-    allCompsMeta,
-    setDataByCurrent,
-    addComponentForService,
-    updateHostsForComponent,
-    confirmServiceDependencies,
-    createService,
-    previousStep,
-    nextStep
-  }
-}
-
-export default useCreateService
diff --git a/bigtop-manager-ui/src/components/create-service/create.vue 
b/bigtop-manager-ui/src/components/create-service/create.vue
index e687373b..101434bb 100644
--- a/bigtop-manager-ui/src/components/create-service/create.vue
+++ b/bigtop-manager-ui/src/components/create-service/create.vue
@@ -18,48 +18,28 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onUnmounted, ref, shallowRef } from 'vue'
+  import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
   import { message } from 'ant-design-vue'
   import { useI18n } from 'vue-i18n'
-  import useCreateService from './components/use-create-service'
+  import { storeToRefs } from 'pinia'
+  import { useRoute } from 'vue-router'
+  import { StepContext, useCreateServiceStore } from '@/store/create-service'
   import ServiceSelector from './components/service-selector.vue'
   import ComponentAssigner from './components/component-assigner.vue'
   import ServiceConfigurator from './components/service-configurator.vue'
   import ComponentInstaller from './components/component-installer.vue'
 
   const { t } = useI18n()
-  const components = shallowRef<any[]>([ServiceSelector, ComponentAssigner, 
ServiceConfigurator, ServiceConfigurator])
-  const {
-    scope,
-    current,
-    stepsLimit,
-    steps,
-    allComps,
-    allCompsMeta,
-    creationModeType,
-    afterCreateRes,
-    selectedServices,
-    setDataByCurrent,
-    previousStep,
-    nextStep,
-    createService,
-    confirmServiceDependencies,
-    addComponentForService
-  } = useCreateService()
+  const route = useRoute()
+  const createStore = useCreateServiceStore()
+  const { current, stepsLimit, stepContext, selectedServices, createdPayload } 
= storeToRefs(createStore)
+
   const compRef = ref<any>()
+  const components = shallowRef<any[]>([ServiceSelector, ComponentAssigner, 
ServiceConfigurator, ServiceConfigurator])
   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') {
+    if (stepContext.value.type === 'component') {
       return true
     }
 
@@ -74,13 +54,13 @@
   const validateDependenciesOfServiceSelection = async () => {
     let selectedServiceNames = new Set(selectedServices.value.map((service) => 
service.name))
     for (const selectedService of selectedServices.value) {
-      const serviceDependencies = await 
confirmServiceDependencies(selectedService)
+      const serviceDependencies = await 
createStore.confirmServiceDependencyAction('add', selectedService)
       if (serviceDependencies.length === 0) {
         return false
       }
       for (const service of serviceDependencies) {
         if (!selectedServiceNames.has(service.name)) {
-          compRef.value.addInstallItem(service)
+          compRef.value.modifyInstallItems('add', service)
           selectedServiceNames.add(service.name)
         }
       }
@@ -89,30 +69,52 @@
   }
 
   const validateComponentAssignments = () => {
-    const allComponents = Array.from(allComps.value.values())
-    const isValid = allComponents.every((comp) => comp?.hosts?.length > 0)
-    if (!isValid) {
-      message.error(t('service.component_host_assignment'))
+    let valid = true
+    for (const info of createStore.allComps.values()) {
+      if (!info.cardinality) {
+        continue
+      }
+      valid = createStore.validCardinality(info.cardinality, 
info.hosts.length, info.displayName!)
+      if (!valid) {
+        return
+      }
     }
-    return isValid
+    return valid
   }
 
   const stepValidators = [validateServiceSelection, 
validateComponentAssignments, () => true, () => true]
 
   const proceedToNextStep = async () => {
-    if (current.value < 3) {
-      ;(await stepValidators[current.value]()) && nextStep()
+    const { type } = stepContext.value
+    if (current.value < 3 && (await stepValidators[current.value]())) {
+      createStore.nextStep()
     } else if (current.value === 3) {
-      const state = creationModeType.value === 'component' ? await 
addComponentForService() : await createService()
+      const action = type === 'component' ? 
createStore.attachComponentToService : createStore.createService
+      const state = await action()
       if (state) {
-        nextStep()
+        createStore.nextStep()
       }
     }
   }
 
+  const setupStepCtx = () => {
+    const { id: clusterId, serviceId, creationMode, type } = route.params as 
StepContext
+    createStore.setStepContext({ clusterId, serviceId, creationMode, type })
+  }
+
+  watch(
+    () => route,
+    () => {
+      createStore.$reset()
+      setupStepCtx()
+    },
+    {
+      immediate: true
+    }
+  )
+
   onUnmounted(() => {
-    scope.stop()
-    setDataByCurrent([])
+    createStore.$reset()
   })
 </script>
 
@@ -121,7 +123,7 @@
     <header-card>
       <div class="steps-wrp">
         <a-steps :current="current">
-          <a-step v-for="step in steps" :key="step" :disabled="true">
+          <a-step v-for="step in createStore.steps" :key="step" 
:disabled="true">
             <template #title>
               <span>{{ $t(step) }}</span>
             </template>
@@ -130,29 +132,24 @@
       </div>
     </header-card>
     <main-card>
-      <template v-for="stepItem in steps" :key="stepItem.title">
-        <div v-show="steps[current] === stepItem" class="step-title">
+      <template v-for="stepItem in createStore.steps" :key="stepItem.title">
+        <div v-show="createStore.steps[current] === stepItem" 
class="step-title">
           <h5>{{ $t(stepItem) }}</h5>
           <section :class="{ 'step-content': current < stepsLimit }"></section>
         </div>
       </template>
       <keep-alive>
-        <component
-          :is="currComp"
-          ref="compRef"
-          :is-view="current === 3"
-          v-bind="{ ...$route.params, creationMode: $route.params.creationMode 
|| 'internal' }"
-        />
+        <component :is="currComp" ref="compRef" :is-view="current === 3" />
       </keep-alive>
-      <component-installer v-if="current == stepsLimit" 
:step-data="afterCreateRes" />
+      <component-installer v-if="current == stepsLimit" 
:step-data="createdPayload" />
       <div class="step-action">
         <a-space>
           <a-button v-show="current != stepsLimit" @click="() => 
$router.go(-1)">{{ $t('common.exit') }}</a-button>
-          <a-button v-show="current > 0 && current < stepsLimit" 
type="primary" @click="previousStep">
+          <a-button v-show="current > 0 && current < stepsLimit" 
type="primary" @click="createStore.previousStep">
             {{ $t('common.prev') }}
           </a-button>
           <template v-if="current >= 0 && current <= stepsLimit - 1">
-            <a-button :disabled="noUninstallComponent" type="primary" 
@click="proceedToNextStep">
+            <a-button :disabled="false" type="primary" 
@click="proceedToNextStep">
               {{ $t('common.next') }}
             </a-button>
           </template>
diff --git a/bigtop-manager-ui/src/components/service-management/components.vue 
b/bigtop-manager-ui/src/components/service-management/components.vue
index 2d852806..9fb8539f 100644
--- a/bigtop-manager-ui/src/components/service-management/components.vue
+++ b/bigtop-manager-ui/src/components/service-management/components.vue
@@ -31,6 +31,7 @@
   import type { ComponentVO } from '@/api/component/types'
   import type { FilterConfirmProps, FilterResetProps } from 
'ant-design-vue/es/table/interface'
   import type { Command, CommandRequest } from '@/api/command/types'
+  import type { ServiceVO } from '@/api/service/types'
 
   type Key = string | number
   interface TableState {
@@ -40,20 +41,13 @@
     selectedRows: ComponentVO[]
   }
 
-  interface ServieceInfo {
-    cluster: string
-    id: number
-    service: string
-    serviceId: number
-  }
-
   const POLLING_INTERVAL = 3000
   const { t } = useI18n()
   const jobProgressStore = useJobProgress()
   const stackStore = useStackStore()
   const route = useRoute()
   const router = useRouter()
-  const attrs = useAttrs()
+  const attrs = useAttrs() as unknown as Required<ServiceVO> & { clusterId: 
number }
   const { stacks, stackRelationMap } = storeToRefs(stackStore)
   const searchInputRef = ref()
   const pollingIntervalId = ref<any>(null)
@@ -70,7 +64,6 @@
     selectedRows: []
   })
 
-  const currServiceInfo = computed(() => route.params as unknown as 
ServieceInfo)
   const componentsFromStack = computed(
     () =>
       new Map(
@@ -90,7 +83,7 @@
       key: 'name',
       ellipsis: true,
       filterMultiple: false,
-      filters: 
[...(componentsFromStack.value.get(currServiceInfo.value.service)?.values() || 
[])]?.map((v) => ({
+      filters: [...(componentsFromStack.value.get(attrs.name)?.values() || 
[])]?.map((v) => ({
         text: v?.displayName || '',
         value: v?.name || ''
       }))
@@ -201,14 +194,14 @@
     state.searchText = selectedKeys[0] as string
     state.searchedColumn = dataIndex
     stopPolling()
-    startPolling()
+    startPolling(true, true)
   }
 
   const handleReset = (clearFilters: (param?: FilterResetProps) => void) => {
     clearFilters({ confirm: true })
     state.searchText = ''
     stopPolling()
-    startPolling()
+    startPolling(true, true)
   }
 
   const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key 
}) => {
@@ -241,14 +234,11 @@
 
   const execOperation = async () => {
     try {
-      await jobProgressStore.processCommand(
-        { ...commandRequest.value, clusterId: currServiceInfo.value.id },
-        async () => {
-          getComponentList(true, true)
-          state.selectedRowKeys = []
-          state.selectedRows = []
-        }
-      )
+      await jobProgressStore.processCommand({ ...commandRequest.value, 
clusterId: attrs.clusterId }, async () => {
+        getComponentList(true, true)
+        state.selectedRowKeys = []
+        state.selectedRows = []
+      })
     } catch (error) {
       console.log('error :>> ', error)
     }
@@ -259,7 +249,7 @@
       title: t('common.delete_msg'),
       async onOk() {
         try {
-          const data = await deleteComponent({ clusterId: 
currServiceInfo.value.id, id: row.id! })
+          const data = await deleteComponent({ clusterId: attrs.clusterId, id: 
row.id! })
           if (data) {
             message.success(t('common.delete_success'))
             getComponentList(true, true)
@@ -272,8 +262,8 @@
   }
 
   const getComponentList = async (isReset = false, isFirstCall = false) => {
-    const { id: clusterId, serviceId } = currServiceInfo.value
-    if (attrs.id == undefined || !paginationProps.value) {
+    const { clusterId, id: serviceId } = attrs
+    if (!paginationProps.value) {
       loading.value = false
       return
     }
@@ -301,8 +291,8 @@
     startPolling()
   }
 
-  const startPolling = () => {
-    getComponentList(true, true)
+  const startPolling = (isReset = false, isFirstCall = false) => {
+    getComponentList(isReset, isFirstCall)
     pollingIntervalId.value = setInterval(() => {
       getComponentList()
     }, POLLING_INTERVAL)
@@ -316,9 +306,8 @@
   }
 
   const addComponent = () => {
-    const { cluster: clusterId } = route.params
-    const creationMode = clusterId == '0' ? 'public' : 'internal'
-    const routerName = clusterId == '0' ? 'CreateInfraComponent' : 
'CreateComponent'
+    const creationMode = Number(attrs.clusterId) === 0 ? 'public' : 'internal'
+    const routerName = Number(attrs.clusterId) === 0 ? 'CreateInfraComponent' 
: 'CreateComponent'
     router.push({
       name: routerName,
       params: { ...route.params, creationMode, type: 'component' }
diff --git a/bigtop-manager-ui/src/components/service-management/configs.vue 
b/bigtop-manager-ui/src/components/service-management/configs.vue
index 346e2b89..06f997d7 100644
--- a/bigtop-manager-ui/src/components/service-management/configs.vue
+++ b/bigtop-manager-ui/src/components/service-management/configs.vue
@@ -18,7 +18,7 @@
 -->
 
 <script setup lang="ts">
-  import { onActivated, inject, useAttrs, shallowRef, ref, onDeactivated, 
ComputedRef } from 'vue'
+  import { onActivated, inject, useAttrs, shallowRef, ref, onDeactivated } 
from 'vue'
   import { debounce } from 'lodash'
   import { useI18n } from 'vue-i18n'
   import { Empty, message } from 'ant-design-vue'
@@ -27,16 +27,8 @@
   import SnapshotManagement from './components/snapshot-management.vue'
   import type { Property, ServiceConfig, ServiceVO } from '@/api/service/types'
 
-  interface ServieceInfo {
-    cluster: string
-    id: number
-    service: string
-    serviceId: number
-  }
-
-  const { routeParams }: { routeParams: ComputedRef<ServieceInfo> } = 
inject('service') as any
-  const attrs = useAttrs() as unknown as ServiceVO
   const { t } = useI18n()
+  const attrs = useAttrs() as unknown as Required<ServiceVO> & { clusterId: 
number }
   const getServiceDetail = inject('getServiceDetail') as () => any
   const searchStr = ref('')
   const loading = ref(false)
@@ -99,9 +91,9 @@
 
   const saveConfigs = async () => {
     try {
-      const { serviceId, id: clusterId } = routeParams.value
+      const { id, clusterId } = attrs
       loading.value = true
-      const data = await updateServiceConfigs({ id: serviceId, clusterId }, 
[...configs.value])
+      const data = await updateServiceConfigs({ id, clusterId }, 
[...configs.value])
       if (data) {
         message.success(t('common.update_success'))
         getServiceDetail()
@@ -114,13 +106,13 @@
   }
 
   const onCaptureSnapshot = () => {
-    const { serviceId, id: clusterId } = routeParams.value
-    captureRef.value?.handleOpen({ id: serviceId, clusterId })
+    const { id, clusterId } = attrs
+    captureRef.value?.handleOpen({ id, clusterId })
   }
 
   const openSnapshotManagement = () => {
-    const { serviceId, id: clusterId } = routeParams.value
-    snapshotRef.value?.handleOpen({ id: serviceId, clusterId })
+    const { id, clusterId } = attrs
+    snapshotRef.value?.handleOpen({ id, clusterId })
   }
 
   onActivated(async () => {
diff --git a/bigtop-manager-ui/src/components/service-management/index.vue 
b/bigtop-manager-ui/src/components/service-management/index.vue
index d02c5f12..0cf98bca 100644
--- a/bigtop-manager-ui/src/components/service-management/index.vue
+++ b/bigtop-manager-ui/src/components/service-management/index.vue
@@ -32,10 +32,8 @@
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { ServiceVO } from '@/api/service/types'
 
-  interface ServieceInfo {
-    cluster: string
+  interface RouteParams {
     id: number
-    service: string
     serviceId: number
   }
 
@@ -43,10 +41,10 @@
   const route = useRoute()
   const serviceStore = useServiceStore()
   const jobProgressStore = useJobProgress()
-  const { loading } = storeToRefs(serviceStore)
+  const { loading, serviceMap } = storeToRefs(serviceStore)
   const activeKey = ref('1')
   const serviceDetail = shallowRef<ServiceVO>()
-  const routeParams = computed(() => route.params as unknown as ServieceInfo)
+  const routeParams = computed(() => route.params as unknown as RouteParams)
   const tabs = computed((): TabItem[] => [
     {
       key: '1',
@@ -90,13 +88,15 @@
   })
 
   const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key 
}) => {
+    const { id: clusterId, serviceId } = routeParams.value
+    const serviceName = serviceMap.value[clusterId].filter((service) => 
Number(serviceId) === service.id)[0].name!
     try {
       await jobProgressStore.processCommand(
         {
           command: key as keyof typeof Command,
-          clusterId: routeParams.value.id,
+          clusterId,
           commandLevel: 'service',
-          serviceCommands: [{ serviceName: routeParams.value.service, 
installed: true }]
+          serviceCommands: [{ serviceName, installed: true }]
         },
         getServiceDetail
       )
@@ -118,7 +118,6 @@
   }
 
   provide('getServiceDetail', getServiceDetail)
-  provide('service', { routeParams })
 
   onMounted(() => {
     getServiceDetail()
@@ -136,7 +135,7 @@
     <main-card v-model:active-key="activeKey" :tabs="tabs">
       <template #tab-item>
         <keep-alive>
-          <component :is="getCompName" v-bind="serviceDetail"></component>
+          <component :is="getCompName" v-bind="{ ...serviceDetail, clusterId: 
routeParams.id }"></component>
         </keep-alive>
       </template>
     </main-card>
diff --git a/bigtop-manager-ui/src/components/service-management/overview.vue 
b/bigtop-manager-ui/src/components/service-management/overview.vue
index 9ede9128..6fe460d5 100644
--- a/bigtop-manager-ui/src/components/service-management/overview.vue
+++ b/bigtop-manager-ui/src/components/service-management/overview.vue
@@ -32,7 +32,7 @@
   }
 
   const { t } = useI18n()
-  const attrs = useAttrs()
+  const attrs = useAttrs() as unknown as Required<ServiceVO> & { clusterId: 
number }
   const currTimeRange = ref<TimeRangeText>('15m')
   const chartData = ref({
     chart1: [],
@@ -45,7 +45,6 @@
     2: 'unhealthy',
     3: 'unknown'
   })
-  const serviceDetail = computed(() => attrs as unknown as ServiceVO)
   const serviceKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ServiceVO)[])
   const noChartData = computed(() => Object.values(chartData.value).every((v) 
=> v.length === 0))
   const timeRanges = computed((): TimeRangeItem[] => [
@@ -119,27 +118,27 @@
                         <a-tag
                           v-if="key === 'status'"
                           class="reset-tag"
-                          
:color="CommonStatus[statusColors[serviceDetail[key]]]"
+                          :color="CommonStatus[statusColors[attrs[key]]]"
                         >
-                          <status-dot 
:color="CommonStatus[statusColors[serviceDetail[key]]]" />
-                          {{ serviceDetail[key] && 
$t(`common.${statusColors[serviceDetail[key]]}`) }}
+                          <status-dot 
:color="CommonStatus[statusColors[attrs[key]]]" />
+                          {{ attrs[key] && 
$t(`common.${statusColors[attrs[key]]}`) }}
                         </a-tag>
                         <a-typography-text
                           v-else-if="key === 'stack'"
                           class="desc-sub-item-desc-column"
-                          :content="serviceDetail[key]?.toLowerCase()"
+                          :content="attrs[key]?.toLowerCase()"
                         />
                         <a-typography-text
                           v-else-if="key === 'restartFlag'"
                           class="desc-sub-item-desc-column"
-                          :content="serviceDetail[key] ? $t('common.yes') : 
$t('common.no')"
+                          :content="attrs[key] ? $t('common.yes') : 
$t('common.no')"
                         />
                         <a-typography-text
                           v-else-if="['kerberos', 'metrics'].includes(key)"
                           class="desc-sub-item-desc-column"
                           :content="$t('common.disabled')"
                         />
-                        <a-typography-text v-else 
class="desc-sub-item-desc-column" :content="serviceDetail[key]" />
+                        <a-typography-text v-else 
class="desc-sub-item-desc-column" :content="attrs[key]" />
                       </div>
                     </template>
                   </div>
diff --git a/bigtop-manager-ui/src/layouts/index.vue 
b/bigtop-manager-ui/src/layouts/index.vue
index 11e3604a..a9032d53 100644
--- a/bigtop-manager-ui/src/layouts/index.vue
+++ b/bigtop-manager-ui/src/layouts/index.vue
@@ -24,21 +24,21 @@
 
   import { useUserStore } from '@/store/user'
   import { useMenuStore } from '@/store/menu'
-  import { useInstalledStore } from '@/store/installed'
   import { useClusterStore } from '@/store/cluster'
   import { useStackStore } from '@/store/stack'
+  import { useServiceStore } from '@/store/service'
   import { onMounted } from 'vue'
 
   const userStore = useUserStore()
   const menuStore = useMenuStore()
-  const installedStore = useInstalledStore()
+  const serviceStore = useServiceStore()
   const stackStore = useStackStore()
   const clusterStore = useClusterStore()
 
   onMounted(async () => {
     stackStore.loadStacks()
     await clusterStore.loadClusters()
-    installedStore.getServicesOfInfra()
+    serviceStore.getServicesOfInfra()
     userStore.getUserInfo()
     menuStore.setupMenu()
   })
diff --git a/bigtop-manager-ui/src/layouts/sider.vue 
b/bigtop-manager-ui/src/layouts/sider.vue
index 060a7b06..34a5cb34 100644
--- a/bigtop-manager-ui/src/layouts/sider.vue
+++ b/bigtop-manager-ui/src/layouts/sider.vue
@@ -32,7 +32,7 @@
   const routeParamsLen = ref(0)
   const openKeys = ref<string[]>(['clusters'])
   const { siderMenuSelectedKey, headerSelectedKey, siderMenus, 
routePathFromClusters } = storeToRefs(menuStore)
-  const { clusters, clusterCount } = storeToRefs(clusterStore)
+  const { clusterCount, clusterMap } = storeToRefs(clusterStore)
 
   const showCreateClusterBtn = computed(() => headerSelectedKey.value === 
'/cluster-manage')
   const selectMenuKeyFromClusters = computed(() => 
siderMenuSelectedKey.value.includes(routePathFromClusters.value))
@@ -48,10 +48,13 @@
     () => route,
     (newRoute) => {
       const { params, path, meta } = newRoute
+      const targetCluster = clusterMap.value[`${params.id}`]
       routeParamsLen.value = Object.keys(params).length
       if (path.includes(routePathFromClusters.value) && routeParamsLen.value > 
0 && clusterCount.value > 0) {
-        const cluster = clusters.value.find((v) => `${v.id}` === params.id)
-        cluster && (siderMenuSelectedKey.value = 
`${routePathFromClusters.value}/${cluster.name}/${cluster.id}`)
+        if (targetCluster) {
+          // siderMenuSelectedKey.value = 
`${routePathFromClusters.value}/${targetCluster.name}/${targetCluster.id}`
+          siderMenuSelectedKey.value = 
`${routePathFromClusters.value}/${targetCluster.id}`
+        }
       } else {
         siderMenuSelectedKey.value = meta.activeMenu ?? path
       }
@@ -81,7 +84,7 @@
       v-model="openKeys"
       :selected-keys="[siderMenuSelectedKey]"
       mode="inline"
-      @select="({ key }) => menuStore.onSiderClick(key)"
+      @click="({ key }) => menuStore.onSiderClick(key)"
     >
       <template v-for="menuItem in siderMenus" :key="menuItem.path">
         <a-sub-menu v-if="menuItem.name === 'Clusters'" :key="menuItem.path">
@@ -100,8 +103,8 @@
             <span>{{ $t(menuItem.meta!.title!) }}</span>
           </template>
           <a-menu-item
-            v-for="child in clusters"
-            :key="`${routePathFromClusters}/${child.name}/${child.id}`"
+            v-for="child of clusterMap"
+            :key="`${routePathFromClusters}/${child.id}`"
             :title="child.displayName"
           >
             <template #icon>
diff --git a/bigtop-manager-ui/src/locales/en_US/service.ts 
b/bigtop-manager-ui/src/locales/en_US/service.ts
index b34603db..a64422f3 100644
--- a/bigtop-manager-ui/src/locales/en_US/service.ts
+++ b/bigtop-manager-ui/src/locales/en_US/service.ts
@@ -32,10 +32,14 @@ export default {
   component_host_assignment: 'Assign at least one host for each component',
   service_selection: 'Please select services to install',
   dependencies_conflict_msg: '{0} requires infra service {1} to be installed 
first',
-  dependencies_msg: '{0} requires service {1}, add it also?',
+  dependencies_add_msg: '{0} requires service {1}, add it also?',
+  dependencies_remove_msg: '{0} requires service {1}, remove it also?',
   capture_snapshot: 'Capture Snapshot',
   snapshot_management: 'Snapshot Management',
   snapshot_name: 'Name',
   snapshot_description: 'Description',
-  snapshot_notes: 'Snapshot Notes'
+  snapshot_notes: 'Snapshot Notes',
+  exact: '{0} requires exactly {1} host(s).',
+  range: '{0} requires between {1} and {2} host(s).',
+  minOnly: '{0} requires at least {1} host(s).'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/service.ts 
b/bigtop-manager-ui/src/locales/zh_CN/service.ts
index 515c839e..5fde4be5 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/service.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/service.ts
@@ -32,10 +32,14 @@ export default {
   component_host_assignment: '每个组件至少分配一个主机',
   service_selection: '请选择需要安装的服务',
   dependencies_conflict_msg: '{0} 依赖于基础服务 {1},请先前往安装',
-  dependencies_msg: '{0} 依赖于服务 {1}, 是否一起安装?',
+  dependencies_add_msg: '{0} 依赖于服务 {1}, 是否一起安装?',
+  dependencies_remove_msg: '{0} 依赖于服务 {1}, 是否一起移除?',
   capture_snapshot: '拍摄快照',
   snapshot_management: '快照管理',
   snapshot_name: '快照名',
   snapshot_description: '快照描述',
-  snapshot_notes: '快照备注'
+  snapshot_notes: '快照备注',
+  exact: '{0} 需要 {1} 个主机',
+  range: '{0} 需要 {1} 到 {2} 个主机',
+  minOnly: '{0} 至少需要 {1} 个主机'
 }
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 61784027..5f056391 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
@@ -121,7 +121,7 @@
   }
 
   const addService: GroupItem['clickEvent'] = () => {
-    router.push({ name: 'CreateService', params: { creationMode: 'internal' } 
})
+    router.push({ name: 'CreateService', params: { id: currCluster.value.id, 
creationMode: 'internal' } })
   }
 
   onMounted(() => {
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 03165b57..253b89ef 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -139,7 +139,7 @@
   const viewServiceDetail = (payload: ServiceVO) => {
     router.push({
       name: 'ServiceDetail',
-      params: { service: payload.name, serviceId: payload.id }
+      params: { serviceId: payload.id }
     })
   }
 
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 45d67fb2..027aaf45 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
@@ -380,7 +380,7 @@
 
   const getClusterSelectOptions = async () => {
     await nextTick()
-    const formatClusters = clusterStore.clusters.map((v) => ({ ...v, 
clusterId: v.id }))
+    const formatClusters = Object.values(clusterStore.clusterMap).map((v) => 
({ ...v, clusterId: v.id }))
     autoFormRef.value?.setOptions('clusterId', formatClusters)
   }
 
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 233cccfb..b184f20d 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
@@ -55,7 +55,7 @@
     selectedRowKeys: []
   })
   const filtersOfClusterDisplayName = computed(() =>
-    clusterStore.clusters.map((v) => ({
+    Object.values(clusterStore.clusterMap).map((v) => ({
       text: v.displayName || v.name,
       value: v.id!
     }))
@@ -212,7 +212,7 @@
   }
 
   const editHost = (row: HostVO) => {
-    const cluster = clusterStore.clusters.find((v) => v.name === 
row.clusterName)
+    const cluster = Object.values(clusterStore.clusterMap).find((v) => v.name 
=== row.clusterName)
     const formatHost = { ...row, displayName: row.clusterDisplayName, 
clusterId: cluster?.id }
     hostCreateRef.value?.handleOpen('EDIT', formatHost)
   }
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 559bdbbf..c9c2d034 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
@@ -48,7 +48,7 @@
   const { t } = useI18n()
   const emits = defineEmits<Emits>()
   const clusterStore = useClusterStore()
-  const { clusters } = storeToRefs(clusterStore)
+  const { clusterMap } = storeToRefs(clusterStore)
   const installing = ref(false)
   const open = ref(false)
   const searchInputRef = ref()
@@ -60,7 +60,6 @@
     selectedRowKeys: []
   })
   const allInstallSuccess = computed(() => dataSource.value.every((v) => 
v.status === Status.Success))
-  const clusterNameMap = computed(() => new Map(clusters.value.map((v) => 
[v.id, v])))
   const columns = computed((): TableColumnType<HostReq & { status: string }>[] 
=> [
     {
       title: t('host.hostname'),
@@ -312,8 +311,8 @@
         </template>
         <template #bodyCell="{ record, column }">
           <template v-if="column.key === 'clusterId'">
-            <span 
:title="`${clusterNameMap.get(record.clusterId)?.displayName}`">
-              {{ clusterNameMap.get(record.clusterId)?.displayName }}
+            <span :title="`${clusterMap[record.clusterId]?.displayName}`">
+              {{ clusterMap[record.clusterId]?.displayName }}
             </span>
           </template>
           <template v-if="column.key === 'status'">
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
index ae19d107..54fece85 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -24,9 +24,9 @@
   import { formatFromByte } from '@/utils/storage.ts'
   import { usePngImage } from '@/utils/tools'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state.ts'
+  import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
   import { useStackStore } from '@/store/stack'
-  import { useInstalledStore } from '@/store/installed'
   import { getComponentsByHost } from '@/api/hosts'
   import { Command } from '@/api/command/types'
   import CategoryChart from 
'@/pages/cluster-manage/cluster/components/category-chart.vue'
@@ -50,8 +50,8 @@
   const { hostInfo } = toRefs(props)
   const { t } = useI18n()
   const stackStore = useStackStore()
+  const serviceStore = useServiceStore()
   const jobProgressStore = useJobProgress()
-  const installedStore = useInstalledStore()
   const currTimeRange = ref<TimeRangeText>('15m')
   const statusColors = shallowRef<Record<HostStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
@@ -133,9 +133,9 @@
 
   const handleHostOperate = async (item: MenuItem, component: ComponentVO) => {
     const { serviceName } = component
-    const installedServiceMap = 
Object.values(installedStore.installedServiceMap)
+    const installedServiceMap = Object.values(serviceStore.serviceMap)
       .flat()
-      .filter((v) => v.serviceName === serviceName)
+      .filter((v) => v.name === serviceName)
     if (installedServiceMap.length > 0) {
       try {
         await jobProgressStore.processCommand(
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 1824c0d0..e667fdb3 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
@@ -32,8 +32,7 @@
   const router = useRouter()
   const activeKey = ref('1')
   const currCluster = shallowRef<ClusterVO>({
-    id: 0,
-    name: '0'
+    id: 0
   })
 
   const tabs = computed((): TabItem[] => [
@@ -62,7 +61,7 @@
   })
 
   const addService: GroupItem['clickEvent'] = () => {
-    router.push({ name: 'CreateInfraService', params: { id: 0, cluster: 0, 
creationMode: 'public' } })
+    router.push({ name: 'CreateInfraService', params: { id: 0, creationMode: 
'public' } })
   }
 </script>
 
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue
index 18a0d459..f014ee53 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/service.vue
@@ -119,8 +119,6 @@
       name: 'InfraServiceDetail',
       params: {
         id: clusterInfo.id,
-        service: payload.name,
-        cluster: clusterInfo.name,
         serviceId: payload.id
       }
     })
diff --git a/bigtop-manager-ui/src/router/guard.ts 
b/bigtop-manager-ui/src/router/guard.ts
index 6f7030f9..e907185e 100644
--- a/bigtop-manager-ui/src/router/guard.ts
+++ b/bigtop-manager-ui/src/router/guard.ts
@@ -20,8 +20,9 @@
 import type { NavigationGuardNext, Router } from 'vue-router'
 import { useClusterStore } from '@/store/cluster'
 function setCommonGuard(router: Router) {
-  router.beforeEach(async (to, from, next) => {
-    if (to.name === 'Clusters' && from.name !== 'Login') {
+  const token = localStorage.getItem('Token') ?? 
sessionStorage.getItem('Token') ?? undefined
+  router.beforeEach(async (to, _from, next) => {
+    if (to.name === 'Clusters' && token) {
       checkClusterSelect(next)
     } else {
       next()
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts 
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index 7e5139b9..be915a89 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -50,7 +50,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'ClusterDetail',
-            path: ':cluster/:id',
+            path: ':id',
             component: () => 
import('@/pages/cluster-manage/cluster/index.vue'),
             meta: {
               hidden: true
@@ -66,7 +66,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'CreateService',
-            path: ':cluster/:id/create-service/:creationMode?',
+            path: ':id/create-service/:creationMode?',
             component: () => import('@/components/create-service/create.vue'),
             meta: {
               hidden: true
@@ -74,7 +74,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'ServiceDetail',
-            path: ':cluster/:id/service-detail/:service/:serviceId',
+            path: ':id/service-detail/:serviceId',
             component: () => 
import('@/components/service-management/index.vue'),
             meta: {
               hidden: true
@@ -82,7 +82,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'CreateComponent',
-            path: 
':cluster/:id/create-component/:service/:serviceId/:creationMode?/:type',
+            path: ':id/create-component/:serviceId/:creationMode?/:type',
             component: () => import('@/components/create-service/create.vue'),
             meta: {
               hidden: true
@@ -119,7 +119,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'InfraServiceDetail',
-            path: 
'create-infra-service/service-detail/:id/:cluster/:service/:serviceId',
+            path: 'create-infra-service/service-detail/:id/:serviceId',
             component: () => 
import('@/components/service-management/index.vue'),
             meta: {
               hidden: true,
@@ -128,7 +128,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'CreateInfraComponent',
-            path: 
'/create-infra-service/create-infra-component/:id/:cluster/:service/:serviceId/:creationMode/:type',
+            path: 
'/create-infra-service/create-infra-component/:id/:serviceId/:creationMode/:type',
             component: () => import('@/components/create-service/create.vue'),
             meta: {
               hidden: true,
diff --git a/bigtop-manager-ui/src/store/cluster/index.ts 
b/bigtop-manager-ui/src/store/cluster/index.ts
index b9a77030..c42048dd 100644
--- a/bigtop-manager-ui/src/store/cluster/index.ts
+++ b/bigtop-manager-ui/src/store/cluster/index.ts
@@ -17,51 +17,50 @@
  * under the License.
  */
 
-import { computed, ref, watch } from 'vue'
+import { computed, ref } from 'vue'
 import { defineStore } from 'pinia'
 import { useRoute } from 'vue-router'
 import { getCluster, getClusterList } from '@/api/cluster'
 import { useServiceStore } from '@/store/service'
-import { useInstalledStore } from '@/store/installed'
 import type { ClusterVO } from '@/api/cluster/types.ts'
 
 export const useClusterStore = defineStore(
   'cluster',
   () => {
     const route = useRoute()
-    const installedStore = useInstalledStore()
     const serviceStore = useServiceStore()
-    const clusters = ref<ClusterVO[]>([])
     const loading = ref(false)
+    const clusters = ref<ClusterVO[]>([])
     const currCluster = ref<ClusterVO>({})
+    const clusterMap = ref<Record<string, ClusterVO>>({})
     const clusterId = computed(() => (route.params.id as string) || undefined)
-    const clusterCount = computed(() => clusters.value.length)
+    const clusterCount = computed(() => Object.values(clusterMap.value).length)
 
-    watch(
-      () => clusters.value,
-      (val) => {
-        val.forEach((cluster) => {
-          installedStore.setInstalledMapKey(`${cluster.id}`)
-        })
+    const loadClusters = async () => {
+      try {
+        const clusterList = await getClusterList()
+        clusterMap.value = clusterList.reduce(
+          (pre, cluster) => {
+            pre[cluster.id!] = cluster
+            return pre
+          },
+          {} as Record<string, ClusterVO>
+        )
+      } catch (error) {
+        clusterMap.value = {}
+        console.log('error :>> ', error)
       }
-    )
-
-    const addCluster = async () => {
-      await loadClusters()
-    }
-
-    const delCluster = async () => {
-      await loadClusters()
     }
 
     const getClusterDetail = async () => {
       if (clusterId.value == undefined) {
+        currCluster.value = {}
         return
       }
       try {
         loading.value = true
-        currCluster.value = await getCluster(parseInt(clusterId.value))
-        await serviceStore.getServices(currCluster.value.id!)
+        currCluster.value = await getCluster(Number(clusterId.value))
+        await serviceStore.getServices(Number(clusterId.value))
       } catch (error) {
         currCluster.value = {}
         console.log('error :>> ', error)
@@ -70,30 +69,30 @@ export const useClusterStore = defineStore(
       }
     }
 
-    const loadClusters = async () => {
-      try {
-        clusters.value = await getClusterList()
-      } catch (error) {
-        clusters.value.length = 0
-        console.log('error :>> ', error)
-      }
+    const addCluster = async () => {
+      await loadClusters()
+    }
+
+    const delCluster = async () => {
+      await loadClusters()
     }
 
     return {
       clusters,
+      clusterMap,
       loading,
       currCluster,
       clusterCount,
-      addCluster,
-      delCluster,
       loadClusters,
-      getClusterDetail
+      getClusterDetail,
+      addCluster,
+      delCluster
     }
   },
   {
     persist: {
       storage: sessionStorage,
-      paths: ['clusters']
+      paths: ['clusterMap']
     }
   }
 )
diff --git a/bigtop-manager-ui/src/store/create-service/index.ts 
b/bigtop-manager-ui/src/store/create-service/index.ts
new file mode 100644
index 00000000..385d0919
--- /dev/null
+++ b/bigtop-manager-ui/src/store/create-service/index.ts
@@ -0,0 +1,304 @@
+/*
+ * 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 { computed, ref, shallowRef } from 'vue'
+import { defineStore } from 'pinia'
+import useSteps from '@/composables/use-steps'
+import { useValidations } from './validation'
+import { ExpandServiceVO, useStackStore } from '@/store/stack'
+import { execCommand } from '@/api/command'
+import type { ServiceVO } from '@/api/service/types'
+import type { CommandRequest, CommandVO, ComponentCommandReq, 
ServiceCommandReq } from '@/api/command/types'
+import type { HostVO } from '@/api/hosts/types'
+import type { ComponentVO } from '@/api/component/types'
+
+const STEPS_TITLES = [
+  'service.select_service',
+  'service.assign_component',
+  'service.configure_service',
+  'service.service_overview',
+  'service.install_component'
+]
+
+export interface ProcessResult {
+  success: boolean
+  conflictService?: ExpandServiceVO
+}
+
+export interface StepContext {
+  clusterId: number
+  serviceId: number
+  creationMode: 'internal' | 'public'
+  type?: 'component'
+  [propName: string]: any
+}
+
+export interface CompItem extends ComponentVO {
+  hosts: HostVO[]
+}
+
+export const useCreateServiceStore = defineStore(
+  'service-create',
+  () => {
+    const validations = useValidations()
+    const stackStore = useStackStore()
+
+    const steps = shallowRef(STEPS_TITLES)
+    const selectedServices = ref<ExpandServiceVO[]>([])
+    const selectedServicesMeta = ref<ExpandServiceVO[]>([])
+    const createdPayload = ref<CommandVO>({})
+    const stepContext = ref<StepContext>({
+      clusterId: 0,
+      serviceId: 0,
+      creationMode: 'internal'
+    })
+    const commandRequest = ref<CommandRequest>({
+      command: 'Add',
+      commandLevel: 'service'
+    })
+    const { current, stepsLimit, previousStep, nextStep } = 
useSteps(steps.value)
+    const infraServices = computed(() => 
stackStore.getServicesByExclude(['bigtop', 'extra']) as ExpandServiceVO[])
+    const excludeInfraServices = computed(() => 
stackStore.getServicesByExclude(['infra']))
+    const infraServiceNames = computed(() => infraServices.value.map((v) => 
v.name!))
+    const processedServices = computed(() => new 
Set(selectedServices.value.map((v) => v.name)))
+    const creationMode = computed(() => stepContext.value.creationMode)
+    const currentClusterId = computed(() => (creationMode.value === 'internal' 
? stepContext.value.clusterId : 0))
+
+    const allComps = computed(() => {
+      return new Map(
+        selectedServices.value.flatMap((s) =>
+          s.components!.map((comp) => {
+            return [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>
+    })
+
+    function getServiceMap(services: ServiceVO[]) {
+      return new Map(services.map((s) => [s.name as string, s as 
ExpandServiceVO]))
+    }
+
+    function updateSelectedService(partial: ExpandServiceVO[]) {
+      selectedServices.value = partial
+    }
+
+    function setTempData(partial: ExpandServiceVO[]) {
+      selectedServicesMeta.value = JSON.parse(JSON.stringify(partial))
+    }
+
+    function setStepContext(partial: StepContext) {
+      stepContext.value = partial
+    }
+
+    async function confirmServiceDependencyAction(type: 'add' | 'remove', 
preSelectedService: ExpandServiceVO) {
+      const { requiredServices } = preSelectedService
+      if (!requiredServices && type === 'add') {
+        return [preSelectedService]
+      }
+      const valid = validations.validServiceFromInfra(preSelectedService, 
requiredServices!, infraServiceNames.value)
+      if (type === 'add' && creationMode.value === 'internal' && valid) {
+        return []
+      } else {
+        return await handleServiceDependencyConfirm(type, preSelectedService)
+      }
+    }
+
+    async function handleServiceDependencyConfirm(type: 'add' | 'remove', 
preSelectedService: ExpandServiceVO) {
+      let dependenciesSuccess: ProcessResult = { success: false }
+      const result: ExpandServiceVO[] = []
+      if (type === 'add') {
+        const target = creationMode.value === 'public' ? infraServices.value : 
excludeInfraServices.value
+        const serviceMap = getServiceMap(target)
+        dependenciesSuccess = await processDependencies(preSelectedService, 
serviceMap, infraServices.value, result)
+      } else {
+        dependenciesSuccess = await notifyDependents(preSelectedService, 
result)
+      }
+      if (dependenciesSuccess.success) {
+        result.unshift(preSelectedService)
+        return result
+      }
+      return []
+    }
+
+    async function notifyDependents(
+      targetService: ExpandServiceVO,
+      collected: ExpandServiceVO[]
+    ): Promise<ProcessResult> {
+      for (const service of selectedServices.value) {
+        if (!service.requiredServices?.includes(targetService.name!)) continue
+
+        const shouldRemove = await 
validations.confirmDependencyAddition('remove', service, targetService)
+        if (!shouldRemove) return { success: false }
+        collected.push(service)
+
+        const result = await notifyDependents(service, collected)
+        if (!result.success) {
+          collected.splice(collected.indexOf(service), 1)
+          processedServices.value.delete(service.name!)
+          return result
+        }
+      }
+      return { success: true }
+    }
+
+    async function processDependencies(
+      targetService: ExpandServiceVO,
+      serviceMap: Map<string, ExpandServiceVO>,
+      servicesOfInfra: ExpandServiceVO[],
+      collected: ExpandServiceVO[]
+    ): Promise<ProcessResult> {
+      const dependencies = targetService.requiredServices || []
+
+      const valid = validations.validServiceFromInfra(targetService, 
dependencies, infraServiceNames.value)
+      if (creationMode.value === 'internal' && valid) {
+        return { success: false, conflictService: targetService }
+      }
+
+      for (const serviceName of dependencies) {
+        const dependency = serviceMap.get(serviceName)
+        if (!dependency || processedServices.value.has(dependency.name!)) 
continue
+
+        if (dependency.isInstalled) continue
+
+        const shouldAdd = await validations.confirmDependencyAddition('add', 
targetService, dependency)
+        if (!shouldAdd) return { success: false }
+
+        collected.push(dependency)
+        processedServices.value.add(dependency.name!)
+        const result = await processDependencies(dependency, serviceMap, 
servicesOfInfra, collected)
+        if (!result.success) {
+          collected.splice(collected.indexOf(dependency), 1)
+          processedServices.value.delete(dependency.name!)
+          return result
+        }
+      }
+      return { success: true }
+    }
+
+    function formatServiceData(services: ExpandServiceVO[]) {
+      return services.map((service) => ({
+        serviceName: service.name,
+        installed: service.isInstalled === undefined ? false : 
service.isInstalled,
+        componentHosts: (service.components || []).map((component) => ({
+          componentName: component.name,
+          hostnames: (component.hosts || []).map((host: HostVO) => 
host.hostname)
+        })),
+        configs: service.configs
+      })) as ServiceCommandReq[]
+    }
+
+    function formatComponentData(components: Map<string, CompItem>) {
+      const componentCommands = [] as ComponentCommandReq[]
+      for (const [compName, comp] of components) {
+        componentCommands?.push({
+          componentName: compName!,
+          hostnames: comp.hosts.map((v) => v.hostname!)
+        })
+      }
+      return componentCommands
+    }
+
+    function setComponentHosts(compName: string, hosts: HostVO[]) {
+      const [serviceName, componentName] = compName.split('/')
+      const service = selectedServices.value.find((svc) => svc.name === 
serviceName)
+      if (!service) return false
+      const component = service.components?.find((comp) => comp.name === 
componentName)
+      if (!component) return false
+      component.hosts = hosts
+    }
+
+    async function createService() {
+      try {
+        commandRequest.value.serviceCommands = 
formatServiceData(selectedServices.value)
+        createdPayload.value = await execCommand({ ...commandRequest.value, 
clusterId: currentClusterId.value })
+        Object.assign(createdPayload.value, { clusterId: 
currentClusterId.value })
+        return true
+      } catch (error) {
+        console.log('error :>> ', error)
+        return false
+      }
+    }
+
+    async function attachComponentToService() {
+      try {
+        commandRequest.value.commandLevel = 'component'
+        commandRequest.value.componentCommands = 
formatComponentData(allComps.value)
+        createdPayload.value = await execCommand({ ...commandRequest.value, 
clusterId: currentClusterId.value })
+        Object.assign(createdPayload.value, { clusterId: 
currentClusterId.value })
+        return true
+      } catch (error) {
+        console.log('error :>> ', error)
+        return false
+      }
+    }
+
+    function $reset() {
+      current.value = 0
+      selectedServices.value = []
+      selectedServicesMeta.value = []
+      createdPayload.value = {}
+      stepContext.value = {
+        clusterId: 0,
+        serviceId: 0,
+        creationMode: 'internal'
+      }
+      commandRequest.value = {
+        command: 'Add',
+        commandLevel: 'service'
+      }
+    }
+
+    return {
+      steps,
+      selectedServices,
+      stepContext,
+      infraServices,
+      excludeInfraServices,
+      current,
+      stepsLimit,
+      nextStep,
+      previousStep,
+      allComps,
+      allCompsMeta,
+      updateSelectedService,
+      setStepContext,
+      setTempData,
+      confirmServiceDependencyAction,
+      setComponentHosts,
+      createService,
+      createdPayload,
+      attachComponentToService,
+      $reset,
+      validCardinality: validations.validCardinality
+    }
+  },
+  {
+    persist: false
+  }
+)
diff --git a/bigtop-manager-ui/src/store/create-service/validation.ts 
b/bigtop-manager-ui/src/store/create-service/validation.ts
new file mode 100644
index 00000000..cf5c361e
--- /dev/null
+++ b/bigtop-manager-ui/src/store/create-service/validation.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 { message, Modal } from 'ant-design-vue'
+import { createVNode } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useServiceStore } from '@/store/service'
+import SvgIcon from '@/components/common/svg-icon/index.vue'
+import type { ExpandServiceVO } from '@/store/stack'
+
+export function useValidations() {
+  const { t } = useI18n()
+  const serviceStore = useServiceStore()
+
+  // Validate services from infra
+  function validServiceFromInfra(
+    targetService: ExpandServiceVO,
+    requiredServices: string[],
+    infraServiceNames: string[]
+  ) {
+    const installedInfra = 
serviceStore.getInstalledNamesOrIdsOfServiceByKey('0', 'names')
+    const set = new Set(installedInfra)
+    const missServices = requiredServices.reduce((acc, name) => {
+      !set.has(name) && infraServiceNames.includes(name) && acc.push(name)
+      return acc
+    }, [] as string[])
+
+    if (missServices.length === 0) return false
+    message.error(t('service.dependencies_conflict_msg', 
[targetService.displayName!, missServices.join(',')]))
+    return true
+  }
+
+  function confirmDependencyAddition(
+    type: 'add' | 'remove',
+    targetService: ExpandServiceVO,
+    requiredService: ExpandServiceVO
+  ) {
+    const content = type === 'add' ? 'dependencies_add_msg' : 
'dependencies_remove_msg'
+    return new Promise((resolve) => {
+      Modal.confirm({
+        content: t(`service.${content}`, [targetService.displayName, 
requiredService.displayName]),
+        icon: createVNode(SvgIcon, { name: 'unknown' }),
+        cancelText: t('common.no'),
+        okText: t('common.yes'),
+        onOk: () => resolve(true),
+        onCancel: () => {
+          Modal.destroyAll()
+          return resolve(false)
+        }
+      })
+    })
+  }
+
+  function validCardinality(cardinality: string, count: number, displayName: 
string): boolean {
+    if (/^\d+$/.test(cardinality)) {
+      const expected = parseInt(cardinality, 10)
+      if (count != expected) {
+        message.error(t('service.exact', [displayName, expected]))
+        return false
+      }
+    }
+
+    if (/^\d+-\d+$/.test(cardinality)) {
+      const [minStr, maxStr] = cardinality.split('-')
+      const min = parseInt(minStr, 10)
+      const max = parseInt(maxStr, 10)
+      if (count < min || count > max) {
+        message.error(t('service.range', [displayName, min, max]))
+        return false
+      }
+    }
+
+    if (/^\d+\+$/.test(cardinality)) {
+      const min = parseInt(cardinality.slice(0, -1), 10)
+      if (count < min) {
+        message.error(t('service.minOnly', [displayName, min]))
+        return false
+      }
+    }
+
+    return true
+  }
+
+  return {
+    confirmDependencyAddition,
+    validCardinality,
+    validServiceFromInfra
+  }
+}
diff --git a/bigtop-manager-ui/src/store/installed/index.ts 
b/bigtop-manager-ui/src/store/installed/index.ts
deleted file mode 100644
index bb404d47..00000000
--- a/bigtop-manager-ui/src/store/installed/index.ts
+++ /dev/null
@@ -1,89 +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 { defineStore } from 'pinia'
-import { ref } from 'vue'
-import { useServiceStore } from '@/store/service'
-import type { ServiceVO } from '@/api/service/types.ts'
-
-export interface InstalledMapItem {
-  serviceId: number
-  serviceName: string
-  serviceDisplayName: string
-  clusterId: number
-}
-
-export const useInstalledStore = defineStore(
-  'installed',
-  () => {
-    const serviceStore = useServiceStore()
-    const installedServiceMap = ref<Record<string, InstalledMapItem[]>>({})
-
-    const getInstalledNamesOrIdsOfServiceByKey = (key: string, flag: 'names' | 
'ids' = 'names') => {
-      return installedServiceMap.value[key].map((value) => {
-        if (flag === 'ids') {
-          return value.serviceId
-        } else {
-          return value.serviceName
-        }
-      })
-    }
-
-    const setInstalledMapKey = (key: string) => {
-      installedServiceMap.value[key] = []
-    }
-
-    const setInstalledMapKeyOfValue = (key: string, value: InstalledMapItem[]) 
=> {
-      installedServiceMap.value[key] = value
-    }
-
-    const getServicesOfInfra = async () => {
-      await serviceStore.getServices(0)
-    }
-
-    const getInstalledServicesDetailByKey = async (key: string): 
Promise<ServiceVO[] | undefined> => {
-      try {
-        const serviceIds = getInstalledNamesOrIdsOfServiceByKey(key, 'ids')
-        const allDetail = serviceIds?.map((id) =>
-          serviceStore.getServiceDetail(Number(key), Number(id))
-        ) as Promise<ServiceVO>[]
-        return await Promise.all(allDetail)
-      } catch (error) {
-        console.log(error)
-      }
-    }
-
-    return {
-      serviceStore,
-      installedServiceMap,
-      getServicesOfInfra,
-      setInstalledMapKey,
-      setInstalledMapKeyOfValue,
-      getInstalledNamesOrIdsOfServiceByKey,
-      getInstalledServicesDetailByKey
-    }
-  },
-  {
-    persist: {
-      storage: sessionStorage,
-      key: 'installed',
-      paths: ['installedServiceMap']
-    }
-  }
-)
diff --git a/bigtop-manager-ui/src/store/job-progress/index.ts 
b/bigtop-manager-ui/src/store/job-progress/index.ts
index 597ad2b8..e2bf4d1d 100644
--- a/bigtop-manager-ui/src/store/job-progress/index.ts
+++ b/bigtop-manager-ui/src/store/job-progress/index.ts
@@ -90,12 +90,8 @@ export const useJobProgress = defineStore('job-progress', () 
=> {
   }))
 
   const getClusterDisplayName = (clusterId: number) => {
-    const clusters = clusterStore.clusters
-    const index = clusters.findIndex((v) => v.id == clusterId)
-    if (index != -1) {
-      return clusters[index].displayName
-    }
-    return 'Global'
+    const targetCluster = clusterStore.clusterMap[clusterId]
+    return targetCluster ? targetCluster.displayName : 'Global'
   }
 
   const createStateIcon = (execRes: CommandRes) => {
diff --git a/bigtop-manager-ui/src/store/menu/helper.ts 
b/bigtop-manager-ui/src/store/menu/helper.ts
deleted file mode 100644
index 69810f3b..00000000
--- a/bigtop-manager-ui/src/store/menu/helper.ts
+++ /dev/null
@@ -1,24 +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 { MenuItem } from './types'
-
-export const findActivePath = (menu: MenuItem): string | undefined => {
-  return menu?.children && menu?.children.length > 0 ? 
findActivePath(menu.children[0]) : menu?.key
-}
diff --git a/bigtop-manager-ui/src/store/menu/index.ts 
b/bigtop-manager-ui/src/store/menu/index.ts
index ab265c16..1f0ed985 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { nextTick, ref, shallowRef } from 'vue'
+import { computed, nextTick, ref, shallowRef } from 'vue'
 import { RouteRecordRaw, useRoute, useRouter } from 'vue-router'
 import { dynamicRoutes as dr } from '@/router/routes/index'
 import { defineStore } from 'pinia'
@@ -35,6 +35,7 @@ export const useMenuStore = defineStore(
     const headerSelectedKey = ref()
     const siderMenuSelectedKey = ref()
     const routePathFromClusters = shallowRef('/cluster-manage/clusters')
+    const clusterList = computed(() => Object.values(clusterStore.clusterMap))
 
     const buildMenuMap = () => {
       baseRoutesMap.value = dr.reduce((buildRes, { path, name, meta, children 
}) => {
@@ -46,17 +47,17 @@ export const useMenuStore = defineStore(
     const setupHeader = () => {
       headerSelectedKey.value = route.matched[0].path ?? '/cluster-manage'
       headerMenus.value = Object.values(baseRoutesMap.value)
-      siderMenus.value = baseRoutesMap.value[headerSelectedKey.value].children 
|| []
+      siderMenus.value = 
baseRoutesMap.value[headerSelectedKey.value]?.children || []
     }
 
     const setupSider = () => {
-      siderMenus.value = baseRoutesMap.value[headerSelectedKey.value].children 
|| []
-      if (siderMenus.value[0].redirect) {
+      siderMenus.value = 
baseRoutesMap.value[headerSelectedKey.value]?.children || []
+      if (siderMenus.value[0]?.redirect) {
         siderMenuSelectedKey.value = siderMenus.value[0].redirect
       } else {
-        if (clusterStore.clusters[0]) {
-          const { id, name } = clusterStore.clusters[0]
-          onSiderClick(`${routePathFromClusters.value}/${name}/${id}`)
+        if (clusterList.value.length > 0) {
+          const { id } = clusterList.value[0]
+          onSiderClick(`${routePathFromClusters.value}/${id}`)
         } else {
           onSiderClick(`${routePathFromClusters.value}/default`)
         }
@@ -75,9 +76,9 @@ export const useMenuStore = defineStore(
 
     const updateSider = async () => {
       await clusterStore.loadClusters()
-      const { id, name } = clusterStore.clusters[clusterStore.clusterCount - 1]
+      const { id } = clusterList.value[clusterList.value.length - 1]
       await nextTick()
-      onSiderClick(`${routePathFromClusters.value}/${name}/${id}`)
+      onSiderClick(`${routePathFromClusters.value}/${id}`)
     }
 
     const setupMenu = async () => {
@@ -100,7 +101,7 @@ export const useMenuStore = defineStore(
   },
   {
     persist: {
-      storage: sessionStorage,
+      storage: localStorage,
       paths: ['headerMenus', 'siderMenus']
     }
   }
diff --git a/bigtop-manager-ui/src/store/service/index.ts 
b/bigtop-manager-ui/src/store/service/index.ts
index 3eb04458..1c233cdb 100644
--- a/bigtop-manager-ui/src/store/service/index.ts
+++ b/bigtop-manager-ui/src/store/service/index.ts
@@ -21,15 +21,14 @@ import { defineStore, storeToRefs } from 'pinia'
 import { computed, ref } from 'vue'
 import { getService, getServiceList } from '@/api/service'
 import { useStackStore } from '@/store/stack'
-import { InstalledMapItem, useInstalledStore } from '@/store/installed'
 import type { ServiceListParams, ServiceVO } from '@/api/service/types'
 
 export const useServiceStore = defineStore(
   'service',
   () => {
     const stackStore = useStackStore()
-    const installedStore = useInstalledStore()
     const services = ref<ServiceVO[]>([])
+    const serviceMap = ref<Record<string, (ServiceVO & { clusterId: number 
})[]>>({})
     const total = ref(0)
     const loading = ref(false)
     const { stacks } = storeToRefs(stackStore)
@@ -40,20 +39,26 @@ export const useServiceStore = defineStore(
       )
     })
 
+    const serviceFlatMap = computed(() => {
+      const result: Record<string, ServiceVO> = {}
+
+      for (const services of Object.values(serviceMap.value)) {
+        for (const service of services) {
+          result[service.id!] = service
+        }
+      }
+      return result
+    })
+
     const getServices = async (clusterId: number, filterParams?: 
ServiceListParams) => {
       try {
         loading.value = true
         const data = await getServiceList(clusterId, { ...filterParams, 
pageNum: 1, pageSize: 100 })
         services.value = data.content
         total.value = data.total
-        const serviceMap = services.value.map((v) => ({
-          serviceId: v.id,
-          serviceName: v.name,
-          serviceDisplayName: v.displayName,
-          clusterId: clusterId
-        })) as InstalledMapItem[]
-        installedStore.setInstalledMapKeyOfValue(`${clusterId}`, serviceMap)
+        serviceMap.value[clusterId] = data.content.map((service) => ({ 
...service, clusterId }))
       } catch (error) {
+        serviceMap.value = {}
         console.log('error :>> ', error)
       } finally {
         loading.value = false
@@ -68,16 +73,48 @@ export const useServiceStore = defineStore(
       }
     }
 
+    const getServicesOfInfra = async () => {
+      await getServices(0)
+    }
+
+    const getInstalledNamesOrIdsOfServiceByKey = (key: string, flag: 'names' | 
'ids' = 'names') => {
+      return Object.values(serviceMap.value[key] || {}).map((service: 
ServiceVO) => {
+        if (flag === 'ids') {
+          return service.id
+        } else {
+          return service.name
+        }
+      })
+    }
+
+    const getInstalledServicesDetailByKey = async (key: string): 
Promise<ServiceVO[] | undefined> => {
+      try {
+        const serviceIds = getInstalledNamesOrIdsOfServiceByKey(key, 'ids')
+        const allDetail = serviceIds?.map((id) => 
getServiceDetail(Number(key), Number(id))) as Promise<ServiceVO>[]
+        return await Promise.all(allDetail)
+      } catch (error) {
+        console.log(error)
+      }
+    }
+
     return {
+      serviceMap,
       services,
       loading,
+      serviceNames,
+      locateStackWithService,
+      serviceFlatMap,
       getServices,
       getServiceDetail,
-      serviceNames,
-      locateStackWithService
+      getServicesOfInfra,
+      getInstalledServicesDetailByKey,
+      getInstalledNamesOrIdsOfServiceByKey
     }
   },
   {
-    persist: false
+    persist: {
+      storage: sessionStorage,
+      paths: ['serviceMap']
+    }
   }
 )
diff --git a/bigtop-manager-ui/src/store/stack/index.ts 
b/bigtop-manager-ui/src/store/stack/index.ts
index 99acbaa5..30aeda34 100644
--- a/bigtop-manager-ui/src/store/stack/index.ts
+++ b/bigtop-manager-ui/src/store/stack/index.ts
@@ -25,17 +25,20 @@ import type { ServiceConfig, ServiceVO } from 
'@/api/service/types'
 import type { StackVO } from '@/api/stack/types'
 
 export type ExpandServiceVO = ServiceVO & { order: number }
+export type ServiceMap = {
+  displayName: string
+  stack: string
+  components: string[]
+  configs: ServiceConfig
+  requiredServices: string[]
+}
+export type ComponentMap = {
+  service: string
+  stack: string
+} & ComponentVO
 export interface StackRelationMap {
-  services: {
-    displayName: string
-    stack: string
-    components: string[]
-    configs: ServiceConfig
-  }
-  components: {
-    service: string
-    stack: string
-  } & ComponentVO
+  services: ServiceMap
+  components: ComponentMap
 }
 
 export const useStackStore = defineStore(
@@ -62,7 +65,8 @@ export const useStackStore = defineStore(
             displayName,
             stack: stackName,
             components: components!.map(({ name }) => name),
-            configs
+            configs,
+            requiredServices: service.requiredServices
           }
           for (const component of components!) {
             relationMap.components[component.name!] = {

Reply via email to