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 b207d9e0 BIGTOP-4390: Add service creation page for infra (#190)
b207d9e0 is described below

commit b207d9e032e596192bc10fb2b3ef3c3109b75b90
Author: Fdefined <[email protected]>
AuthorDate: Fri Mar 21 11:52:24 2025 +0800

    BIGTOP-4390: Add service creation page for infra (#190)
---
 bigtop-manager-ui/src/api/hosts/types.ts           |  4 +-
 bigtop-manager-ui/src/api/service/types.ts         |  1 +
 .../components/component-assigner.vue              | 38 +++++++---
 .../components/component-installer.vue             |  8 +-
 .../components/service-configurator.vue            | 22 ++++--
 .../create-service/components/service-selector.vue | 87 ++++++++++++++++++++--
 .../create-service/components/tree-selector.vue    |  1 +
 .../components/use-create-service.ts               | 56 +++++++++-----
 .../src/components/create-service/create.vue       | 20 +++--
 .../src/composables/use-base-table.ts              |  5 +-
 bigtop-manager-ui/src/layouts/header.vue           |  1 +
 bigtop-manager-ui/src/layouts/index.vue            | 13 +++-
 bigtop-manager-ui/src/locales/en_US/cluster.ts     |  2 +-
 bigtop-manager-ui/src/locales/en_US/service.ts     |  2 +-
 bigtop-manager-ui/src/locales/zh_CN/cluster.ts     |  2 +-
 .../cluster/components/host-config.vue             |  4 +-
 .../cluster/components/set-source.vue              |  5 +-
 .../src/pages/cluster-manage/cluster/create.vue    | 28 +++----
 .../src/pages/cluster-manage/cluster/host.vue      |  2 +-
 .../src/pages/cluster-manage/cluster/index.vue     | 10 +--
 .../src/pages/cluster-manage/cluster/service.vue   | 14 ++--
 .../pages/cluster-manage/infrastructures/index.vue |  7 ++
 .../src/router/routes/modules/clusters.ts          | 11 ++-
 bigtop-manager-ui/src/store/cluster/index.ts       | 19 +++--
 bigtop-manager-ui/src/store/installed/index.ts     | 75 +++++++++++++++++++
 bigtop-manager-ui/src/store/menu/index.ts          |  2 +-
 bigtop-manager-ui/src/store/service/index.ts       | 19 ++++-
 bigtop-manager-ui/src/store/stack/index.ts         |  3 +-
 28 files changed, 354 insertions(+), 107 deletions(-)

diff --git a/bigtop-manager-ui/src/api/hosts/types.ts 
b/bigtop-manager-ui/src/api/hosts/types.ts
index 06e67b0a..dfbd53e6 100644
--- a/bigtop-manager-ui/src/api/hosts/types.ts
+++ b/bigtop-manager-ui/src/api/hosts/types.ts
@@ -91,12 +91,12 @@ export enum Status {
   Installing = 'INSTALLING',
   Success = 'SUCCESS',
   Failed = 'FAILED',
-  Unknown = 'UNKNOW'
+  Unknown = 'UNKNOWN'
 }
 
 export enum BaseStatus {
   'INSTALLING',
   'SUCCESS',
   'FAILED',
-  'UNKNOW'
+  'UNKNOWN'
 }
diff --git a/bigtop-manager-ui/src/api/service/types.ts 
b/bigtop-manager-ui/src/api/service/types.ts
index 9ecd25a8..7d81c6aa 100644
--- a/bigtop-manager-ui/src/api/service/types.ts
+++ b/bigtop-manager-ui/src/api/service/types.ts
@@ -44,6 +44,7 @@ export interface ServiceVO {
   status: ServiceStatusType
   user?: string
   version?: string
+  isInstalled?: boolean
 }
 
 export interface ServiceConfig {
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 2175959c..df9574b1 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
@@ -19,14 +19,13 @@
 
 <script setup lang="ts">
   import { useI18n } from 'vue-i18n'
-  import { useRoute } from 'vue-router'
   import { computed, onActivated, reactive, ref, shallowRef } from 'vue'
   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 TreeSelector from './tree-selector.vue'
   import useBaseTable from '@/composables/use-base-table'
+  import TreeSelector from './tree-selector.vue'
   import type { FilterConfirmProps, FilterResetProps, TableRowSelection } from 
'ant-design-vue/es/table/interface'
   import type { Key } from 'ant-design-vue/es/_util/type'
 
@@ -37,7 +36,6 @@
   }
 
   const { t } = useI18n()
-  const route = useRoute()
   const searchInputRef = ref()
   const currComp = ref<string>('')
   const fieldNames = shallowRef({
@@ -50,8 +48,14 @@
     searchedColumn: '',
     selectedRowKeys: []
   })
-  const { allComps, selectedServices, updateHostsForComponent } = 
useCreateService()
-  const serviceList = computed(() => selectedServices.value.map((v) => ({ 
...v, selectable: false })))
+  const { clusterId, installedStore, allComps, selectedServices, 
updateHostsForComponent } = useCreateService()
+  const serviceList = computed(() =>
+    selectedServices.value.map((v) => ({
+      ...v,
+      selectable: false
+    }))
+  )
+
   const hostsOfCurrComp = computed((): HostVO[] => {
     const temp = currComp.value.split('/').at(-1)
     return allComps.value.has(temp) ? allComps.value.get(temp)?.hosts : []
@@ -93,13 +97,12 @@
 
   const getHostList = async () => {
     loading.value = true
-    const clusterId = route.params.id as unknown as number
     if (!paginationProps.value) {
       loading.value = false
       return
     }
     try {
-      const res = await getHosts({ ...filtersParams.value, clusterId })
+      const res = await getHosts({ ...filtersParams.value, clusterId: 
clusterId.value })
       dataSource.value = res.content.map((v) => ({ ...v, name: v.id, 
displayName: v.hostname }))
       paginationProps.value.total = res.total
       loading.value = false
@@ -116,13 +119,20 @@
     onChangeCallback: getHostList
   })
 
+  const getCheckboxProps: TableRowSelection['getCheckboxProps'] = (record) => 
({
+    disabled: installedStore
+      .getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
+      .includes(currComp.value.split('/')[0]),
+    name: record.hostname
+  })
+
   const onSelectChange: TableRowSelection['onChange'] = (selectedRowKeys, 
selectedRows) => {
     allComps.value.has(currComp.value.split('/').at(-1)) && 
updateHostsForComponent(currComp.value, selectedRows)
     state.selectedRowKeys = selectedRowKeys
   }
 
   const resetSelectedRowKeys = (key: string) => {
-    state.selectedRowKeys = allComps.value.has(key) ? 
allComps.value.get(key)?.hosts.map((v: HostVO) => v.id) : []
+    state.selectedRowKeys = allComps.value.has(key) ? 
allComps.value.get(key)?.hosts.map((v: HostVO) => v.hostname) : []
   }
 
   const treeSelectedChange = (keyPath: string) => {
@@ -149,12 +159,16 @@
         <div>{{ $t('service.select_host') }}</div>
       </div>
       <a-table
-        row-key="id"
+        row-key="hostname"
         :loading="loading"
         :data-source="dataSource"
         :columns="columns"
         :pagination="paginationProps"
-        :row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: 
onSelectChange }"
+        :row-selection="{
+          selectedRowKeys: state.selectedRowKeys,
+          getCheckboxProps: getCheckboxProps,
+          onChange: onSelectChange
+        }"
         @change="onChange"
       >
         <template #customFilterDropdown="{ setSelectedKeys, selectedKeys, 
confirm, clearFilters, column }">
@@ -199,6 +213,7 @@
   .component-assigner {
     display: grid;
     grid-template-columns: 1fr auto 3fr auto 1fr;
+
     .list-title {
       display: flex;
       height: 32px;
@@ -208,6 +223,7 @@
       padding-bottom: 16px;
       margin-bottom: 16px;
     }
+
     .divider {
       height: 100%;
       margin-inline: 16px;
@@ -224,11 +240,13 @@
     display: grid;
     gap: $space-sm;
     padding: $space-sm;
+
     &-option {
       width: 100%;
       display: grid;
       gap: $space-sm;
       grid-template-columns: 1fr 1fr;
+
       button {
         width: 100%;
       }
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
 
b/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
index 90cbed7f..825533af 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
@@ -26,7 +26,6 @@
   import type { JobVO, StageVO, StateType, TaskVO } from '@/api/job/types'
 
   const props = defineProps<{ stepData: CommandVO }>()
-  const emits = defineEmits(['updateData'])
   const { stepData } = toRefs(props)
   const activeKey = ref<number[]>([])
   const jobDetail = ref<JobVO>({})
@@ -57,7 +56,6 @@
       const data = await getJobDetails({ jobId, clusterId: 0 })
       jobDetail.value = data
       activeKey.value = data.stages ? data.stages?.map((v) => v.id!) : []
-      emits('updateData', { ...stepData.value, ...data })
       return ['Successful', 'Failed'].includes(data.state as StateType)
     } catch (error) {
       console.log('error :>> ', error)
@@ -124,9 +122,9 @@
     <a-empty v-if="stages.length == 0" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
     <div v-else class="component-installer">
       <div class="retry">
-        <a-button v-if="stepData.state === 'Failed'" type="link" 
@click="handleRetryJob">{{
-          $t('common.retry')
-        }}</a-button>
+        <a-button v-if="jobDetail.state === 'Failed'" type="link" 
@click="handleRetryJob">
+          {{ $t('common.retry') }}
+        </a-button>
       </div>
       <a-collapse v-model:active-key="activeKey" :bordered="false" 
:ghost="true">
         <a-collapse-panel v-for="stage in stages" :key="stage.id">
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 0d0f0609..446d8a30 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
@@ -18,7 +18,7 @@
 -->
 
 <script setup lang="ts">
-  import { onActivated, onDeactivated, ref, shallowRef, watch } from 'vue'
+  import { computed, onActivated, onDeactivated, ref, shallowRef, watch } from 
'vue'
   import { debounce } from 'lodash'
   import { Empty } from 'ant-design-vue'
   import TreeSelector from './tree-selector.vue'
@@ -35,7 +35,7 @@
     isView: false
   })
 
-  const { selectedServices } = useCreateService()
+  const { clusterId, installedStore, selectedServices } = useCreateService()
   const searchStr = ref('')
   const currService = ref<Key>('')
   const configs = ref<ServiceConfigReq[]>([])
@@ -61,6 +61,13 @@
       lg: { span: 18 }
     }
   })
+  const serviceList = computed(() => selectedServices.value)
+
+  const disabled = computed(() =>
+    installedStore
+      .getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
+      .includes(currService.value.toString().split('/').at(-1)!)
+  )
 
   watch(
     () => props.isView,
@@ -118,7 +125,7 @@
   // }
 
   onActivated(() => {
-    debouncedOnSearch.value = debounce(filterConfigurations, 500)
+    debouncedOnSearch.value = debounce(filterConfigurations, 300)
     filterConfigs.value = [...configs.value]
   })
 
@@ -133,7 +140,7 @@
       <div class="list-title">
         <div>{{ $t('service.service_list') }}</div>
       </div>
-      <tree-selector :tree="selectedServices" :field-names="fieldNames" 
@change="handleChange" />
+      <tree-selector :tree="serviceList" :field-names="fieldNames" 
@change="handleChange" />
     </section>
     <a-divider type="vertical" class="divider" />
     <section>
@@ -146,7 +153,7 @@
         />
       </div>
       <a-empty v-if="filterConfigs.length === 0" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
-      <a-form v-else :disabled="$props.isView" :label-wrap="true">
+      <a-form v-else :disabled="$props.isView || disabled" :label-wrap="true">
         <a-collapse v-model:active-key="activeKey" :bordered="false" 
:ghost="true">
           <a-collapse-panel v-for="config in filterConfigs" :key="config.id">
             <template #extra>
@@ -212,9 +219,11 @@
     background-color: rgb(255, 192, 105);
     padding: 0px;
   }
+
   .service-configurator-view {
     grid-template-columns: 1fr auto 4fr auto 1fr !important;
   }
+
   .service-configurator {
     display: grid;
     grid-template-columns: 1fr auto 4fr;
@@ -225,6 +234,7 @@
       background-color: $color-fill-quaternary;
       border-bottom: 1px solid $color-border-secondary;
     }
+
     :deep(.ant-collapse-content-box) {
       padding-inline: 24px !important;
     }
@@ -243,10 +253,12 @@
       border-bottom: 1px solid $color-border;
       padding-bottom: 16px;
       margin-bottom: 16px;
+
       .ant-input {
         flex: 0 1 160px;
       }
     }
+
     .divider {
       height: 100%;
       margin-inline: 16px;
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 83997dfb..f8b47788 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
@@ -18,23 +18,36 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onActivated, reactive, ref, toRefs } from 'vue'
+  import { computed, onActivated, reactive, ref, toRefs, useAttrs } from 'vue'
   import { usePngImage } from '@/utils/tools'
   import useCreateService from './use-create-service'
-  import { ExpandServiceVO } from '@/store/stack'
+  import type { ExpandServiceVO } from '@/store/stack'
+  import { ComponentVO } from '@/api/component/types.ts'
 
   interface State {
     isAddableData: ExpandServiceVO[]
     selectedData: ExpandServiceVO[]
   }
 
+  const { creationMode } = useAttrs()
   const searchStr = ref('')
   const state = reactive<State>({
     isAddableData: [],
     selectedData: []
   })
-  const { selectedServices, servicesOfExcludeInfra, 
confirmServiceDependencies, setDataByCurrent } = useCreateService()
+  const {
+    clusterId,
+    selectedServices,
+    servicesOfInfra,
+    servicesOfExcludeInfra,
+    installedStore,
+    confirmServiceDependencies,
+    setDataByCurrent
+  } = useCreateService()
   const { isAddableData } = toRefs(state)
+  const checkSelectedServicesOnlyInstalled = computed(
+    () => selectedServices.value.filter((v: ExpandServiceVO) => 
!v.isInstalled).length === 0
+  )
   const filterAddableData = computed(() =>
     isAddableData.value.filter(
       (v) =>
@@ -95,10 +108,62 @@
     return splitStr.toString().split(new 
RegExp(`(?<=${searchStr.value})|(?=${searchStr.value})`, 'i'))
   }
 
-  onActivated(() => {
-    selectedServices.value.length > 0
-      ? (state.selectedData = [...selectedServices.value])
-      : (state.isAddableData = [...(servicesOfExcludeInfra.value as 
ExpandServiceVO[])])
+  const mergeComponents = (components: ComponentVO[]) => {
+    return Object.values(
+      components.reduce((acc, item) => {
+        const { name, hostname } = item
+        if (!acc[name!]) {
+          acc[name!] = {
+            ...item,
+            hosts: [{ hostname, name: hostname, displayName: hostname }]
+          }
+        } else {
+          acc[name!].hosts.push({ hostname, name: hostname, displayName: 
hostname })
+        }
+        return acc
+      }, {})
+    )
+  }
+
+  const initInstalledServicesDetail = async () => {
+    const detailRes = await 
installedStore.getInstalledServicesDetailByKey(`${clusterId.value}`)
+    const detailMap = new Map<string, ExpandServiceVO>()
+    if (!detailRes) {
+      return detailMap
+    } else {
+      return detailRes.reduce(
+        (pre, val) =>
+          pre.set(val.name!, {
+            ...val,
+            components: mergeComponents(val.components || [])
+          } as ExpandServiceVO),
+        detailMap
+      )
+    }
+  }
+
+  const addInstalledSymbolForSelectedServices = async (onlyInstalled: boolean) 
=> {
+    if (onlyInstalled) {
+      const installedServiceMap = await initInstalledServicesDetail()
+      const installedServiceNames = 
installedStore.getInstalledNamesOrIdsOfServiceByKey(`${clusterId.value}`)
+      const data = creationMode === 'internal' ? servicesOfExcludeInfra.value 
: servicesOfInfra.value
+      data.forEach((v) => {
+        if (installedServiceNames.includes(v.name || '')) {
+          Object.assign(v, installedServiceMap.get(v.name!))
+          v.isInstalled = true
+          state.selectedData.push(v as ExpandServiceVO)
+        } else {
+          state.isAddableData.push(v as ExpandServiceVO)
+        }
+      })
+      setDataByCurrent(state.selectedData)
+    } else {
+      state.selectedData = [...selectedServices.value]
+    }
+  }
+
+  onActivated(async () => {
+    await 
addInstalledSymbolForSelectedServices(checkSelectedServicesOnlyInstalled.value)
   })
 
   defineExpose({
@@ -162,7 +227,7 @@
         <template #renderItem="{ item }">
           <a-list-item>
             <template #actions>
-              <a-button danger type="primary" @click="removeInstallItem(item)">
+              <a-button danger :disabled="item.isInstalled" type="primary" 
@click="removeInstallItem(item)">
                 {{ $t('common.remove') }}
               </a-button>
             </template>
@@ -208,6 +273,7 @@
     grid-template-columns: 1fr auto 1fr;
     grid-template-rows: auto;
     justify-content: space-between;
+
     .list-title {
       display: flex;
       height: 32px;
@@ -216,21 +282,26 @@
       font-weight: 500;
       border-bottom: 1px solid $color-border;
       padding-bottom: 16px;
+
       .ant-input {
         flex: 0 1 160px;
       }
     }
+
     .ant-list {
       max-height: 500px;
       overflow: auto;
     }
   }
+
   .divider {
     height: 100%;
     margin-inline: 16px;
   }
+
   :deep(.ant-avatar) {
     border-radius: 4px;
+
     img {
       object-fit: contain !important;
     }
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/tree-selector.vue 
b/bigtop-manager-ui/src/components/create-service/components/tree-selector.vue
index e3924cef..4e1a0650 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/tree-selector.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/tree-selector.vue
@@ -97,6 +97,7 @@
   <div class="sidebar">
     <a-tree
       v-if="$props.tree.length > 0"
+      :default-expand-all="true"
       :selected-keys="checkSelectedKeys"
       :selectable="$props.selectable"
       :tree-data="$props.tree"
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
 
b/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
index 702cd8d7..021c8fd5 100644
--- 
a/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
+++ 
b/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
@@ -17,16 +17,17 @@
  * under the License.
  */
 
-import { computed, createVNode, ref, watch, effectScope, Ref, ComputedRef } 
from 'vue'
+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 { CommandVO, CommandRequest, ServiceCommandReq } from 
'@/api/command/types'
+import type { CommandRequest, CommandVO, ServiceCommandReq } from 
'@/api/command/types'
 import type { ServiceVO } from '@/api/service/types'
 
 interface ProcessResult {
@@ -63,10 +64,13 @@ const useCreateService = () => {
     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')
 
   watch(
     () => selectedServices.value,
@@ -81,7 +85,7 @@ const useCreateService = () => {
   const commandRequest = ref<CommandRequest>({
     command: 'Add',
     commandLevel: 'service',
-    clusterId: parseInt(route.params.id as string)
+    clusterId: clusterId.value
   })
 
   const allComps = computed(() => {
@@ -104,9 +108,10 @@ const useCreateService = () => {
   const transformServiceData = (services: ExpandServiceVO[]) => {
     return services.map((service) => ({
       serviceName: service.name,
-      componentHosts: service.components!.map((component) => ({
+      installed: service.isInstalled === undefined ? false : 
service.isInstalled,
+      componentHosts: (service.components || []).map((component) => ({
         componentName: component.name,
-        hostnames: component.hosts.map((host: HostVO) => host.hostname)
+        hostnames: (component.hosts || []).map((host: HostVO) => host.hostname)
       })),
       configs: service.configs
     })) as ServiceCommandReq[]
@@ -114,12 +119,16 @@ const useCreateService = () => {
 
   // Validate services from infra
   const validServiceFromInfra = (targetService: ExpandServiceVO, 
requiredServices: string[]) => {
-    const filterServiceNames = servicesOfInfra.value
-      .filter((service) => requiredServices?.includes(service.name!))
-      .map((service) => service.displayName)
-
-    if (!filterServiceNames.length) return false
-    message.error(t('service.dependencies_conflict_msg', 
[targetService.displayName!, filterServiceNames.join(',')]))
+    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
   }
 
@@ -131,7 +140,7 @@ const useCreateService = () => {
   ): Promise<ProcessResult> => {
     const dependencies = targetService.requiredServices || []
 
-    if (validServiceFromInfra(targetService, dependencies)) {
+    if (creationMode.value === 'internal' && 
validServiceFromInfra(targetService, dependencies)) {
       return {
         success: false,
         conflictService: targetService
@@ -142,6 +151,8 @@ const useCreateService = () => {
       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 }
 
@@ -158,8 +169,16 @@ const useCreateService = () => {
     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 = new Map(servicesOfExcludeInfra.value.map((s) => [s.name 
as string, s as 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) {
@@ -174,7 +193,7 @@ const useCreateService = () => {
     if (!requiredServices) {
       return [preSelectedService]
     }
-    if (validServiceFromInfra(preSelectedService, requiredServices)) {
+    if (creationMode.value === 'internal' && 
validServiceFromInfra(preSelectedService, requiredServices)) {
       return []
     } else {
       return await handlePreSelectedServiceDependencies(preSelectedService)
@@ -199,8 +218,7 @@ const useCreateService = () => {
 
   const createService = async () => {
     try {
-      const formatData = transformServiceData(selectedServices.value)
-      commandRequest.value.serviceCommands = formatData
+      commandRequest.value.serviceCommands = 
transformServiceData(selectedServices.value)
       afterCreateRes.value = await execCommand(commandRequest.value)
       return true
     } catch (error) {
@@ -211,10 +229,13 @@ const useCreateService = () => {
 
   return {
     steps,
+    clusterId,
     current,
     stepsLimit,
     selectedServices,
     servicesOfExcludeInfra,
+    servicesOfInfra,
+    installedStore,
     allComps,
     afterCreateRes,
     setDataByCurrent,
@@ -223,7 +244,8 @@ const useCreateService = () => {
     createService,
     previousStep,
     nextStep,
-    scope
+    scope,
+    creationMode
   }
 }
 
diff --git a/bigtop-manager-ui/src/components/create-service/create.vue 
b/bigtop-manager-ui/src/components/create-service/create.vue
index 7459dffd..48f7e7cb 100644
--- a/bigtop-manager-ui/src/components/create-service/create.vue
+++ b/bigtop-manager-ui/src/components/create-service/create.vue
@@ -47,7 +47,7 @@
   const currComp = computed(() => components.value[current.value])
 
   const validateServiceSelection = async () => {
-    if (selectedServices.value.length === 0) {
+    if (selectedServices.value.filter((v) => !v.isInstalled).length === 0) {
       message.error(t('service.service_selection'))
       return false
     } else {
@@ -80,7 +80,7 @@
     return isValid
   }
 
-  const stepValidators = [validateServiceSelection, 
validateComponentAssignments, () => true]
+  const stepValidators = [validateServiceSelection, 
validateComponentAssignments, () => true, () => true]
 
   const proceedToNextStep = async () => {
     if (current.value < 3) {
@@ -116,11 +116,16 @@
       <template v-for="stepItem in steps" :key="stepItem.title">
         <div v-show="steps[current] === stepItem" class="step-title">
           <h5>{{ $t(stepItem) }}</h5>
-          <section :class="{ 'step-content': current < stepsLimit }"> 
</section>
+          <section :class="{ 'step-content': current < stepsLimit }"></section>
         </div>
       </template>
       <keep-alive>
-        <component :is="currComp" ref="compRef" :is-view="current === 3" />
+        <component
+          :is="currComp"
+          ref="compRef"
+          :is-view="current === 3"
+          v-bind="{ creationMode: $route.params.creationMode || 'internal' }"
+        />
       </keep-alive>
       <component-installer v-if="current == stepsLimit" 
:step-data="afterCreateRes" />
       <div class="step-action">
@@ -146,27 +151,32 @@
 <style lang="scss" scoped>
   .infra-creation {
     min-width: 600px;
+
     .header-card {
       min-height: 80px;
     }
+
     .steps-wrp {
       width: 100%;
       height: 100%;
       padding-inline: 6%;
     }
   }
+
   .step-title {
     h5 {
       margin: 0;
       font-size: 16px;
       font-weight: 500;
       letter-spacing: 0px;
-      line-height: 16px;
+      line-height: 24px;
     }
   }
+
   .step-content {
     padding-block: $space-md;
   }
+
   .step-action {
     text-align: end;
     margin-top: $space-md;
diff --git a/bigtop-manager-ui/src/composables/use-base-table.ts 
b/bigtop-manager-ui/src/composables/use-base-table.ts
index 2354c63d..851aedb9 100644
--- a/bigtop-manager-ui/src/composables/use-base-table.ts
+++ b/bigtop-manager-ui/src/composables/use-base-table.ts
@@ -24,12 +24,14 @@ import type { PaginationProps, TableColumnType, TableProps 
} from 'ant-design-vu
 import type { FilterValue } from 'ant-design-vue/es/table/interface'
 
 type PaginationType = PaginationProps | false | undefined
+
 export interface UseBaseTableProps<T = any> {
   columns: TableColumnType[]
   rows?: T[]
   pagination?: PaginationType
   onChangeCallback?: () => void
 }
+
 const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
   const { columns, rows, pagination } = props
   const { t } = useI18n()
@@ -87,9 +89,6 @@ const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
       pageSizeOptions: ['10', '20', '30', '40', '50'],
       showTotal: (total) => `${t('common.total', [total])}`
     }
-    if (props.onChangeCallback) {
-      props.onChangeCallback()
-    }
   }
 
   onUnmounted(() => {
diff --git a/bigtop-manager-ui/src/layouts/header.vue 
b/bigtop-manager-ui/src/layouts/header.vue
index eeaf0090..1ce93fc3 100644
--- a/bigtop-manager-ui/src/layouts/header.vue
+++ b/bigtop-manager-ui/src/layouts/header.vue
@@ -103,6 +103,7 @@
     .header-left {
       width: $layout-sider-width;
       margin: 0;
+      flex-shrink: 0;
       :deep(.svg-icon) {
         width: 180px;
         height: 30px;
diff --git a/bigtop-manager-ui/src/layouts/index.vue 
b/bigtop-manager-ui/src/layouts/index.vue
index f36aca0a..37bb1ed5 100644
--- a/bigtop-manager-ui/src/layouts/index.vue
+++ b/bigtop-manager-ui/src/layouts/index.vue
@@ -23,18 +23,19 @@
   import LayoutHeader from '@/layouts/header.vue'
   import LayoutSider from '@/layouts/sider.vue'
   import { useUserStore } from '@/store/user'
-  // import { useClusterStore } from '@/store/cluster'
-  import { useMenuStore } from '@/store/menu/index'
+  import { useMenuStore } from '@/store/menu'
   import { useStackStore } from '@/store/stack'
+  import { useInstalledStore } from '@/store/installed'
   import { storeToRefs } from 'pinia'
 
   const userStore = useUserStore()
   const menuStore = useMenuStore()
-  // const clusterStore = useClusterStore()
+  const installedStore = useInstalledStore()
   const stackStore = useStackStore()
   const { headerSelectedKey, headerMenus, siderMenuSelectedKey, siderMenus } = 
storeToRefs(menuStore)
 
-  onMounted(async () => {
+  onMounted(() => {
+    installedStore.getServicesOfInfra()
     userStore.getUserInfo()
     menuStore.setUpMenu()
     stackStore.loadStacks()
@@ -65,7 +66,11 @@
 <style lang="scss" scoped>
   .layout {
     height: 100vh;
+
     &-inner {
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
       padding: $space-lg $space-md;
       overflow: auto;
     }
diff --git a/bigtop-manager-ui/src/locales/en_US/cluster.ts 
b/bigtop-manager-ui/src/locales/en_US/cluster.ts
index a05ff116..05612202 100644
--- a/bigtop-manager-ui/src/locales/en_US/cluster.ts
+++ b/bigtop-manager-ui/src/locales/en_US/cluster.ts
@@ -19,7 +19,7 @@
 export default {
   base_info: 'Basic Information',
   cluster_config: 'Cluster Configuration',
-  cluster_management: 'Cluster',
+  cluster_info: 'Cluster',
   component_info: 'Stack',
   host_config: 'Hosts',
   create: 'Create',
diff --git a/bigtop-manager-ui/src/locales/en_US/service.ts 
b/bigtop-manager-ui/src/locales/en_US/service.ts
index 6b14adcb..9e281799 100644
--- a/bigtop-manager-ui/src/locales/en_US/service.ts
+++ b/bigtop-manager-ui/src/locales/en_US/service.ts
@@ -23,7 +23,7 @@ export default {
   assign_component: 'Assign Component',
   configure_service: 'Configure Service',
   service_overview: 'Service Overview',
-  install_component: 'Install Component',
+  install_component: 'Install',
   service_list: 'Service List',
   pending_installation_services: 'Selected Services',
   select_host: 'Select  Host',
diff --git a/bigtop-manager-ui/src/locales/zh_CN/cluster.ts 
b/bigtop-manager-ui/src/locales/zh_CN/cluster.ts
index eb9ba504..29a9cd70 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/cluster.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/cluster.ts
@@ -19,7 +19,7 @@
 export default {
   base_info: '基本信息',
   cluster_config: '集群配置',
-  cluster_management: '集群管理',
+  cluster_info: '集群信息',
   component_info: '组件信息',
   host_config: '主机管理',
   create: '创建集群',
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
index a222bc43..a04074c3 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
@@ -165,7 +165,7 @@
           ...item,
           key: generateRandomId(),
           hostname: v,
-          status: 'UNKNOW'
+          status: 'UNKNOWN'
         }
       }) as HostReq[]
       dataSource.value?.unshift(...items)
@@ -230,7 +230,7 @@
           <svg-icon :name="record.status.toLowerCase()" />
           <span :title="`${record.message ? record.message : ''}`">
             {{ `${$t(`common.${record.status.toLowerCase()}`)}` }}
-            {{ record.message ? `:  ${record.message}` : '' }}
+            {{ record.message && record.status.toLowerCase() === 'failed' ? `: 
 ${record.message}` : '' }}
           </span>
         </template>
         <template v-if="column.key === 'operation'">
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/set-source.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/set-source.vue
index 4c8d996a..b2449ee0 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/set-source.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/set-source.vue
@@ -20,7 +20,7 @@
 <script setup lang="ts">
   import { computed, reactive, ref, toRefs } from 'vue'
   import { getRepoList, updateRepo } from '@/api/repo'
-  import { message, type FormInstance, type TableColumnType } from 
'ant-design-vue'
+  import { type FormInstance, message, type TableColumnType } from 
'ant-design-vue'
   import { useI18n } from 'vue-i18n'
   import type { RepoVO } from '@/api/repo/types'
 
@@ -62,8 +62,7 @@
   const getSource = async () => {
     loading.value = true
     try {
-      const data = await getRepoList()
-      list.value = data
+      list.value = await getRepoList()
     } catch (error) {
       console.log('error :>> ', error)
     } finally {
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
index 9d809340..37134201 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
@@ -30,7 +30,7 @@
   import ComponentInfo from './components/component-info.vue'
   import HostConfig from './components/host-config.vue'
   import CheckWorkflow from './components/check-workflow.vue'
-  import { ClusterCommandReq, HostReq, type CommandRequest, type CommandVO } 
from '@/api/command/types'
+  import { ClusterCommandReq, type CommandRequest, type CommandVO, HostReq } 
from '@/api/command/types'
 
   const { t } = useI18n()
   const menuStore = useMenuStore()
@@ -44,16 +44,16 @@
   const installStatus = shallowRef<InstalledStatusVO[]>([])
   const components = shallowRef<any[]>([ClusterBase, ComponentInfo, 
HostConfig, CheckWorkflow])
   const isInstall = computed(() => current.value === 2)
-  const hasUnknowHost = computed(() => stepData.value[2].filter((v) => 
v.status === Status.Unknown).length == 0)
+  const hasUnknownHost = computed(() => stepData.value[2].filter((v) => 
v.status === Status.Unknown).length == 0)
   const allInstallSuccess = computed(
     () =>
       stepData.value[2].length != 0 &&
       stepData.value[2].every((v) => v.status === Status.Success) &&
-      hasUnknowHost.value
+      hasUnknownHost.value
   )
   const isDone = computed(() => ['Successful', 
'Failed'].includes(stepData.value[stepData.value.length - 1].state))
   const steps = computed(() => [
-    'cluster.cluster_management',
+    'cluster.cluster_info',
     'cluster.component_info',
     'cluster.host_config',
     'cluster.create'
@@ -119,12 +119,7 @@
       const data = await getInstalledStatus()
       installStatus.value = data
       stepData.value[current.value] = 
mergeByHostname(stepData.value[current.value], data)
-      const allResolved = data.every((item) => item.status != 
Status.Installing)
-      if (allResolved) {
-        return true
-      } else {
-        return false
-      }
+      return data.every((item) => item.status != Status.Installing)
     } catch (error) {
       console.log('error :>> ', error)
     }
@@ -196,7 +191,7 @@
       <template v-for="stepItem in steps" :key="stepItem.title">
         <div v-show="steps[current] === stepItem" class="step-title">
           <h5>{{ $t(stepItem) }}</h5>
-          <section :class="{ 'step-content': current < stepsLimit }"> 
</section>
+          <section :class="{ 'step-content': current < stepsLimit }"></section>
         </div>
       </template>
       <component :is="getCompName" ref="compRef" 
:step-data="stepData[current]" @update-data="updateData" />
@@ -219,9 +214,9 @@
               {{ $t('common.next') }}
             </a-button>
           </template>
-          <a-button v-show="current === stepsLimit && isDone" type="primary" 
@click="onSave">{{
-            $t('common.done')
-          }}</a-button>
+          <a-button v-show="current === stepsLimit && isDone" type="primary" 
@click="onSave"
+            >{{ $t('common.done') }}
+          </a-button>
         </a-space>
       </div>
     </main-card>
@@ -231,15 +226,18 @@
 <style lang="scss" scoped>
   .cluster-create {
     min-width: 600px;
+
     .header-card {
       min-height: 80px;
     }
+
     .steps-wrp {
       width: 100%;
       height: 100%;
       padding-inline: 6%;
     }
   }
+
   .step-title {
     h5 {
       margin: 0;
@@ -249,9 +247,11 @@
       line-height: 16px;
     }
   }
+
   .step-content {
     padding-block: $space-md;
   }
+
   .step-action {
     text-align: end;
     margin-top: $space-md;
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
index ca7a6616..2dddd13e 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -38,7 +38,7 @@
   const { t } = useI18n()
   const attrs = useAttrs() as ClusterVO
   const searchInputRef = ref()
-  const hostStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOW'])
+  const hostStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
   const state = reactive<TableState>({
     searchText: '',
     searchedColumn: '',
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 9baf3300..4c42360a 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
@@ -97,8 +97,8 @@
   ])
 
   const getCompName = computed(() => {
-    const componnts = [Overview, Service, Host, User, Job]
-    return componnts[parseInt(activeKey.value) - 1]
+    const components = [Overview, Service, Host, User, Job]
+    return components[parseInt(activeKey.value) - 1]
   })
 
   const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key 
}) => {
@@ -108,15 +108,15 @@
         clusterId: currCluster.value.id,
         commandLevel: 'cluster'
       })
-      clusterStore.loadClusters()
-      clusterStore.getClusterDetail()
+      await clusterStore.loadClusters()
+      await clusterStore.getClusterDetail()
     } catch (error) {
       console.log('error :>> ', error)
     }
   }
 
   const addService: GroupItem['clickEvent'] = () => {
-    router.push({ name: 'CreateService' })
+    router.push({ name: 'CreateService', params: { 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 80b2cc56..8e0c606e 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -74,7 +74,7 @@
   const filterFormItems = computed((): FilterFormItem[] => [
     {
       type: 'search',
-      key: 'serviceName',
+      key: 'name',
       label: t('service.name')
     },
     {
@@ -84,11 +84,11 @@
       options: [
         {
           label: t('common.required'),
-          value: 1
+          value: true
         },
         {
           label: t('common.not_required'),
-          value: 2
+          value: false
         }
       ]
     },
@@ -126,7 +126,7 @@
   <a-spin :spinning="loading" class="service">
     <filter-form :filter-items="filterFormItems" @filter="getServices" />
     <a-empty v-if="services.length == 0" style="width: 100%" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
-    <template v-else>
+    <div v-else class="service-item-wrp">
       <a-card v-for="item in services" :key="item.id" :hoverable="true" 
class="service-item">
         <div class="header">
           <div class="header-base-wrp">
@@ -158,7 +158,7 @@
           </button-group>
         </div>
       </a-card>
-    </template>
+    </div>
   </a-spin>
 </template>
 
@@ -173,10 +173,10 @@
     color: rgba(0, 0, 0, 0.45);
   }
 
-  .service {
+  .service-item-wrp {
     display: flex;
     flex-wrap: wrap;
-    gap: 16px;
+    gap: 19px;
   }
 
   .service-item {
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 6cfb5856..2fc14d22 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
@@ -21,6 +21,13 @@
 
 <template>
   <div>
+    <a-button
+      @click="
+        () => $router.push({ name: 'CreateInfraService', params: { id: 0, 
cluster: '', creationMode: 'public' } })
+      "
+    >
+      add Service
+    </a-button>
     <span> infra list </span>
   </div>
 </template>
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts 
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index 54ce44ae..1c9ecd9f 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -65,7 +65,7 @@ const routes: RouteRecordRaw[] = [
           },
           {
             name: 'CreateService',
-            path: `${RouteExceptions.DYNAMIC_ROUTE_MATCH}/create-service`,
+            path: 
`${RouteExceptions.DYNAMIC_ROUTE_MATCH}/create-service/:creationMode`,
             component: () => import('@/components/create-service/create.vue'),
             meta: {
               hidden: true
@@ -90,6 +90,15 @@ const routes: RouteRecordRaw[] = [
               hidden: true,
               activeMenu: '/cluster-manage/infrastructures'
             }
+          },
+          {
+            name: 'CreateInfraService',
+            path: 'create-infra-service/:id/:creationMode',
+            component: () => import('@/components/create-service/create.vue'),
+            meta: {
+              hidden: true,
+              activeMenu: '/cluster-manage/infrastructures'
+            }
           }
         ]
       },
diff --git a/bigtop-manager-ui/src/store/cluster/index.ts 
b/bigtop-manager-ui/src/store/cluster/index.ts
index f994d139..365b9fb7 100644
--- a/bigtop-manager-ui/src/store/cluster/index.ts
+++ b/bigtop-manager-ui/src/store/cluster/index.ts
@@ -17,23 +17,34 @@
  * under the License.
  */
 
-import { computed, ref } from 'vue'
+import { computed, ref, watch } 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 currCluster = ref<ClusterVO>({})
     const clusterId = computed(() => (route.params.id as string) || undefined)
 
+    watch(
+      () => clusters.value,
+      (val) => {
+        val.forEach((cluster) => {
+          installedStore.setInstalledMapKey(`${cluster.id}`)
+        })
+      }
+    )
+
     const addCluster = async () => {
       await loadClusters()
     }
@@ -49,7 +60,7 @@ export const useClusterStore = defineStore(
       try {
         loading.value = true
         currCluster.value = await getCluster(parseInt(clusterId.value))
-        currCluster.value.id != undefined && 
serviceStore.getServices(currCluster.value.id)
+        currCluster.value.id != undefined && (await 
serviceStore.getServices(currCluster.value.id))
       } catch (error) {
         console.log('error :>> ', error)
       } finally {
@@ -73,9 +84,5 @@ export const useClusterStore = defineStore(
   },
   {
     persist: false
-    // persist: {
-    //   storage: sessionStorage,
-    //   paths: ['clusters',]
-    // }
   }
 )
diff --git a/bigtop-manager-ui/src/store/installed/index.ts 
b/bigtop-manager-ui/src/store/installed/index.ts
new file mode 100644
index 00000000..4a417345
--- /dev/null
+++ b/bigtop-manager-ui/src/store/installed/index.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { ServiceVO } from '@/api/service/types.ts'
+
+export const useInstalledStore = defineStore(
+  'installed',
+  () => {
+    const serviceStore = useServiceStore()
+    const installedMap = ref<Record<string, string[]>>({})
+
+    const getInstalledNamesOrIdsOfServiceByKey = (key: string, flag: 'names' | 
'ids' = 'names') => {
+      return installedMap.value[key].map((value) => value.split('-')[flag === 
'names' ? 1 : 0])
+    }
+
+    const setInstalledMapKey = (key: string) => {
+      installedMap.value[key] = []
+    }
+
+    const setInstalledMapKeyOfValue = (key: string, value: string[]) => {
+      installedMap.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 {
+      installedMap,
+      getServicesOfInfra,
+      setInstalledMapKey,
+      setInstalledMapKeyOfValue,
+      getInstalledNamesOrIdsOfServiceByKey,
+      getInstalledServicesDetailByKey
+    }
+  },
+  {
+    persist: {
+      storage: sessionStorage,
+      key: 'installed',
+      paths: ['installedMap']
+    }
+  }
+)
diff --git a/bigtop-manager-ui/src/store/menu/index.ts 
b/bigtop-manager-ui/src/store/menu/index.ts
index 5fbfb470..492c4901 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -65,7 +65,7 @@ export const useMenuStore = defineStore(
       // resolve highlight menu
       let activeMenu = route.meta.activeMenu || route.path
       if (route.name === 'CreateService') {
-        activeMenu = route.path.split('/').slice(0, -1).join('/')
+        activeMenu = route.path.split('/').slice(0, -2).join('/')
       }
       const matchedNames = [RouteExceptions.SPECIAL_ROUTE_NAME, 
RouteExceptions.DEFAULT_ROUTE_NAME] as string[]
       headerSelectedKey.value = route.matched[0].path
diff --git a/bigtop-manager-ui/src/store/service/index.ts 
b/bigtop-manager-ui/src/store/service/index.ts
index 910bbf88..47d45a15 100644
--- a/bigtop-manager-ui/src/store/service/index.ts
+++ b/bigtop-manager-ui/src/store/service/index.ts
@@ -19,20 +19,23 @@
 
 import { defineStore, storeToRefs } from 'pinia'
 import { computed, ref } from 'vue'
-import { getServiceList } from '@/api/service'
-import { useStackStore } from '@/store/stack/index'
+import { getService, getServiceList } from '@/api/service'
+import { useStackStore } from '@/store/stack'
+import { 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 total = ref(0)
     const loading = ref(false)
     const { stacks } = storeToRefs(stackStore)
 
     const serviceNames = computed(() => services.value.map((v) => v.name))
+    const serviceMap = computed(() => services.value.map((v) => 
`${v.id}-${v.name}`))
     const locateStackWithService = computed(() => {
       return stacks.value.filter((item) =>
         item.services.some((service) => service.name && 
serviceNames.value.includes(service.name))
@@ -42,9 +45,10 @@ export const useServiceStore = defineStore(
     const getServices = async (clusterId: number, filterParams?: 
ServiceListParams) => {
       try {
         loading.value = true
-        const data = await getServiceList(clusterId, { ...filterParams, 
pageNum: 1, pageSize: 50 })
+        const data = await getServiceList(clusterId, { ...filterParams, 
pageNum: 1, pageSize: 100 })
         services.value = data.content
         total.value = data.total
+        installedStore.setInstalledMapKeyOfValue(`${clusterId}`, 
serviceMap.value)
       } catch (error) {
         console.log('error :>> ', error)
       } finally {
@@ -52,10 +56,19 @@ export const useServiceStore = defineStore(
       }
     }
 
+    const getServiceDetail = async (clusterId: number, serviceId: number) => {
+      try {
+        return await getService({ clusterId, id: serviceId })
+      } catch (error) {
+        console.log(error)
+      }
+    }
+
     return {
       services,
       loading,
       getServices,
+      getServiceDetail,
       serviceNames,
       locateStackWithService
     }
diff --git a/bigtop-manager-ui/src/store/stack/index.ts 
b/bigtop-manager-ui/src/store/stack/index.ts
index c667ad3d..bee272c2 100644
--- a/bigtop-manager-ui/src/store/stack/index.ts
+++ b/bigtop-manager-ui/src/store/stack/index.ts
@@ -32,8 +32,7 @@ export const useStackStore = defineStore(
 
     const loadStacks = async () => {
       try {
-        const data = await getStacks()
-        stacks.value = data
+        stacks.value = await getStacks()
       } catch (error) {
         console.log('error :>> ', error)
       }

Reply via email to