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 8b4a9941 BIGTOP-4389: Add service install page for cluster (#189)
8b4a9941 is described below

commit 8b4a9941c07c36e54864fc58d218e354718ced87
Author: Fdefined <[email protected]>
AuthorDate: Fri Mar 14 10:26:10 2025 +0800

    BIGTOP-4389: Add service install page for cluster (#189)
---
 bigtop-manager-ui/src/api/command/types.ts         |   2 +-
 .../assets/images/svg/{unknow.svg => unknown.svg}  |   0
 .../src/components/ai-assistant/chat-history.vue   |   6 +-
 .../components/component-assigner.vue              | 237 +++++++++++++++++++
 .../components/component-installer.vue}            |   7 +-
 .../components/service-configurator.vue            | 255 +++++++++++++++++++++
 .../create-service/components/service-selector.vue | 238 +++++++++++++++++++
 .../create-service/components/tree-selector.vue    | 110 +++++++++
 .../components/use-create-service.ts               | 230 +++++++++++++++++++
 .../src/components/create-service/create.vue       | 174 ++++++++++++++
 .../src/composables/use-base-table.ts              |   8 +
 .../stack/index.ts => composables/use-steps.ts}    |  42 ++--
 bigtop-manager-ui/src/enums/state.ts               |   4 +-
 bigtop-manager-ui/src/layouts/default.vue          |   2 +-
 bigtop-manager-ui/src/layouts/sider.vue            |   4 +-
 bigtop-manager-ui/src/locales/en_US/common.ts      |   6 +-
 bigtop-manager-ui/src/locales/en_US/service.ts     |  16 +-
 bigtop-manager-ui/src/locales/zh_CN/common.ts      |   6 +-
 bigtop-manager-ui/src/locales/zh_CN/service.ts     |  16 +-
 .../cluster/components/check-workflow.vue          |   3 +-
 .../src/pages/cluster-manage/cluster/create.vue    |  17 +-
 .../src/pages/cluster-manage/cluster/host.vue      |   2 +-
 .../src/pages/cluster-manage/cluster/index.vue     |   6 +-
 .../src/pages/cluster-manage/cluster/overview.vue  |  51 +++--
 .../src/pages/cluster-manage/cluster/service.vue   |  10 +-
 .../pages/cluster-manage/infrastructures/index.vue |   4 +-
 .../src/router/routes/modules/clusters.ts          |  33 ++-
 bigtop-manager-ui/src/store/menu/index.ts          |   9 +-
 bigtop-manager-ui/src/store/service/index.ts       |   7 +-
 bigtop-manager-ui/src/store/stack/index.ts         |  24 +-
 bigtop-manager-ui/src/styles/index.scss            |  10 +
 31 files changed, 1446 insertions(+), 93 deletions(-)

diff --git a/bigtop-manager-ui/src/api/command/types.ts 
b/bigtop-manager-ui/src/api/command/types.ts
index ca24cdbe..e5e876b4 100644
--- a/bigtop-manager-ui/src/api/command/types.ts
+++ b/bigtop-manager-ui/src/api/command/types.ts
@@ -114,7 +114,7 @@ export interface ComponentHostReq {
 export interface ServiceConfigReq {
   id?: number
   name?: string
-  properties?: PropertyReq[]
+  properties: PropertyReq[]
   [property: string]: any
 }
 
diff --git a/bigtop-manager-ui/src/assets/images/svg/unknow.svg 
b/bigtop-manager-ui/src/assets/images/svg/unknown.svg
similarity index 100%
rename from bigtop-manager-ui/src/assets/images/svg/unknow.svg
rename to bigtop-manager-ui/src/assets/images/svg/unknown.svg
diff --git a/bigtop-manager-ui/src/components/ai-assistant/chat-history.vue 
b/bigtop-manager-ui/src/components/ai-assistant/chat-history.vue
index ae18febc..05997ece 100644
--- a/bigtop-manager-ui/src/components/ai-assistant/chat-history.vue
+++ b/bigtop-manager-ui/src/components/ai-assistant/chat-history.vue
@@ -23,7 +23,7 @@
   import { formatTime } from '@/utils/tools'
   import { EllipsisOutlined } from '@ant-design/icons-vue'
   import type { ChatThread, ThreadId } from '@/api/ai-assistant/types'
-  import { message, Modal } from 'ant-design-vue'
+  import { message, Modal, Empty } from 'ant-design-vue'
   import { useI18n } from 'vue-i18n'
 
   interface Props {
@@ -153,7 +153,7 @@
         </a-button>
       </template>
       <main>
-        <a-empty v-if="threads.length == 0" />
+        <a-empty v-if="threads.length == 0" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
         <a-menu v-else v-model:selected-keys="selectKey" 
@select="handleSelect">
           <a-menu-item v-for="thread in threads" :key="thread.threadId" 
:title="thread.name">
             <div class="chat-history-item">
@@ -170,7 +170,7 @@
           <a-typography-title :level="5">{{ $t(title) }}</a-typography-title>
         </header>
         <main>
-          <a-empty v-if="threads.length == 0" />
+          <a-empty v-if="threads.length == 0" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
           <a-menu v-else v-model:selected-keys="selectKey" 
@select="handleSelect">
             <a-menu-item v-for="(thread, idx) in threads" 
:key="thread.threadId" :title="thread.name">
               <div class="chat-history-item">
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
new file mode 100644
index 00000000..2175959c
--- /dev/null
+++ 
b/bigtop-manager-ui/src/components/create-service/components/component-assigner.vue
@@ -0,0 +1,237 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { 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 type { FilterConfirmProps, FilterResetProps, TableRowSelection } from 
'ant-design-vue/es/table/interface'
+  import type { Key } from 'ant-design-vue/es/_util/type'
+
+  interface TableState {
+    selectedRowKeys: Key[]
+    searchText: string
+    searchedColumn: keyof HostVO
+  }
+
+  const { t } = useI18n()
+  const route = useRoute()
+  const searchInputRef = ref()
+  const currComp = ref<string>('')
+  const fieldNames = shallowRef({
+    children: 'components',
+    title: 'displayName',
+    key: 'name'
+  })
+  const state = reactive<TableState>({
+    searchText: '',
+    searchedColumn: '',
+    selectedRowKeys: []
+  })
+  const { 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 : []
+  })
+  const columns = computed((): TableColumnType<HostVO>[] => [
+    {
+      title: t('host.hostname'),
+      dataIndex: 'hostname',
+      key: 'hostname',
+      ellipsis: true,
+      customFilterDropdown: true
+    },
+    {
+      title: t('host.ip_address'),
+      dataIndex: 'ipv4',
+      key: 'ipv4',
+      ellipsis: true,
+      customFilterDropdown: true
+    },
+    {
+      title: t('common.desc'),
+      dataIndex: 'desc',
+      ellipsis: true
+    }
+  ])
+
+  const handleSearch = (selectedKeys: Key[], confirm: (param?: 
FilterConfirmProps) => void, dataIndex: string) => {
+    confirm()
+    Object.assign(state, {
+      searchText: selectedKeys[0] as string,
+      searchedColumn: dataIndex
+    })
+  }
+
+  const handleReset = (clearFilters: (param?: FilterResetProps) => void) => {
+    clearFilters({ confirm: true })
+    state.searchText = ''
+  }
+
+  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 })
+      dataSource.value = res.content.map((v) => ({ ...v, name: v.id, 
displayName: v.hostname }))
+      paginationProps.value.total = res.total
+      loading.value = false
+    } catch (error) {
+      console.log('error :>> ', error)
+    } finally {
+      loading.value = false
+    }
+  }
+
+  const { loading, dataSource, filtersParams, paginationProps, onChange } = 
useBaseTable<HostVO>({
+    columns: columns.value,
+    rows: [],
+    onChangeCallback: getHostList
+  })
+
+  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) : []
+  }
+
+  const treeSelectedChange = (keyPath: string) => {
+    currComp.value = keyPath ?? ''
+    resetSelectedRowKeys(keyPath.split('/')[1])
+  }
+
+  onActivated(() => {
+    getHostList()
+  })
+</script>
+
+<template>
+  <div class="component-assigner">
+    <section>
+      <div class="list-title">
+        <div>{{ $t('service.service_list') }}</div>
+      </div>
+      <tree-selector :tree="serviceList" :field-names="fieldNames" 
@change="treeSelectedChange" />
+    </section>
+    <a-divider type="vertical" class="divider" />
+    <section>
+      <div class="list-title">
+        <div>{{ $t('service.select_host') }}</div>
+      </div>
+      <a-table
+        row-key="id"
+        :loading="loading"
+        :data-source="dataSource"
+        :columns="columns"
+        :pagination="paginationProps"
+        :row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: 
onSelectChange }"
+        @change="onChange"
+      >
+        <template #customFilterDropdown="{ setSelectedKeys, selectedKeys, 
confirm, clearFilters, column }">
+          <div class="search">
+            <a-input
+              ref="searchInputRef"
+              :placeholder="$t('common.enter_error', [column.title])"
+              :value="selectedKeys[0]"
+              @change="(e: any) => setSelectedKeys(e.target?.value ? 
[e.target?.value] : [])"
+              @press-enter="handleSearch(selectedKeys, confirm, 
column.dataIndex)"
+            />
+            <div class="search-option">
+              <a-button size="small" @click="handleReset(clearFilters)">
+                {{ $t('common.reset') }}
+              </a-button>
+              <a-button type="primary" size="small" 
@click="handleSearch(selectedKeys, confirm, column.dataIndex)">
+                {{ $t('common.search') }}
+              </a-button>
+            </div>
+          </div>
+        </template>
+      </a-table>
+    </section>
+    <a-divider type="vertical" class="divider" />
+    <section>
+      <div class="list-title">
+        <div>{{ $t('service.host_preview') }}</div>
+      </div>
+      <div class="preview">
+        <a-empty v-if="hostsOfCurrComp.length === 0" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
+        <template v-else>
+          <div v-for="host in hostsOfCurrComp" :key="host.id">
+            {{ host.hostname }}
+          </div>
+        </template>
+      </div>
+    </section>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+  .component-assigner {
+    display: grid;
+    grid-template-columns: 1fr auto 3fr auto 1fr;
+    .list-title {
+      display: flex;
+      height: 32px;
+      align-items: center;
+      font-weight: 500;
+      border-bottom: 1px solid $color-border;
+      padding-bottom: 16px;
+      margin-bottom: 16px;
+    }
+    .divider {
+      height: 100%;
+      margin-inline: 16px;
+    }
+  }
+
+  .preview {
+    display: grid;
+    gap: 8px;
+    padding-inline: 24px;
+  }
+
+  .search {
+    display: grid;
+    gap: $space-sm;
+    padding: $space-sm;
+    &-option {
+      width: 100%;
+      display: grid;
+      gap: $space-sm;
+      grid-template-columns: 1fr 1fr;
+      button {
+        width: 100%;
+      }
+    }
+  }
+</style>
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
 
b/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
similarity index 96%
copy from 
bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
copy to 
bigtop-manager-ui/src/components/create-service/components/component-installer.vue
index 8bb2b193..90cbed7f 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
+++ 
b/bigtop-manager-ui/src/components/create-service/components/component-installer.vue
@@ -19,6 +19,7 @@
 
 <script setup lang="ts">
   import { computed, onMounted, reactive, ref, shallowRef, toRefs } from 'vue'
+  import { Empty } from 'ant-design-vue'
   import { getJobDetails, retryJob } from '@/api/job'
   import { CommandVO } from '@/api/command/types'
   import LogsView, { type LogViewProps } from '@/components/log-view/index.vue'
@@ -120,8 +121,8 @@
 
 <template>
   <a-spin :spinning="spinning">
-    <a-empty v-if="stages.length == 0" />
-    <div v-else class="check-workflow">
+    <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')
@@ -160,7 +161,7 @@
 </template>
 
 <style lang="scss" scoped>
-  .check-workflow {
+  .component-installer {
     button {
       padding: 0;
     }
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
new file mode 100644
index 00000000..0d0f0609
--- /dev/null
+++ 
b/bigtop-manager-ui/src/components/create-service/components/service-configurator.vue
@@ -0,0 +1,255 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { onActivated, onDeactivated, ref, shallowRef, watch } from 'vue'
+  import { debounce } from 'lodash'
+  import { Empty } from 'ant-design-vue'
+  import TreeSelector from './tree-selector.vue'
+  import useCreateService from './use-create-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'
+
+  interface Props {
+    isView?: boolean
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    isView: false
+  })
+
+  const { selectedServices } = useCreateService()
+  const searchStr = ref('')
+  const currService = ref<Key>('')
+  const configs = ref<ServiceConfigReq[]>([])
+  const activeKey = ref<number[]>([])
+  const debouncedOnSearch = ref()
+  const hostPreviewList = ref<ComponentVO[]>([])
+  const filterConfigs = ref<ServiceConfigReq[]>([])
+  const fieldNames = shallowRef({
+    title: 'displayName',
+    key: 'name'
+  })
+  const layout = shallowRef({
+    labelCol: {
+      xs: { span: 24 },
+      sm: { span: 8 },
+      md: { span: 8 },
+      lg: { span: 6 }
+    },
+    wrapperCol: {
+      xs: { span: 24 },
+      sm: { span: 16 },
+      md: { span: 16 },
+      lg: { span: 18 }
+    }
+  })
+
+  watch(
+    () => props.isView,
+    () => {
+      searchStr.value = ''
+      filterConfigs.value = configs.value
+    }
+  )
+
+  const createNewConfigItem = () => {
+    return {
+      name: '',
+      displayName: '',
+      value: '',
+      isManual: true
+    }
+  }
+
+  const manualAddConfig = (config: ServiceConfigReq) => {
+    config.properties?.push(createNewConfigItem())
+  }
+
+  const handleChange = (expandSelectedKeyPath: string) => {
+    currService.value = expandSelectedKeyPath
+    const index = selectedServices.value.findIndex((v) => v.name === 
expandSelectedKeyPath.split('/').at(-1))
+    if (index !== -1) {
+      const temp = selectedServices.value[index]
+      configs.value = temp.configs as ServiceConfigReq[]
+      hostPreviewList.value = temp.components as ComponentVO[]
+    } else {
+      configs.value = []
+      hostPreviewList.value = []
+    }
+    filterConfigurations()
+  }
+
+  const filterConfigurations = () => {
+    if (!searchStr.value) {
+      filterConfigs.value = configs.value
+    }
+    const lowerSearchTerm = searchStr.value.toLowerCase()
+    filterConfigs.value = configs.value.filter((config) => {
+      return config.properties.some((property) => {
+        return (
+          (property.displayName || '').toLowerCase().includes(lowerSearchTerm) 
||
+          property.name.toLowerCase().includes(lowerSearchTerm) ||
+          (property.value && 
property.value.toString().toLowerCase().includes(lowerSearchTerm))
+        )
+      })
+    })
+  }
+
+  // const splitSearchStr = (splitStr: string) => {
+  //   return splitStr.toString().split(new 
RegExp(`(?<=${searchStr.value})|(?=${searchStr.value})`, 'i'))
+  // }
+
+  onActivated(() => {
+    debouncedOnSearch.value = debounce(filterConfigurations, 500)
+    filterConfigs.value = [...configs.value]
+  })
+
+  onDeactivated(() => {
+    debouncedOnSearch.value.cancel()
+  })
+</script>
+
+<template>
+  <div class="service-configurator" :class="{ 'service-configurator-view': 
$props.isView }">
+    <section>
+      <div class="list-title">
+        <div>{{ $t('service.service_list') }}</div>
+      </div>
+      <tree-selector :tree="selectedServices" :field-names="fieldNames" 
@change="handleChange" />
+    </section>
+    <a-divider type="vertical" class="divider" />
+    <section>
+      <div class="list-title">
+        <div>{{ $t('service.host_preview') }}</div>
+        <a-input
+          v-model:value="searchStr"
+          :placeholder="$t('service.please_enter_search_keyword')"
+          @input="debouncedOnSearch"
+        />
+      </div>
+      <a-empty v-if="filterConfigs.length === 0" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
+      <a-form v-else :disabled="$props.isView" :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>
+              <a-button type="text" shape="circle" 
@click.stop="manualAddConfig(config)">
+                <template #icon>
+                  <svg-icon name="plus_dark" />
+                </template>
+              </a-button>
+            </template>
+            <template #header>
+              <span>{{ config.name }}</span>
+            </template>
+            <a-row v-for="(item, idx) in config.properties" :key="idx" 
:gutter="[16, 0]" :wrap="true">
+              <a-col v-bind="layout.labelCol">
+                <a-form-item>
+                  <a-textarea v-if="item.isManual" v-model:value="item.name" 
:auto-size="{ minRows: 1, maxRows: 5 }" />
+                  <span v-else style="overflow-wrap: break-word" 
:title="item.displayName ?? item.name">
+                    {{ item.displayName ?? item.name }}
+                  </span>
+
+                  <!-- <template v-else>
+                    <template v-for="(fragment, i) in 
splitSearchStr(item.displayName ?? item.name)">
+                      <mark v-if="fragment.toLowerCase() === 
searchStr.toLowerCase()" :key="i" class="highlight">
+                        {{ fragment }}
+                      </mark>
+                      <template v-else>
+                        <span :key="i" style="overflow-wrap: break-word" 
:title="item.displayName ?? item.name">
+                          {{ fragment }}
+                        </span>
+                      </template>
+                    </template>
+                  </template> -->
+                </a-form-item>
+              </a-col>
+              <a-col v-bind="layout.wrapperCol">
+                <a-form-item>
+                  <a-textarea v-model:value="item.value" :auto-size="{ 
minRows: 1, maxRows: 5 }" />
+                </a-form-item>
+              </a-col>
+            </a-row>
+          </a-collapse-panel>
+        </a-collapse>
+      </a-form>
+    </section>
+    <template v-if="isView">
+      <a-divider type="vertical" class="divider" />
+      <section>
+        <div class="list-title">
+          <div>{{ $t('service.host_preview') }}</div>
+        </div>
+        <tree-selector
+          :tree="hostPreviewList"
+          :selectable="false"
+          :field-names="{ ...fieldNames, children: 'hosts' }"
+        />
+      </section>
+    </template>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+  .highlight {
+    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;
+
+    :deep(.ant-collapse-header) {
+      display: flex;
+      align-items: center;
+      background-color: $color-fill-quaternary;
+      border-bottom: 1px solid $color-border-secondary;
+    }
+    :deep(.ant-collapse-content-box) {
+      padding-inline: 24px !important;
+    }
+
+    .ant-form {
+      max-height: 500px;
+      overflow: auto;
+    }
+
+    .list-title {
+      display: flex;
+      height: 32px;
+      align-items: center;
+      justify-content: space-between;
+      font-weight: 500;
+      border-bottom: 1px solid $color-border;
+      padding-bottom: 16px;
+      margin-bottom: 16px;
+      .ant-input {
+        flex: 0 1 160px;
+      }
+    }
+    .divider {
+      height: 100%;
+      margin-inline: 16px;
+    }
+  }
+</style>
diff --git 
a/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
 
b/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
new file mode 100644
index 00000000..83997dfb
--- /dev/null
+++ 
b/bigtop-manager-ui/src/components/create-service/components/service-selector.vue
@@ -0,0 +1,238 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { computed, onActivated, reactive, ref, toRefs } from 'vue'
+  import { usePngImage } from '@/utils/tools'
+  import useCreateService from './use-create-service'
+  import { ExpandServiceVO } from '@/store/stack'
+
+  interface State {
+    isAddableData: ExpandServiceVO[]
+    selectedData: ExpandServiceVO[]
+  }
+
+  const searchStr = ref('')
+  const state = reactive<State>({
+    isAddableData: [],
+    selectedData: []
+  })
+  const { selectedServices, servicesOfExcludeInfra, 
confirmServiceDependencies, setDataByCurrent } = useCreateService()
+  const { isAddableData } = toRefs(state)
+  const filterAddableData = computed(() =>
+    isAddableData.value.filter(
+      (v) =>
+        
v.displayName?.toString().toLowerCase().includes(searchStr.value.toString().toLowerCase())
 ||
+        
v.desc?.toString().toLowerCase().includes(searchStr.value.toString().toLowerCase())
+    )
+  )
+
+  const insertByOrder = <T extends { order: number }>(array: T[], item: T) => {
+    const index = findInsertIndex(array, item.order)
+    array.splice(index, 0, item)
+  }
+
+  const moveItem = <T extends { name?: string; order: number }>(from: T[], to: 
T[], item: T, key: keyof T = 'name') => {
+    const index = from.findIndex((v) => v[key] === item[key])
+    if (index !== -1) {
+      const [removedItem] = from.splice(index, 1)
+      insertByOrder(to, removedItem)
+    }
+  }
+
+  //  Binary search
+  const findInsertIndex = <T extends { order: number }>(array: T[], order: 
number) => {
+    let low = 0
+    let high = array.length
+
+    while (low < high) {
+      const mid = (low + high) >>> 1
+      if (array[mid].order < order) {
+        low = mid + 1
+      } else {
+        high = mid
+      }
+    }
+    return low
+  }
+
+  const handleInstallItem = (item: ExpandServiceVO, from: ExpandServiceVO[], 
to: ExpandServiceVO[]) => {
+    item.components = item.components?.map((v) => ({ ...v, hosts: [] }))
+    moveItem(from, to, item)
+    setDataByCurrent(state.selectedData)
+  }
+
+  const addInstallItem = async (item: ExpandServiceVO) => {
+    const items = await confirmServiceDependencies(item)
+    if (items.length > 0) {
+      items.forEach((i) => {
+        handleInstallItem(i, state.isAddableData, state.selectedData)
+      })
+    }
+  }
+
+  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'))
+  }
+
+  onActivated(() => {
+    selectedServices.value.length > 0
+      ? (state.selectedData = [...selectedServices.value])
+      : (state.isAddableData = [...(servicesOfExcludeInfra.value as 
ExpandServiceVO[])])
+  })
+
+  defineExpose({
+    addInstallItem
+  })
+</script>
+
+<template>
+  <div class="service-selector">
+    <div>
+      <div class="list-title">
+        <div>{{ $t('service.select_service') }}</div>
+        <a-input v-model:value="searchStr" 
:placeholder="$t('service.please_enter_search_keyword')" />
+      </div>
+      <a-list item-layout="horizontal" :data-source="filterAddableData">
+        <template #renderItem="{ item }">
+          <a-list-item>
+            <template #actions>
+              <a-button type="primary" @click="addInstallItem(item)">{{ 
$t('common.add') }}</a-button>
+            </template>
+            <a-list-item-meta>
+              <template #title>
+                <div class="ellipsis item-name" :title="item.displayName">
+                  <template v-for="(fragment, i) in 
splitSearchStr(item.displayName)">
+                    <mark v-if="fragment.toLowerCase() === 
searchStr.toLowerCase()" :key="i" class="highlight">
+                      {{ fragment }}
+                    </mark>
+                    <template v-else>{{ fragment }}</template>
+                  </template>
+                </div>
+              </template>
+              <template #description>
+                <div class="ellipsis" :title="item.desc">
+                  <template v-for="(fragment, i) in splitSearchStr(item.desc)">
+                    <mark v-if="fragment.toLowerCase() === 
searchStr.toLowerCase()" :key="i" class="highlight">
+                      {{ fragment }}
+                    </mark>
+                    <template v-else>{{ fragment }}</template>
+                  </template>
+                </div>
+              </template>
+              <template #avatar>
+                <a-avatar
+                  v-if="item.displayName"
+                  :src="usePngImage(item.displayName.toLowerCase())"
+                  :size="54"
+                  class="header-icon"
+                />
+              </template>
+            </a-list-item-meta>
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+    <a-divider type="vertical" class="divider" />
+    <div>
+      <div class="list-title">
+        <div>{{ $t('service.pending_installation_services') }}</div>
+      </div>
+      <a-list item-layout="horizontal" :data-source="state.selectedData">
+        <template #renderItem="{ item }">
+          <a-list-item>
+            <template #actions>
+              <a-button danger type="primary" @click="removeInstallItem(item)">
+                {{ $t('common.remove') }}
+              </a-button>
+            </template>
+            <a-list-item-meta>
+              <template #title>
+                <div class="ellipsis item-name" 
:data-tooltip="item.displayName">
+                  {{ item.displayName }}
+                </div>
+              </template>
+              <template #description>
+                <div class="ellipsis" :data-tooltip="item.desc">
+                  {{ item.desc }}
+                </div>
+              </template>
+              <template #avatar>
+                <a-avatar
+                  v-if="item.displayName"
+                  :src="usePngImage(item.displayName.toLowerCase())"
+                  :size="54"
+                  class="header-icon"
+                />
+              </template>
+            </a-list-item-meta>
+          </a-list-item>
+        </template>
+      </a-list>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+  .highlight {
+    background-color: rgb(255, 192, 105);
+    padding: 0px;
+  }
+
+  .item-name {
+    font-size: 16px;
+  }
+
+  .service-selector {
+    display: grid;
+    grid-template-columns: 1fr auto 1fr;
+    grid-template-rows: auto;
+    justify-content: space-between;
+    .list-title {
+      display: flex;
+      height: 32px;
+      align-items: center;
+      justify-content: space-between;
+      font-weight: 500;
+      border-bottom: 1px solid $color-border;
+      padding-bottom: 16px;
+      .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;
+    }
+  }
+</style>
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
new file mode 100644
index 00000000..e3924cef
--- /dev/null
+++ 
b/bigtop-manager-ui/src/components/create-service/components/tree-selector.vue
@@ -0,0 +1,110 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue'
+  import { type TreeProps, Empty } from 'ant-design-vue'
+  import type { DataNode, FieldNames, Key } from 
'ant-design-vue/es/vc-tree/interface'
+
+  interface Props {
+    tree: any
+    fieldNames?: FieldNames
+    selectable?: boolean
+  }
+
+  interface Emits {
+    (event: 'change', expandSelectedKeyPath: string): void
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    selectable: true,
+    fieldNames: () => ({ children: 'children', title: 'title', key: 'key' })
+  })
+
+  const emits = defineEmits<Emits>()
+  const checkSelectedKeys = ref<Key[]>([])
+
+  const setupDefaultSelectKey = (treeNode: DataNode, fieldNames: 
Required<FieldNames>) => {
+    const { children, key } = fieldNames
+    const stack: string[] = []
+    const pathParts: string[] = []
+
+    function traverse(node: DataNode, stack: string[]): boolean {
+      stack.push(node[key])
+      if (!node[children] || node[children].length === 0) {
+        pathParts.push(...stack)
+        return true
+      }
+      for (const child of node[children]) {
+        if (traverse(child, stack)) {
+          return true
+        }
+      }
+      stack.pop()
+      return false
+    }
+
+    traverse(treeNode, stack)
+    return pathParts.join('/')
+  }
+
+  const handleSelect: TreeProps['onSelect'] = (selectedKeys, e) => {
+    const selectedKey = selectedKeys[0]
+    if (!selectedKey) {
+      return
+    }
+    const checkSelectedKey = checkSelectedKeys.value[0]
+    if (selectedKey !== checkSelectedKey) {
+      const keyPath = `${e.node.parent?.key}/${selectedKey}`
+      checkSelectedKeys.value = selectedKeys
+      emits('change', keyPath)
+    }
+  }
+
+  watch(
+    () => props.tree,
+    (val) => {
+      if (val[0]) {
+        const findPath = setupDefaultSelectKey(val[0], props.fieldNames as 
Required<FieldNames>)
+        const selectedKey = findPath.split('/').at(-1)
+        checkSelectedKeys.value = selectedKey ? [selectedKey] : []
+        emits('change', findPath)
+      }
+    },
+    {
+      immediate: true
+    }
+  )
+</script>
+
+<template>
+  <div class="sidebar">
+    <a-tree
+      v-if="$props.tree.length > 0"
+      :selected-keys="checkSelectedKeys"
+      :selectable="$props.selectable"
+      :tree-data="$props.tree"
+      :field-names="$props.fieldNames"
+      @select="handleSelect"
+    />
+    <a-empty v-else :image="Empty.PRESENTED_IMAGE_SIMPLE" />
+  </div>
+</template>
+
+<style lang="scss" scoped></style>
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
new file mode 100644
index 00000000..702cd8d7
--- /dev/null
+++ 
b/bigtop-manager-ui/src/components/create-service/components/use-create-service.ts
@@ -0,0 +1,230 @@
+/*
+ * 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, createVNode, ref, watch, effectScope, Ref, ComputedRef } 
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 { 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 { ServiceVO } from '@/api/service/types'
+
+interface ProcessResult {
+  success: boolean
+  conflictService?: ExpandServiceVO
+}
+
+const scope = effectScope()
+let isChange = false
+let selectedServices: 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[]>([])
+    afterCreateRes = ref<CommandVO>({ id: undefined })
+    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 route = useRoute()
+  const { t } = useI18n()
+  const processedServices = ref(new Set())
+  const { current, stepsLimit, previousStep, nextStep } = useSteps(steps.value)
+
+  watch(
+    () => selectedServices.value,
+    () => {
+      processedServices.value = new Set(selectedServices.value.map((v) => 
v.name))
+    },
+    {
+      deep: true
+    }
+  )
+
+  const commandRequest = ref<CommandRequest>({
+    command: 'Add',
+    commandLevel: 'service',
+    clusterId: parseInt(route.params.id as string)
+  })
+
+  const allComps = computed(() => {
+    return new Map(selectedServices.value.flatMap((s) => 
s.components!.map((comp) => [comp.name, comp])))
+  })
+
+  const setDataByCurrent = (val: ExpandServiceVO[]) => {
+    selectedServices.value = 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,
+      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 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(',')]))
+    return true
+  }
+
+  const processDependencies = async (
+    targetService: ExpandServiceVO,
+    serviceMap: Map<string, ExpandServiceVO>,
+    servicesOfInfra: ExpandServiceVO[],
+    collected: ExpandServiceVO[]
+  ): Promise<ProcessResult> => {
+    const dependencies = targetService.requiredServices || []
+
+    if (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
+
+      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 handlePreSelectedServiceDependencies = async (preSelectedService: 
ExpandServiceVO) => {
+    const serviceMap = new Map(servicesOfExcludeInfra.value.map((s) => [s.name 
as string, s as ExpandServiceVO]))
+    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 (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 {
+      const formatData = transformServiceData(selectedServices.value)
+      commandRequest.value.serviceCommands = formatData
+      afterCreateRes.value = await execCommand(commandRequest.value)
+      return true
+    } catch (error) {
+      console.log('error :>> ', error)
+      return false
+    }
+  }
+
+  return {
+    steps,
+    current,
+    stepsLimit,
+    selectedServices,
+    servicesOfExcludeInfra,
+    allComps,
+    afterCreateRes,
+    setDataByCurrent,
+    updateHostsForComponent,
+    confirmServiceDependencies,
+    createService,
+    previousStep,
+    nextStep,
+    scope
+  }
+}
+
+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
new file mode 100644
index 00000000..7459dffd
--- /dev/null
+++ b/bigtop-manager-ui/src/components/create-service/create.vue
@@ -0,0 +1,174 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+
+<script setup lang="ts">
+  import { computed, onUnmounted, ref, shallowRef } from 'vue'
+  import { message } from 'ant-design-vue'
+  import { useI18n } from 'vue-i18n'
+  import useCreateService from './components/use-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,
+    afterCreateRes,
+    selectedServices,
+    setDataByCurrent,
+    previousStep,
+    nextStep,
+    createService,
+    confirmServiceDependencies
+  } = useCreateService()
+  const compRef = ref<any>()
+  const currComp = computed(() => components.value[current.value])
+
+  const validateServiceSelection = async () => {
+    if (selectedServices.value.length === 0) {
+      message.error(t('service.service_selection'))
+      return false
+    } else {
+      return await validateDependenciesOfServiceSelection()
+    }
+  }
+
+  const validateDependenciesOfServiceSelection = async () => {
+    let selectedServiceNames = new Set(selectedServices.value.map((service) => 
service.name))
+    for (const service of selectedServices.value) {
+      const res = await confirmServiceDependencies(service)
+      if (res.length === 0) {
+        return false
+      }
+      const filter = res.filter((service) => 
!selectedServiceNames.has(service.name))
+      filter.forEach((service) => {
+        compRef.value.addInstallItem(service)
+        selectedServiceNames.add(service.name)
+      })
+    }
+    return true
+  }
+
+  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'))
+    }
+    return isValid
+  }
+
+  const stepValidators = [validateServiceSelection, 
validateComponentAssignments, () => true]
+
+  const proceedToNextStep = async () => {
+    if (current.value < 3) {
+      ;(await stepValidators[current.value]()) && nextStep()
+    } else if (current.value === 3) {
+      const state = await createService()
+      if (state) {
+        nextStep()
+      }
+    }
+  }
+
+  onUnmounted(() => {
+    scope.stop()
+    setDataByCurrent([])
+  })
+</script>
+
+<template>
+  <div class="infra-creation">
+    <header-card>
+      <div class="steps-wrp">
+        <a-steps :current="current">
+          <a-step v-for="step in steps" :key="step" :disabled="true">
+            <template #title>
+              <span>{{ $t(step) }}</span>
+            </template>
+          </a-step>
+        </a-steps>
+      </div>
+    </header-card>
+    <main-card>
+      <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>
+        </div>
+      </template>
+      <keep-alive>
+        <component :is="currComp" ref="compRef" :is-view="current === 3" />
+      </keep-alive>
+      <component-installer v-if="current == stepsLimit" 
:step-data="afterCreateRes" />
+      <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">
+            {{ $t('common.prev') }}
+          </a-button>
+          <template v-if="current >= 0 && current <= stepsLimit - 1">
+            <a-button type="primary" @click="proceedToNextStep">
+              {{ $t('common.next') }}
+            </a-button>
+          </template>
+          <a-button v-show="current === stepsLimit" type="primary" 
@click="$router.go(-1)">
+            {{ $t('common.done') }}
+          </a-button>
+        </a-space>
+      </div>
+    </main-card>
+  </div>
+</template>
+
+<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;
+    }
+  }
+  .step-content {
+    padding-block: $space-md;
+  }
+  .step-action {
+    text-align: end;
+    margin-top: $space-md;
+  }
+</style>
diff --git a/bigtop-manager-ui/src/composables/use-base-table.ts 
b/bigtop-manager-ui/src/composables/use-base-table.ts
index 5bb04cbf..2354c63d 100644
--- a/bigtop-manager-ui/src/composables/use-base-table.ts
+++ b/bigtop-manager-ui/src/composables/use-base-table.ts
@@ -28,6 +28,7 @@ export interface UseBaseTableProps<T = any> {
   columns: TableColumnType[]
   rows?: T[]
   pagination?: PaginationType
+  onChangeCallback?: () => void
 }
 const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
   const { columns, rows, pagination } = props
@@ -68,6 +69,10 @@ const useBaseTable = <T>(props: UseBaseTableProps<T>) => {
       pageNum: pagination.current,
       pageSize: pagination.pageSize
     })
+
+    if (props.onChangeCallback) {
+      props.onChangeCallback()
+    }
   }
 
   const resetState = () => {
@@ -82,6 +87,9 @@ 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/store/stack/index.ts 
b/bigtop-manager-ui/src/composables/use-steps.ts
similarity index 63%
copy from bigtop-manager-ui/src/store/stack/index.ts
copy to bigtop-manager-ui/src/composables/use-steps.ts
index 02d7884c..094694fb 100644
--- a/bigtop-manager-ui/src/store/stack/index.ts
+++ b/bigtop-manager-ui/src/composables/use-steps.ts
@@ -17,28 +17,30 @@
  * under the License.
  */
 
-import { getStacks } from '@/api/stack'
-import { StackVO } from '@/api/stack/types'
-import { defineStore } from 'pinia'
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 
-export const useStackStore = defineStore(
-  'stack',
-  () => {
-    const stacks = ref<StackVO[]>([])
-    const loadStacks = async () => {
-      const data = await getStacks()
-      stacks.value = data
-    }
+const useSteps = (stepList: string[]) => {
+  const current = ref(0)
+  const stepsLimit = computed(() => stepList.length - 1)
 
-    return {
-      stacks,
-      loadStacks
+  const previousStep = () => {
+    if (current.value > 0) {
+      current.value = current.value - 1
     }
-  },
-  {
-    persist: {
-      storage: sessionStorage
+  }
+
+  const nextStep = () => {
+    if (current.value < stepsLimit.value) {
+      current.value = current.value + 1
     }
   }
-)
+
+  return {
+    current,
+    stepsLimit,
+    previousStep,
+    nextStep
+  }
+}
+
+export default useSteps
diff --git a/bigtop-manager-ui/src/enums/state.ts 
b/bigtop-manager-ui/src/enums/state.ts
index 5f4cee82..2a775cba 100644
--- a/bigtop-manager-ui/src/enums/state.ts
+++ b/bigtop-manager-ui/src/enums/state.ts
@@ -29,13 +29,13 @@ export enum StatusColors {
 export enum CommonStatus {
   healthy = 'success',
   unhealthy = 'error',
-  unknow = 'warning'
+  unknown = 'warning'
 }
 
 export enum CommonStatusTexts {
   'healthy',
   'unhealthy',
-  'unknow'
+  'unknown'
 }
 
 export enum JobState {
diff --git a/bigtop-manager-ui/src/layouts/default.vue 
b/bigtop-manager-ui/src/layouts/default.vue
index 3dbf230a..1b5e45b2 100644
--- a/bigtop-manager-ui/src/layouts/default.vue
+++ b/bigtop-manager-ui/src/layouts/default.vue
@@ -26,7 +26,7 @@
     <div class="default">
       <img :src="usePngImage('default')" />
       <a-typography-text>{{ $t('cluster.cluster_unavailable_message') 
}}</a-typography-text>
-      <a-typography-link underline @click="() => $router.push({ name: 
'ClusterCreate' })">
+      <a-typography-link underline @click="() => $router.push({ name: 
'CreateCluster' })">
         {{ $t('menu.create') }}
       </a-typography-link>
     </div>
diff --git a/bigtop-manager-ui/src/layouts/sider.vue 
b/bigtop-manager-ui/src/layouts/sider.vue
index ba88658d..09b55480 100644
--- a/bigtop-manager-ui/src/layouts/sider.vue
+++ b/bigtop-manager-ui/src/layouts/sider.vue
@@ -66,7 +66,7 @@
   }
 
   const addCluster = () => {
-    router.push({ name: 'ClusterCreate' })
+    router.push({ name: 'CreateCluster' })
   }
 
   const onSiderClick = ({ key }: any) => {
@@ -111,7 +111,7 @@
         </template>
       </template>
     </a-menu>
-    <div v-show="menuStore.isClusterCreateVisible">
+    <div v-show="menuStore.isCreateClusterVisible">
       <a-divider />
       <div class="create-option">
         <a-button type="primary" ghost @click="addCluster">
diff --git a/bigtop-manager-ui/src/locales/en_US/common.ts 
b/bigtop-manager-ui/src/locales/en_US/common.ts
index 86a78908..61040ccf 100644
--- a/bigtop-manager-ui/src/locales/en_US/common.ts
+++ b/bigtop-manager-ui/src/locales/en_US/common.ts
@@ -26,7 +26,7 @@ export default {
   status: 'Status',
   healthy: 'Healthy',
   unhealthy: 'Unhealthy',
-  unknow: 'Unknown',
+  unknown: 'Unknown',
   failed: 'Failed',
   success: 'Normal',
   edit: 'Edit',
@@ -106,5 +106,7 @@ export default {
   stop: 'Stop {0}',
   add: 'Add {0}',
   more_operations: 'More Operations',
-  ok: 'Ok'
+  ok: 'Ok',
+  yes: 'Yes',
+  no: 'No'
 }
diff --git a/bigtop-manager-ui/src/locales/en_US/service.ts 
b/bigtop-manager-ui/src/locales/en_US/service.ts
index 26d0b328..6b14adcb 100644
--- a/bigtop-manager-ui/src/locales/en_US/service.ts
+++ b/bigtop-manager-ui/src/locales/en_US/service.ts
@@ -18,5 +18,19 @@
  */
 export default {
   name: 'Service Name',
-  required_restart: 'Restart'
+  required_restart: 'Restart',
+  select_service: 'Services',
+  assign_component: 'Assign Component',
+  configure_service: 'Configure Service',
+  service_overview: 'Service Overview',
+  install_component: 'Install Component',
+  service_list: 'Service List',
+  pending_installation_services: 'Selected Services',
+  select_host: 'Select  Host',
+  host_preview: 'Host Preview',
+  please_enter_search_keyword: 'Please enter search keyword',
+  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?'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/common.ts 
b/bigtop-manager-ui/src/locales/zh_CN/common.ts
index 3005772f..cc7880f4 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/common.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/common.ts
@@ -26,7 +26,7 @@ export default {
   status: '状态',
   healthy: '健康',
   unhealthy: '不健康',
-  unknow: '未知',
+  unknown: '未知',
   success: '正常',
   failed: '异常',
   edit: '编辑',
@@ -106,5 +106,7 @@ export default {
   add: '添加{0}',
   reset: '重置',
   more_operations: '其他操作',
-  ok: '确认'
+  ok: '确认',
+  yes: '是',
+  no: '否'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/service.ts 
b/bigtop-manager-ui/src/locales/zh_CN/service.ts
index e2f6ad20..db6692b5 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/service.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/service.ts
@@ -18,5 +18,19 @@
  */
 export default {
   name: '服务名',
-  required_restart: '需要重启'
+  required_restart: '需要重启',
+  select_service: '选择服务',
+  assign_component: '分配组件',
+  configure_service: '配置服务',
+  service_overview: '服务总览',
+  install_component: '安装组件',
+  service_list: '服务列表',
+  pending_installation_services: '待安装服务',
+  select_host: '选择主机',
+  host_preview: '主机预览',
+  please_enter_search_keyword: '请输入搜索关键字',
+  component_host_assignment: '每个组件至少分配一个主机',
+  service_selection: '请选择需要安装的服务',
+  dependencies_conflict_msg: '{0} 依赖于基础服务 {1},请先前往安装',
+  dependencies_msg: '{0} 依赖于服务 {1}, 是否一起安装?'
 }
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
index 8bb2b193..c10c0920 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
+++ 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
@@ -19,6 +19,7 @@
 
 <script setup lang="ts">
   import { computed, onMounted, reactive, ref, shallowRef, toRefs } from 'vue'
+  import { Empty } from 'ant-design-vue'
   import { getJobDetails, retryJob } from '@/api/job'
   import { CommandVO } from '@/api/command/types'
   import LogsView, { type LogViewProps } from '@/components/log-view/index.vue'
@@ -120,7 +121,7 @@
 
 <template>
   <a-spin :spinning="spinning">
-    <a-empty v-if="stages.length == 0" />
+    <a-empty v-if="stages.length == 0" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
     <div v-else class="check-workflow">
       <div class="retry">
         <a-button v-if="stepData.state === 'Failed'" type="link" 
@click="handleRetryJob">{{
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 a9428be5..9d809340 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
@@ -25,6 +25,7 @@
   import { message } from 'ant-design-vue'
   import { InstalledStatusVO, Status } from '@/api/hosts/types'
   import { execCommand } from '@/api/command'
+  import useSteps from '@/composables/use-steps'
   import ClusterBase from './components/cluster-base.vue'
   import ComponentInfo from './components/component-info.vue'
   import HostConfig from './components/host-config.vue'
@@ -33,7 +34,6 @@
 
   const { t } = useI18n()
   const menuStore = useMenuStore()
-  const current = ref(0)
   const compRef = ref<any>()
   const installing = ref(false)
   const stepData = ref<[Partial<ClusterCommandReq>, any, HostReq[], 
CommandVO]>([{}, {}, [], {}])
@@ -44,7 +44,6 @@
   const installStatus = shallowRef<InstalledStatusVO[]>([])
   const components = shallowRef<any[]>([ClusterBase, ComponentInfo, 
HostConfig, CheckWorkflow])
   const isInstall = computed(() => current.value === 2)
-  const stepsLimit = computed(() => steps.value.length - 1)
   const hasUnknowHost = computed(() => stepData.value[2].filter((v) => 
v.status === Status.Unknown).length == 0)
   const allInstallSuccess = computed(
     () =>
@@ -64,6 +63,8 @@
     return components.value[current.value]
   })
 
+  const { current, stepsLimit, previousStep, nextStep } = useSteps(steps.value)
+
   const updateData = (val: Partial<ClusterCommandReq> | any | HostReq[]) => {
     stepData.value[current.value] = val
   }
@@ -80,18 +81,6 @@
     }
   }
 
-  const previousStep = () => {
-    if (current.value > 0) {
-      current.value = current.value - 1
-    }
-  }
-
-  const nextStep = async () => {
-    if (current.value < stepsLimit.value) {
-      current.value = current.value + 1
-    }
-  }
-
   const prepareNextStep = async () => {
     if (current.value === 0) {
       const check = await compRef.value.check()
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 4637b96b..ca7a6616 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -94,7 +94,7 @@
           value: 2
         },
         {
-          text: t('common.unknow'),
+          text: t('common.unknown'),
           value: 3
         }
       ]
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 71ab061b..9baf3300 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
@@ -25,6 +25,7 @@
   import { execCommand } from '@/api/command'
   import { Command } from '@/api/command/types'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+  import { useRouter } from 'vue-router'
   import Overview from './overview.vue'
   import Service from './service.vue'
   import Host from './host.vue'
@@ -35,13 +36,14 @@
   import type { ClusterStatusType } from '@/api/cluster/types'
 
   const { t } = useI18n()
+  const router = useRouter()
   const clusterStore = useClusterStore()
   const { currCluster, loading } = storeToRefs(clusterStore)
   const activeKey = ref('1')
   const statusColors = shallowRef<Record<ClusterStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
     2: 'unhealthy',
-    3: 'unknow'
+    3: 'unknown'
   })
   const tabs = computed((): TabItem[] => [
     {
@@ -114,7 +116,7 @@
   }
 
   const addService: GroupItem['clickEvent'] = () => {
-    console.log('add :>> ')
+    router.push({ name: 'CreateService' })
   }
 
   onMounted(() => {
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
index 3ec9aed4..131ff884 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
@@ -18,16 +18,19 @@
 -->
 
 <script setup lang="ts">
-  import { computed, ref, shallowRef, useAttrs } from 'vue'
+  import { computed, onActivated, ref, shallowRef, useAttrs } from 'vue'
   import { useI18n } from 'vue-i18n'
   import { storeToRefs } from 'pinia'
   import { useServiceStore } from '@/store/service'
   import { formatFromByte } from '@/utils/storage'
+  import { usePngImage } from '@/utils/tools'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import GaugeChart from './components/gauge-chart.vue'
   import CategoryChart from './components/category-chart.vue'
   import type { ClusterStatusType, ClusterVO } from '@/api/cluster/types'
-  import type { MenuProps } from 'ant-design-vue'
+  import { type MenuProps, Empty } from 'ant-design-vue'
+  import type { ServiceListParams } from '@/api/service/types'
+  import type { StackVO } from '@/api/stack/types'
 
   type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
   type TimeRangeItem = {
@@ -48,9 +51,9 @@
   const statusColors = shallowRef<Record<ClusterStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
     2: 'unhealthy',
-    3: 'unknow'
+    3: 'unknown'
   })
-  const { locateStackWithService } = storeToRefs(serviceStore)
+  const { locateStackWithService, serviceNames } = storeToRefs(serviceStore)
   const clusterDetail = computed(() => ({
     ...attrs,
     totalMemory: formatFromByte(attrs.totalMemory as number),
@@ -104,7 +107,6 @@
     }
   })
   const detailKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ClusterVO)[])
-  const serviceStack = computed(() => locateStackWithService.value)
   const serviceOperates = computed(() => [
     {
       action: 'start',
@@ -127,6 +129,18 @@
   const handleTimeRange = (time: TimeRangeItem) => {
     currTimeRange.value = time.text
   }
+
+  const getServices = (filters?: ServiceListParams) => {
+    attrs.id != undefined && serviceStore.getServices(attrs.id, filters)
+  }
+
+  const servicesFromCurrentCluster = (stack: StackVO) => {
+    return stack.services.filter((v) => serviceNames.value.includes(v.name))
+  }
+
+  onActivated(() => {
+    getServices()
+  })
 </script>
 
 <template>
@@ -183,13 +197,13 @@
           </div>
         </div>
         <!-- service info -->
-        <template v-if="serviceStack.length == 0">
+        <template v-if="locateStackWithService.length == 0">
           <div class="service-info">
             <div class="box-title">
               <a-typography-text strong :content="$t('overview.service_info')" 
/>
             </div>
             <div class="box-empty">
-              <a-empty />
+              <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
             </div>
           </div>
         </template>
@@ -197,15 +211,18 @@
           <template #title>
             <a-typography-text strong :content="$t('overview.service_info')" />
           </template>
-          <a-descriptions-item v-for="stack in serviceStack" 
:key="stack.stackName">
+          <a-descriptions-item v-for="stack in locateStackWithService" 
:key="stack.stackName">
             <template #label>
               <div class="desc-sub-label">
-                <a-typography-text strong :content="stack.stackName" />
-                <a-typography-text type="secondary" 
:content="stack.stackVersion" />
+                <a-typography-text
+                  strong
+                  :content="stack.stackName.charAt(0).toUpperCase() + 
stack.stackName.slice(1) + ' Stack'"
+                />
+                <a-typography-text type="secondary" 
:content="`${stack.stackName}-${stack.stackVersion}`" />
               </div>
             </template>
-            <div v-for="service in stack.services" :key="service.id" 
class="service-item">
-              <a-avatar shape="square" :size="16" />
+            <div v-for="service in servicesFromCurrentCluster(stack)" 
:key="service.id" class="service-item">
+              <a-avatar v-if="service.name" 
:src="usePngImage(service.name.toLowerCase())" :size="16" />
               <a-typography-text :content="service.displayName" />
               <a-dropdown :trigger="['click']">
                 <a-button type="text" shape="circle" size="small">
@@ -272,6 +289,12 @@
 </template>
 
 <style lang="scss" scoped>
+  :deep(.ant-avatar) {
+    border-radius: 4px;
+    img {
+      object-fit: contain !important;
+    }
+  }
   .box {
     &-title {
       @include flexbox($justify: space-between);
@@ -316,8 +339,8 @@
     grid-template-columns: auto 1fr auto;
     gap: $space-md;
     align-items: center;
-    padding: $space-md;
-    border-bottom: 1px solid #3b2020;
+    padding: 12px 16px;
+    border-bottom: 1px solid #f0f0f0;
   }
 
   .chart-item-wrp {
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 f8062f57..80b2cc56 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -19,6 +19,7 @@
 
 <script setup lang="ts">
   import { computed, onActivated, shallowRef, toRefs, useAttrs } from 'vue'
+  import { Empty } from 'ant-design-vue'
   import { usePngImage } from '@/utils/tools'
   import { useI18n } from 'vue-i18n'
   import { useServiceStore } from '@/store/service'
@@ -36,8 +37,9 @@
   const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
     2: 'unhealthy',
-    3: 'unknow'
+    3: 'unknown'
   })
+
   const actionGroups = shallowRef<GroupItem[]>([
     {
       action: 'start',
@@ -123,7 +125,7 @@
 <template>
   <a-spin :spinning="loading" class="service">
     <filter-form :filter-items="filterFormItems" @filter="getServices" />
-    <a-empty v-if="services.length == 0" style="width: 100%" />
+    <a-empty v-if="services.length == 0" style="width: 100%" 
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
     <template v-else>
       <a-card v-for="item in services" :key="item.id" :hoverable="true" 
class="service-item">
         <div class="header">
@@ -134,10 +136,10 @@
               <span class="small-gray">{{ item.version }}</span>
             </div>
             <div class="header-base-status">
-              <a-tag :color="statusColors[item.status]">
+              <a-tag :color="CommonStatus[statusColors[item.status]]">
                 <div class="header-base-status-inner">
                   <status-dot :color="CommonStatus[statusColors[item.status]]" 
/>
-                  <span class="small">{{ 
$t(`common.${CommonStatusTexts[item.status]}`) }}</span>
+                  <span class="small">{{ 
$t(`common.${statusColors[item.status]}`) }}</span>
                 </div>
               </a-tag>
             </div>
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 dea9aa3b..6cfb5856 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
@@ -20,7 +20,9 @@
 <script setup lang="ts"></script>
 
 <template>
-  <div> infra list</div>
+  <div>
+    <span> infra list </span>
+  </div>
 </template>
 
 <style scoped></style>
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts 
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index 32fcf6c8..54ce44ae 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -56,23 +56,42 @@ const routes: RouteRecordRaw[] = [
             }
           },
           {
-            name: 'ClusterCreate',
-            path: 'create',
+            name: 'CreateCluster',
+            path: 'create-cluster',
             component: () => 
import('@/pages/cluster-manage/cluster/create.vue'),
             meta: {
               hidden: true
             }
+          },
+          {
+            name: 'CreateService',
+            path: `${RouteExceptions.DYNAMIC_ROUTE_MATCH}/create-service`,
+            component: () => import('@/components/create-service/create.vue'),
+            meta: {
+              hidden: true
+            }
           }
         ]
       },
       {
         name: 'Infrastructures',
         path: 'infrastructures',
-        component: () => 
import('@/pages/cluster-manage/infrastructures/index.vue'),
+        redirect: '/cluster-manage/infrastructures/list',
         meta: {
           icon: 'infrastructures',
           title: 'menu.infra'
-        }
+        },
+        children: [
+          {
+            name: 'InfraList',
+            path: 'list',
+            component: () => 
import('@/pages/cluster-manage/infrastructures/index.vue'),
+            meta: {
+              hidden: true,
+              activeMenu: '/cluster-manage/infrastructures'
+            }
+          }
+        ]
       },
       {
         name: 'Components',
@@ -93,7 +112,7 @@ const routes: RouteRecordRaw[] = [
         },
         children: [
           {
-            name: 'List',
+            name: 'HostList',
             path: 'list',
             component: () => import('@/pages/cluster-manage/hosts/index.vue'),
             meta: {
@@ -102,8 +121,8 @@ const routes: RouteRecordRaw[] = [
             }
           },
           {
-            name: 'HostCreate',
-            path: 'addhost',
+            name: 'HostCreation',
+            path: 'add',
             component: () => import('@/pages/cluster-manage/hosts/create.vue'),
             meta: {
               hidden: true,
diff --git a/bigtop-manager-ui/src/store/menu/index.ts 
b/bigtop-manager-ui/src/store/menu/index.ts
index 72df905e..5fbfb470 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -40,7 +40,7 @@ export const useMenuStore = defineStore(
     const headerSelectedKey = ref(headerMenus.value[0].path)
 
     const hasCluster = computed(() => clusters.value.length > 0)
-    const isClusterCreateVisible = computed(() => 
RouteExceptions.SPECIAL_ROUTE_PATH.includes(route.matched[0].path))
+    const isCreateClusterVisible = computed(() => 
RouteExceptions.SPECIAL_ROUTE_PATH.includes(route.matched[0].path))
     const isDynamicRouteMatched = computed(() => {
       const path = route.matched.at(-1)?.path
       return path?.includes(RouteExceptions.DYNAMIC_ROUTE_MATCH)
@@ -63,7 +63,10 @@ export const useMenuStore = defineStore(
 
     watchEffect(() => {
       // resolve highlight menu
-      const activeMenu = route.meta.activeMenu || route.path
+      let activeMenu = route.meta.activeMenu || route.path
+      if (route.name === 'CreateService') {
+        activeMenu = route.path.split('/').slice(0, -1).join('/')
+      }
       const matchedNames = [RouteExceptions.SPECIAL_ROUTE_NAME, 
RouteExceptions.DEFAULT_ROUTE_NAME] as string[]
       headerSelectedKey.value = route.matched[0].path
 
@@ -160,7 +163,7 @@ export const useMenuStore = defineStore(
       siderMenus,
       headerSelectedKey,
       siderMenuSelectedKey,
-      isClusterCreateVisible,
+      isCreateClusterVisible,
       isDynamicRouteMatched,
       setUpMenu,
       cleanUpMenu,
diff --git a/bigtop-manager-ui/src/store/service/index.ts 
b/bigtop-manager-ui/src/store/service/index.ts
index 330903f3..910bbf88 100644
--- a/bigtop-manager-ui/src/store/service/index.ts
+++ b/bigtop-manager-ui/src/store/service/index.ts
@@ -33,11 +33,11 @@ export const useServiceStore = defineStore(
     const { stacks } = storeToRefs(stackStore)
 
     const serviceNames = computed(() => services.value.map((v) => v.name))
-    const locateStackWithService = computed(() =>
-      stacks.value.filter((item) =>
+    const locateStackWithService = computed(() => {
+      return stacks.value.filter((item) =>
         item.services.some((service) => service.name && 
serviceNames.value.includes(service.name))
       )
-    )
+    })
 
     const getServices = async (clusterId: number, filterParams?: 
ServiceListParams) => {
       try {
@@ -56,6 +56,7 @@ export const useServiceStore = defineStore(
       services,
       loading,
       getServices,
+      serviceNames,
       locateStackWithService
     }
   },
diff --git a/bigtop-manager-ui/src/store/stack/index.ts 
b/bigtop-manager-ui/src/store/stack/index.ts
index 02d7884c..c667ad3d 100644
--- a/bigtop-manager-ui/src/store/stack/index.ts
+++ b/bigtop-manager-ui/src/store/stack/index.ts
@@ -17,23 +17,37 @@
  * under the License.
  */
 
+import { ServiceVO } from '@/api/service/types'
 import { getStacks } from '@/api/stack'
 import { StackVO } from '@/api/stack/types'
 import { defineStore } from 'pinia'
-import { ref } from 'vue'
+import { shallowRef } from 'vue'
+
+export type ExpandServiceVO = ServiceVO & { order: number }
 
 export const useStackStore = defineStore(
   'stack',
   () => {
-    const stacks = ref<StackVO[]>([])
+    const stacks = shallowRef<StackVO[]>([])
+
     const loadStacks = async () => {
-      const data = await getStacks()
-      stacks.value = data
+      try {
+        const data = await getStacks()
+        stacks.value = data
+      } catch (error) {
+        console.log('error :>> ', error)
+      }
+    }
+
+    const getServicesByExclude = (exclude?: string[], isOrder = true): 
ExpandServiceVO[] | ServiceVO[] => {
+      const filterData = stacks.value.flatMap((stack) => 
(exclude?.includes(stack.stackName) ? [] : stack.services))
+      return isOrder ? filterData.map((service, index) => ({ ...service, 
order: index })) : filterData
     }
 
     return {
       stacks,
-      loadStacks
+      loadStacks,
+      getServicesByExclude
     }
   },
   {
diff --git a/bigtop-manager-ui/src/styles/index.scss 
b/bigtop-manager-ui/src/styles/index.scss
index 6689600f..84c3bac3 100644
--- a/bigtop-manager-ui/src/styles/index.scss
+++ b/bigtop-manager-ui/src/styles/index.scss
@@ -60,3 +60,13 @@
   font-weight: 500;
   margin-bottom: $space-md;
 }
+
+
+.ellipsis {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  width: 100%;
+  display: inline-block;
+  position: relative;
+}
\ No newline at end of file

Reply via email to