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 4c0de7c9 BIGTOP-4454: Add hostname duplication check on host addition 
(#236)
4c0de7c9 is described below

commit 4c0de7c9d88b7dac4c6f5a252f3b76024f08e786
Author: Fdefined <[email protected]>
AuthorDate: Sat Jul 12 11:03:54 2025 +0800

    BIGTOP-4454: Add hostname duplication check on host addition (#236)
---
 bigtop-manager-ui/src/assets/images/svg/warn.svg   |  25 ++
 .../src/components/common/svg-icon/index.vue       |   3 +-
 .../create-cluster/components/host-manage.vue      | 154 ++++++++-----
 .../src/components/create-cluster/create.vue       | 115 ++++++----
 .../create-host/components/conflict-resolver.vue   |  77 +++++++
 .../create-host/components/parsed-preview.vue      | 168 ++++++++++++++
 .../hosts => components/create-host}/create.vue    | 254 ++++++++-------------
 .../create-host}/install-dependencies.vue          |   8 +-
 bigtop-manager-ui/src/layouts/sider.vue            |  29 ++-
 bigtop-manager-ui/src/locales/en_US/cluster.ts     |   6 +-
 bigtop-manager-ui/src/locales/zh_CN/cluster.ts     |   6 +-
 .../src/pages/cluster-manage/cluster/host.vue      |  11 +-
 .../src/pages/cluster-manage/hosts/index.vue       |  15 +-
 bigtop-manager-ui/src/router/guard.ts              |  10 +-
 .../src/router/routes/modules/clusters.ts          |   2 +-
 bigtop-manager-ui/src/store/cluster/index.ts       |   4 -
 16 files changed, 581 insertions(+), 306 deletions(-)

diff --git a/bigtop-manager-ui/src/assets/images/svg/warn.svg 
b/bigtop-manager-ui/src/assets/images/svg/warn.svg
new file mode 100644
index 00000000..6568a199
--- /dev/null
+++ b/bigtop-manager-ui/src/assets/images/svg/warn.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+-->
+<svg xmlns="http://www.w3.org/2000/svg"; 
xmlns:xlink="http://www.w3.org/1999/xlink"; width="16" height="16"
+  viewBox="0 0 16 16" fill="none">
+  <circle cx=" 8" cy="8" r="7.33" stroke="rgba(250, 173, 20, 1)" 
stroke-width="1.333" fill="none" />
+  <path d="M8 4.5v4" stroke="rgba(250, 173, 20, 1)" stroke-width="1.333" 
stroke-linecap="round" />
+  <circle cx="8" cy="10.8" r="0.67" fill="rgba(250, 173, 20, 1)" />
+</svg>
\ No newline at end of file
diff --git a/bigtop-manager-ui/src/components/common/svg-icon/index.vue 
b/bigtop-manager-ui/src/components/common/svg-icon/index.vue
index 0d3fc9d6..0421e613 100644
--- a/bigtop-manager-ui/src/components/common/svg-icon/index.vue
+++ b/bigtop-manager-ui/src/components/common/svg-icon/index.vue
@@ -70,7 +70,8 @@
     stop: IconSvgStop,
     success: IconSvgSuccess,
     unknown: IconSvgUnknown,
-    'carbon-language': IconSvgCarbonLanguage
+    'carbon-language': IconSvgCarbonLanguage,
+    warn: IconSvgWarn
   }
 
   const props = defineProps<{ name: string | undefined }>()
diff --git 
a/bigtop-manager-ui/src/components/create-cluster/components/host-manage.vue 
b/bigtop-manager-ui/src/components/create-cluster/components/host-manage.vue
index e51f2617..9886e430 100644
--- a/bigtop-manager-ui/src/components/create-cluster/components/host-manage.vue
+++ b/bigtop-manager-ui/src/components/create-cluster/components/host-manage.vue
@@ -18,28 +18,34 @@
 -->
 
 <script setup lang="ts">
-  import { TableColumnType } from 'ant-design-vue'
+  import { PaginationProps, TableColumnType } from 'ant-design-vue'
   import { computed, reactive, ref, watch } from 'vue'
   import { generateRandomId } from '@/utils/tools'
   import { useI18n } from 'vue-i18n'
+
   import useBaseTable from '@/composables/use-base-table'
-  import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
+  import HostCreate from '@/components/create-host/create.vue'
+
   import type { FilterConfirmProps, FilterResetProps } from 
'ant-design-vue/es/table/interface'
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { HostReq } from '@/api/command/types'
 
   type Key = string | number
+  type conflictItem = HostReq & { strategy: 'override' | 'keep' }
+
   interface TableState {
     selectedRowKeys: Key[]
     searchText: string
     searchedColumn: string
   }
+
   const { t } = useI18n()
   const props = defineProps<{ stepData: HostReq[] }>()
   const emits = defineEmits(['updateData'])
-  const data = ref<HostReq[]>([])
+
   const searchInputRef = ref()
   const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
+
   const state = reactive<TableState>({
     searchText: '',
     searchedColumn: '',
@@ -86,43 +92,19 @@
     }
   ])
 
-  const { loading, dataSource, paginationProps, onChange } = useBaseTable({
-    columns: columns.value,
-    rows: data.value
-  })
+  const { loading, dataSource, paginationProps, onChange } = 
useBaseTable<HostReq>({ columns: columns.value, rows: [] })
 
   const operations = computed((): GroupItem[] => [
-    {
-      text: 'edit',
-      clickEvent: (_item, args) => editHost(args)
-    },
-    {
-      text: 'remove',
-      danger: true,
-      clickEvent: (_item, args) => deleteHost(args)
-    }
+    { text: 'edit', clickEvent: (_item, args) => updateHost('EDIT', args) },
+    { text: 'remove', danger: true, clickEvent: (_item, args) => 
deleteHost(args) }
   ])
 
-  watch(
-    () => props.stepData,
-    (val) => {
-      dataSource.value = val
-    },
-    {
-      immediate: true
-    }
-  )
-
   const isContain = (source: string, target: string): boolean => {
     return source.toString().toLowerCase().includes(target.toLowerCase())
   }
 
   const onFilterDropdownOpenChange = (visible: boolean) => {
-    if (visible) {
-      setTimeout(() => {
-        searchInputRef.value.focus()
-      }, 100)
-    }
+    visible && setTimeout(searchInputRef.value.focus(), 100)
   }
 
   const onSelectChange = (selectedRowKeys: Key[]) => {
@@ -140,60 +122,108 @@
     state.searchText = ''
   }
 
-  const addHost = () => {
-    hostCreateRef.value?.handleOpen('ADD')
+  const updateHost = (type: 'ADD' | 'EDIT', row?: HostReq) => {
+    hostCreateRef.value?.handleOpen(type, row)
   }
 
-  const editHost = (row: HostReq) => {
-    hostCreateRef.value?.handleOpen('EDIT', row)
+  const deleteHost = (row?: HostReq) => {
+    if (row?.hostnames) {
+      dataSource.value = dataSource.value?.filter((v) => row!.key !== v.key)
+    } else {
+      dataSource.value = dataSource.value?.filter((v) => 
!state.selectedRowKeys.includes(v.key)) || []
+    }
+    updateStepData()
   }
 
   const updateStepData = () => {
-    const res = dataSource.value.map((v) => {
-      return {
-        ...v,
-        hostnames: [v.hostname]
-      }
-    })
+    Object.assign(paginationProps.value as PaginationProps, { total: 
dataSource.value.length })
+    const res = dataSource.value.map((v) => ({ ...v, hostnames: [v.hostname] 
}))
     emits('updateData', res)
   }
 
-  const addHostSuccess = (type: 'ADD' | 'EDIT', item: HostReq) => {
+  /**
+   * Merges duplicate hostnames based on strategy.
+   * @param list
+   * @param duplicateHosts
+   * @param config host config
+   */
+  const mergeByStrategy = (list: HostReq[], duplicateHosts: conflictItem[], 
config: HostReq): HostReq[] => {
+    const strategyMap = new Map<string, conflictItem>()
+    duplicateHosts.forEach((s) => strategyMap.set(s.hostname, s))
+
+    const existingHostnames = new Set<string>()
+    const mainPart: HostReq[] = []
+    const prependPart: HostReq[] = []
+
+    for (const item of list) {
+      existingHostnames.add(item.hostname)
+      const strategy = strategyMap.get(item.hostname)
+
+      if (!strategy) {
+        mainPart.push(item)
+      } else if (strategy.strategy === 'override') {
+        mainPart.push(generateNewHostItem(item.hostname, config))
+      } else {
+        mainPart.push(item)
+      }
+    }
+
+    for (const s of duplicateHosts) {
+      if (!existingHostnames.has(s.hostname)) {
+        prependPart.push(generateNewHostItem(s.hostname, config))
+      }
+    }
+
+    return [...prependPart, ...mainPart]
+  }
+
+  const generateNewHostItem = (hostname: string, config: HostReq): HostReq => 
({
+    ...config,
+    key: generateRandomId(),
+    hostname: hostname,
+    status: 'UNKNOWN'
+  })
+
+  /**
+   * Handles successful addition or editing of hosts.
+   */
+  const addHostSuccess = (type: 'ADD' | 'EDIT', item: HostReq, duplicateHosts: 
any) => {
     if (type === 'ADD') {
-      const items = item.hostnames?.map((v) => {
-        return {
-          ...item,
-          key: generateRandomId(),
-          hostname: v,
-          status: 'UNKNOWN'
-        }
-      }) as HostReq[]
-      dataSource.value?.unshift(...items)
-    } else {
+      if (duplicateHosts.length > 0) {
+        dataSource.value = mergeByStrategy(dataSource.value, duplicateHosts, 
item)
+      } else {
+        const items = item.hostnames?.map((v) => generateNewHostItem(v, item)) 
as HostReq[]
+        dataSource.value?.unshift(...items)
+      }
+    }
+
+    if (type === 'EDIT') {
       const index = dataSource.value.findIndex((data) => data.key === item.key)
+
       if (index !== -1) {
         dataSource.value[index] = item
       }
     }
+
     updateStepData()
   }
 
-  const deleteHost = (row?: HostReq) => {
-    if (!row?.key) {
-      state.selectedRowKeys.length > 0 &&
-        (dataSource.value = dataSource.value?.filter((v) => 
!state.selectedRowKeys.includes(v.key)) || [])
-    } else {
-      dataSource.value = dataSource.value?.filter((v) => row.key !== v.key)
+  watch(
+    () => props.stepData,
+    (val) => {
+      dataSource.value = JSON.parse(JSON.stringify(val))
+    },
+    {
+      immediate: true
     }
-    updateStepData()
-  }
+  )
 </script>
 
 <template>
   <div class="host-config">
     <header>
       <a-space :size="16">
-        <a-button type="primary" @click="addHost">{{ $t('cluster.add_host') 
}}</a-button>
+        <a-button type="primary" @click="updateHost('ADD')">{{ 
$t('cluster.add_host') }}</a-button>
         <a-button type="primary" danger @click="deleteHost">{{ 
$t('common.bulk_remove') }}</a-button>
       </a-space>
     </header>
@@ -246,7 +276,7 @@
         </template>
       </template>
     </a-table>
-    <host-create ref="hostCreateRef" @on-ok="addHostSuccess" />
+    <host-create ref="hostCreateRef" :current-hosts="dataSource" 
@on-ok="addHostSuccess" />
   </div>
 </template>
 
diff --git a/bigtop-manager-ui/src/components/create-cluster/create.vue 
b/bigtop-manager-ui/src/components/create-cluster/create.vue
index b1863257..96090291 100644
--- a/bigtop-manager-ui/src/components/create-cluster/create.vue
+++ b/bigtop-manager-ui/src/components/create-cluster/create.vue
@@ -37,6 +37,7 @@
 
   const { t } = useI18n()
   const menuStore = useMenuStore()
+
   const compRef = ref<any>()
   const installing = ref(false)
   const stepData = ref<[Partial<ClusterCommandReq>, any, HostReq[], 
CommandVO]>([{}, {}, [], {}])
@@ -48,16 +49,17 @@
   const installStatus = shallowRef<InstalledStatusVO[]>([])
   const components = shallowRef<any[]>([ClusterBase, ComponentInfo, 
HostManage, CheckWorkflow])
   const isInstall = computed(() => current.value === 2)
-  const hasUnknownHost = computed(() => stepData.value[2].filter((v) => 
v.status === Status.Unknown).length == 0)
-
+  const currentStepItems = computed(() => stepData.value[2])
+  const hasNoUnknownHosts = computed(() => currentStepItems.value.every((item) 
=> item.status !== Status.Unknown))
   const allInstallSuccess = computed(
     () =>
-      stepData.value[2].length != 0 &&
-      stepData.value[2].every((v) => v.status === Status.Success) &&
-      hasUnknownHost.value
+      currentStepItems.value.length > 0 &&
+      currentStepItems.value.every((item) => item.status === Status.Success) &&
+      hasNoUnknownHosts.value
   )
   const getCompName = computed(() => components.value[current.value])
   const isDone = computed(() => ['Successful', 
'Failed'].includes(stepData.value[stepData.value.length - 1].state))
+
   const steps = computed(() => [
     'cluster.cluster_info',
     'cluster.component_info',
@@ -71,18 +73,6 @@
     stepData.value[current.value] = val
   }
 
-  const createCluster = async () => {
-    try {
-      commandRequest.value.clusterCommand = stepData.value[0] as 
ClusterCommandReq
-      commandRequest.value.clusterCommand.hosts = stepData.value[2]
-      stepData.value[stepData.value.length - 1] = await 
execCommand(commandRequest.value)
-      return true
-    } catch (error) {
-      console.log('error :>> ', error)
-      return false
-    }
-  }
-
   const prepareNextStep = async () => {
     if (current.value === 0) {
       const check = await compRef.value.check()
@@ -101,42 +91,59 @@
     }
   }
 
+  /**
+   * Resolves installation dependencies by setting status and calling API.
+   * Starts polling to track progress.
+   */
   const resolveInstallDependencies = async () => {
+    const targetStepData = stepData.value[current.value]
+
     if (stepData.value[2].length == 0) {
       message.error(t('host.uninstallable'))
       return
     }
     try {
       installing.value = true
-      const data = await installDependencies(stepData.value[current.value])
-      data && pollUntilInstalled()
+      stepData.value[current.value] = setInstallItemStatus(targetStepData, 
Status.Installing)
+
+      const result = await installDependencies(stepData.value[current.value])
+      if (result) {
+        pollUntilInstalled()
+      }
     } catch (error) {
+      console.error('Error resolving dependencies:', error)
       installing.value = false
-      console.log('error :>> ', error)
+      stepData.value[current.value] = 
setInstallItemStatus(stepData.value[current.value], Status.Failed)
     }
   }
 
+  /**
+   * Records and updates current install status for items.
+   * @returns whether all items are no longer in "Installing" state
+   */
   const recordInstalledStatus = async () => {
     try {
       const data = await getInstalledStatus()
       installStatus.value = data
-      stepData.value[current.value] = 
mergeByHostname(stepData.value[current.value], data)
+      stepData.value[current.value] = 
setInstallItemStatus(stepData.value[current.value], data)
+
       return data.every((item) => item.status != Status.Installing)
     } catch (error) {
-      console.log('error :>> ', error)
+      console.error('Error recording installation status:', error)
     }
   }
 
+  /**
+   * Starts polling install status until all items are complete.
+   * @param interval polling interval in milliseconds (default: 1000)
+   */
   const pollUntilInstalled = (interval: number = 1000): void => {
     let isInitialized = false
     let intervalId: NodeJS.Timeout
 
     const poll = async () => {
       if (!isInitialized) {
-        stepData.value[current.value] = 
stepData.value[current.value].map((item: HostReq) => ({
-          ...item,
-          status: Status.Installing
-        }))
+        stepData.value[current.value] = 
setInstallItemStatus(stepData.value[current.value], Status.Installing)
         isInitialized = true
       }
       const result = await recordInstalledStatus()
@@ -150,34 +157,46 @@
     poll()
   }
 
-  const mergeByHostname = (arr1: any[], arr2: any[]): any[] => {
-    const mergedMap = new Map<string, any>()
-    for (const item of arr1) {
+  /**
+   * Merges install items with status.
+   * If status is an array, merge each item by hostname.
+   * If status is a string, apply it uniformly to all items.
+   *
+   * @param items original install item list
+   * @param status array of partial updates OR a uniform status string
+   * @returns merged install item list
+   */
+  const setInstallItemStatus = <T extends HostReq>(items: T[], status: T[] | 
string): T[] => {
+    const mergedMap = new Map<string, T>()
+
+    for (const item of items) {
       mergedMap.set(item.hostname, { ...item })
     }
-    for (const item of arr2) {
-      if (mergedMap.has(item.hostname)) {
-        mergedMap.set(item.hostname, { ...mergedMap.get(item.hostname), 
...item })
-      } else {
-        mergedMap.set(item.hostname, { ...item })
+
+    if (Array.isArray(status)) {
+      for (const item of status) {
+        const mergedMapItem = mergedMap.get(item.hostname)
+        mergedMap.set(item.hostname, mergedMapItem ? { ...mergedMapItem, 
...item } : { ...item })
+      }
+    } else {
+      for (const [hostname, item] of mergedMap) {
+        mergedMap.set(hostname, { ...item, status })
       }
     }
+
     return Array.from(mergedMap.values())
   }
 
-  // const changeStep = (step: number) => {
-  //   if (current.value > step) {
-  //     const previousCount = current.value - step
-  //     Array.from({ length: previousCount }).forEach(() => previousStep())
-  //   }
-  //   if (current.value < step) {
-  //     const nextCount = step - current.value
-  //     Array.from({ length: nextCount }).forEach(() => prepareNextStep())
-  //   }
-  // }
-
-  const onSave = () => {
-    menuStore.updateSider()
+  const createCluster = async () => {
+    try {
+      commandRequest.value.clusterCommand = stepData.value[0] as 
ClusterCommandReq
+      commandRequest.value.clusterCommand.hosts = stepData.value[2]
+      stepData.value[stepData.value.length - 1] = await 
execCommand(commandRequest.value)
+      return true
+    } catch (error) {
+      console.log('error :>> ', error)
+      return false
+    }
   }
 
   onBeforeRouteLeave((_to, _from, next) => {
@@ -243,7 +262,7 @@
               {{ $t('common.next') }}
             </a-button>
           </template>
-          <a-button v-show="current === stepsLimit && isDone" type="primary" 
@click="onSave"
+          <a-button v-show="current === stepsLimit && isDone" type="primary" 
@click="() => menuStore.updateSider()"
             >{{ $t('common.done') }}
           </a-button>
         </a-space>
diff --git 
a/bigtop-manager-ui/src/components/create-host/components/conflict-resolver.vue 
b/bigtop-manager-ui/src/components/create-host/components/conflict-resolver.vue
new file mode 100644
index 00000000..7889b570
--- /dev/null
+++ 
b/bigtop-manager-ui/src/components/create-host/components/conflict-resolver.vue
@@ -0,0 +1,77 @@
+<!--
+  ~ 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 } from 'vue'
+  import { useI18n } from 'vue-i18n'
+
+  import type { HostReq } from '@/api/command/types'
+  import type { TableColumnType } from 'ant-design-vue'
+
+  type conflictItem = HostReq & { strategy: 'override' | 'keep' }
+
+  const props = defineProps<{ parsedHosts: conflictItem[]; duplicateHostnames: 
string[] }>()
+  const emits = defineEmits<{ (event: 'update:parsedHosts', val: 
conflictItem[]): void }>()
+
+  const options = ['keep', 'override']
+
+  const { t } = useI18n()
+  const columns = computed((): TableColumnType[] => [
+    {
+      title: t('host.hostname'),
+      dataIndex: 'hostname',
+      key: 'hostname',
+      ellipsis: true
+    },
+    {
+      title: t('common.operation'),
+      key: 'operation',
+      width: '200px',
+      fixed: 'right'
+    }
+  ])
+
+  const handleStrategyChange = (hostname: string, strategy: 'keep' | 
'override') => {
+    const updated = props.parsedHosts.map((item) => (item.hostname === 
hostname ? { ...item, strategy } : item))
+    emits('update:parsedHosts', updated)
+  }
+</script>
+
+<template>
+  <div>
+    <a-table
+      :data-source="$props.parsedHosts.filter((v) => 
$props.duplicateHostnames.includes(v.hostname))"
+      :columns="columns"
+    >
+      <template #bodyCell="{ record, column }">
+        <template v-if="column.key === 'operation'">
+          <a-radio-group
+            :value="record.strategy"
+            style="display: flex"
+            @change="(e) => handleStrategyChange(record.hostname, 
e.target.value)"
+          >
+            <a-radio v-for="opt in options" :key="opt" :value="opt">{{ opt 
}}</a-radio>
+          </a-radio-group>
+        </template>
+      </template>
+    </a-table>
+  </div>
+</template>
+
+<style scoped></style>
diff --git 
a/bigtop-manager-ui/src/components/create-host/components/parsed-preview.vue 
b/bigtop-manager-ui/src/components/create-host/components/parsed-preview.vue
new file mode 100644
index 00000000..e910b02b
--- /dev/null
+++ b/bigtop-manager-ui/src/components/create-host/components/parsed-preview.vue
@@ -0,0 +1,168 @@
+<!--
+  ~ 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, shallowRef } from 'vue'
+  import { parseHostNamesAsPatternExpression } from '@/utils/array'
+  import ConflictResolver from './conflict-resolver.vue'
+
+  import type { HostReq } from '@/api/command/types'
+
+  type Data = HostReq & { hostname?: string }
+  type conflictItem = HostReq & { strategy: 'override' | 'keep' }
+
+  interface ParsedRes {
+    parsedData: Data
+    confirmStatus: boolean
+    duplicateHosts?: conflictItem[]
+  }
+
+  const props = defineProps<{ data: Data; isPublic: boolean | undefined }>()
+  const emits = defineEmits<{ (event: 'parsed', value: ParsedRes): void }>()
+
+  const open = ref(false)
+  const showConflictList = ref(false)
+  const parsedHostNames = ref<string[]>([])
+  const duplicateHostnames = ref<string[]>([])
+  const parsedHosts = ref<conflictItem[]>([])
+  const cacheCurrentHosts = shallowRef<Data[]>([])
+
+  const parsed = (currentHosts: Data[]) => {
+    resetState()
+    open.value = true
+    cacheCurrentHosts.value = currentHosts
+    parsedHostNames.value = 
parseHostNamesAsPatternExpression(props.data.hostname || '')
+    parsedHosts.value = parsedHostNames.value.map((v) => ({ hostname: v, 
strategy: 'override' }))
+  }
+
+  const getDuplicateHostnames = () => {
+    const currentHostNamse = cacheCurrentHosts.value.map((v) => v.hostname)
+    return parsedHostNames.value.filter((item) => 
currentHostNamse.includes(item))
+  }
+
+  const handleOk = () => {
+    const duplicates = getDuplicateHostnames()
+    duplicateHostnames.value = duplicates
+
+    const hasConflicts = duplicates.length > 0
+
+    if (!hasConflicts || showConflictList.value) {
+      emits('parsed', {
+        parsedData: { ...props.data, hostnames: parsedHostNames.value },
+        confirmStatus: true,
+        duplicateHosts: hasConflicts ? parsedHosts.value : []
+      })
+      resetState()
+    }
+
+    if (hasConflicts) {
+      showConflictList.value = true
+    }
+  }
+
+  const handleCancel = () => {
+    emits('parsed', { parsedData: { ...props.data }, confirmStatus: false, 
duplicateHosts: [] })
+    resetState()
+  }
+
+  const resetState = () => {
+    open.value = false
+    showConflictList.value = false
+    parsedHostNames.value = []
+    duplicateHostnames.value = []
+  }
+
+  defineExpose({
+    parsed
+  })
+</script>
+
+<template>
+  <a-modal v-model:open="open" :centered="true" width="680px" :z-index="2000" 
@ok="handleOk" @cancel="handleCancel">
+    <template #title>
+      <div class="icon">
+        <span class="icon-wrp">
+          <svg-icon :name="showConflictList ? 'warn' : 'success'"></svg-icon>
+        </span>
+        <span class="title">
+          {{
+            showConflictList
+              ? $t('cluster.duplicate_hostname', 
[`${duplicateHostnames.length}`])
+              : $t('cluster.show_hosts_resolved')
+          }}
+        </span>
+      </div>
+    </template>
+    <div v-if="!showConflictList" class="content">
+      <div v-for="(name, index) in parsedHostNames" :key="index" 
class="item">{{ name }}</div>
+    </div>
+    <div v-else>
+      <conflict-resolver v-model:parsedHosts="parsedHosts" 
:duplicate-hostnames="duplicateHostnames" />
+    </div>
+    <template #footer>
+      <footer>
+        <a-space size="middle">
+          <a-button @click="handleCancel">
+            {{ $t('common.cancel') }}
+          </a-button>
+          <a-button v-if="showConflictList" @click="() => (showConflictList = 
false)">
+            {{ $t('common.prev') }}
+          </a-button>
+          <a-button type="primary" @click="handleOk">
+            {{ $t('common.confirm') }}
+          </a-button>
+        </a-space>
+      </footer>
+    </template>
+  </a-modal>
+</template>
+
+<style lang="scss" scoped>
+  .content {
+    margin-inline-start: 2.125rem;
+    max-height: 31.25rem;
+    overflow: auto;
+  }
+
+  .title {
+    font-size: 1rem;
+    font-weight: 600;
+  }
+
+  .icon {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .icon-wrp {
+    font-size: 1.375rem;
+    font-weight: 600;
+    color: green;
+  }
+
+  .item {
+    margin: 0.625rem;
+  }
+
+  footer {
+    width: 100%;
+    @include flexbox($justify: flex-end);
+  }
+</style>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue 
b/bigtop-manager-ui/src/components/create-host/create.vue
similarity index 74%
rename from bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
rename to bigtop-manager-ui/src/components/create-host/create.vue
index c202eb53..0036eec5 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
+++ b/bigtop-manager-ui/src/components/create-host/create.vue
@@ -18,21 +18,25 @@
 -->
 
 <script setup lang="ts">
-  import { computed, h, nextTick, ref, watch } from 'vue'
+  import { computed, nextTick, ref, watch } from 'vue'
+  import { Rule } from 'ant-design-vue/es/form'
+  import { message } from 'ant-design-vue'
+  import { storeToRefs } from 'pinia'
   import { useI18n } from 'vue-i18n'
-  import { FormItemState } from '@/components/common/auto-form/types'
-  import { CheckCircleOutlined, UploadOutlined } from '@ant-design/icons-vue'
-  import { parseHostNamesAsPatternExpression } from '@/utils/array'
-  import { message, Modal } from 'ant-design-vue'
+  import { UploadOutlined } from '@ant-design/icons-vue'
+
   import { useLocaleStore } from '@/store/locale'
-  import { storeToRefs } from 'pinia'
+  import { useClusterStore } from '@/store/cluster'
+
   import { uploadFile } from '@/api/upload-file'
-  import { Rule } from 'ant-design-vue/es/form'
   import { updateHost } from '@/api/hosts'
-  import { useClusterStore } from '@/store/cluster'
+
+  import ParsedPreview from './components/parsed-preview.vue'
+
   import type { UploadProps } from 'ant-design-vue'
   import type { HostReq } from '@/api/command/types'
   import type { HostParams, HostVO } from '@/api/hosts/types'
+  import type { FormItemState } from '@/components/common/auto-form/types'
 
   enum Mode {
     EDIT = 'cluster.edit_host',
@@ -40,18 +44,28 @@
   }
 
   interface Emits {
-    (event: 'onOk', type: keyof typeof Mode, value: HostReq | HostVO): void
+    (
+      event: 'onOk',
+      type: keyof typeof Mode,
+      value: HostReq | HostVO,
+      duplicateHosts?: HostReq & { strategy: 'override' | 'keep' }[]
+    ): void
   }
 
-  const props = withDefaults(defineProps<{ isPublic?: boolean; apiEditCaller?: 
boolean }>(), {
-    isPublic: false,
-    apiEditCaller: false
-  })
+  interface Props {
+    isPublic?: boolean
+    apiEditCaller?: boolean
+    currentHosts: HostReq[]
+  }
 
-  const { t } = useI18n()
+  const props = withDefaults(defineProps<Props>(), { isPublic: false, 
apiEditCaller: false })
   const emits = defineEmits<Emits>()
+
+  const { t } = useI18n()
   const localeStore = useLocaleStore()
   const clusterStore = useClusterStore()
+  const { locale } = storeToRefs(localeStore)
+
   const open = ref(false)
   const loading = ref(false)
   const mode = ref<keyof typeof Mode>('ADD')
@@ -59,9 +73,13 @@
   const autoFormRef = ref<Comp.AutoFormInstance | null>(null)
   const formValue = ref<HostReq & { hostname?: string }>({})
   const fileName = ref('')
-  const { locale } = storeToRefs(localeStore)
+  const previewRef = ref<InstanceType<typeof ParsedPreview> | null>()
+
   const isEdit = computed(() => mode.value === 'EDIT')
 
+  /**
+   * Validates SSH password confirmation.
+   */
   const checkSshPassword = async (_rule: Rule, value: string) => {
     if (!value) {
       return Promise.reject(t('common.enter_error', 
[`${t('host.confirm_password')}`.toLowerCase()]))
@@ -73,6 +91,9 @@
     }
   }
 
+  /**
+   * Validates SSH key password confirmation.
+   */
   const checkSshKeyPassword = async (_rule: Rule, value: string) => {
     if (value != formValue.value?.sshKeyPassword) {
       return Promise.reject(t('host.key_password_not_match'))
@@ -331,185 +352,81 @@
   ])
 
   const filterFormItems = computed((): FormItemState[] => {
+    const baseItems = [...formItems.value]
+    const isPublic = props.isPublic
+
     if (formValue.value.authType === '1') {
-      const data = [...formItems.value]
-      data.splice(2, 0, ...formItemsForSshPassword.value)
-      return props.isPublic ? [...formItemsOfPublicHost.value, ...data] : data
+      baseItems.splice(2, 0, ...formItemsForSshPassword.value)
     } else if (formValue.value.authType === '2') {
-      const data = [...formItems.value]
-      data.splice(2, 0, ...formItemsForSshKeyPassword.value)
-      return props.isPublic ? [...formItemsOfPublicHost.value, ...data] : data
+      baseItems.splice(2, 0, ...formItemsForSshKeyPassword.value)
     }
-    return props.isPublic ? [...formItemsOfPublicHost.value, 
...formItems.value] : [...formItems.value]
+
+    return isPublic ? [...formItemsOfPublicHost.value, ...baseItems] : 
baseItems
   })
 
   watch(
-    () => formValue.value,
-    (val) => {
-      if (val.inputType && val.inputType === '1') {
-        hiddenItems.value = ['sshKeyString']
-      } else if (val.inputType && val.inputType === '2') {
-        hiddenItems.value = ['sshKeyFilename']
-      } else {
-        hiddenItems.value = []
+    () => formValue.value.inputType,
+    (inputType) => {
+      const hiddenMap: Record<string, string[]> = {
+        '1': ['sshKeyString'],
+        '2': ['sshKeyFilename']
       }
-    },
-    {
-      deep: true
+      hiddenItems.value = hiddenMap[inputType] ?? []
     }
   )
 
-  const handleOpen = async (type: keyof typeof Mode, payload?: HostParams) => {
-    open.value = true
-    mode.value = type
-    if (payload) {
-      formValue.value = Object.assign(formValue.value, {
-        ...payload,
-        authType: `${payload?.authType ?? 1}`,
-        inputType: `${payload?.inputType ?? 1}`
-      })
-    } else {
-      Object.assign(formValue.value, {
-        authType: '1',
-        inputType: '1'
-      })
-    }
-
-    props.isPublic && (await getClusterSelectOptions())
-  }
-
-  const getClusterSelectOptions = async () => {
-    await nextTick()
-    const formatClusters = Object.values(clusterStore.clusterMap).map((v) => 
({ ...v, clusterId: v.id }))
-    autoFormRef.value?.setOptions('clusterId', formatClusters)
-  }
-
+  /**
+   * Handles host editing.
+   */
   const editHost = async (hostConfig: HostReq) => {
     try {
       const data = await updateHost(hostConfig)
       if (data) {
-        loading.value = false
         message.success(t('common.update_success'))
         emits('onOk', mode.value, formValue.value)
         handleCancel()
       }
     } catch (error) {
-      console.log('error :>> ', error)
+      console.error('Error editing host:', error)
     } finally {
       loading.value = false
     }
   }
 
+  /**
+   * Handles parsed data from preview.
+   */
+  const handleParsed = ({ parsedData, confirmStatus, duplicateHosts }: any) => 
{
+    if (confirmStatus) {
+      Object.assign(formValue.value, parsedData)
+      emits('onOk', mode.value, formValue.value, duplicateHosts)
+      handleCancel()
+    }
+  }
+
   const handleOk = async () => {
     const validate = await autoFormRef.value?.getFormValidation()
     if (!validate) return
-    try {
-      if (!isEdit.value) {
-        const confirmStatus = await showHosts()
-        if (confirmStatus) {
-          emits('onOk', mode.value, formValue.value)
-          handleCancel()
-        }
-      } else {
-        if (props.apiEditCaller) {
-          loading.value = true
-          await editHost(formValue.value)
-        } else {
-          emits('onOk', mode.value, formValue.value)
-          handleCancel()
-        }
-      }
-    } catch (e) {
-      console.log(e)
-    }
-  }
 
-  const createHostNameEls = (list: string[]) => {
-    const elStyles = {
-      contentStyle: {
-        marginInlineStart: '2.125rem',
-        width: '100%',
-        maxHeight: '31.25rem',
-        overflow: 'auto'
-      },
-      itemStyle: { margin: '0.625rem' },
-      iconWrpStyle: {
-        fontSize: '1.375rem',
-        fontWeight: 600,
-        color: 'green',
-        marginInlineEnd: '0.75rem'
-      },
-      titleStyle: {
-        fontSize: '1rem',
-        fontWeight: 600
-      },
-      redfIconStyle: {
-        display: 'flex',
-        alignItems: 'center'
-      }
+    if (!isEdit.value) {
+      previewRef.value?.parsed(props.currentHosts)
+    } else if (props.apiEditCaller) {
+      loading.value = true
+      await editHost(formValue.value)
+    } else {
+      emits('onOk', mode.value, formValue.value)
+      handleCancel()
     }
-    const items = list.map((item: string, idx: number) => {
-      return h('li', { key: idx, style: elStyles.itemStyle }, item)
-    })
-    const iconWrpStyle = h(
-      'span',
-      {
-        style: elStyles.iconWrpStyle
-      },
-      [h(CheckCircleOutlined)]
-    )
-    const replaceTitle = h('span', { style: elStyles.titleStyle }, 
`${t('cluster.show_hosts_resolved')}`)
-    const reDefineIcon = h('div', { style: elStyles.redfIconStyle }, 
[iconWrpStyle, replaceTitle])
-    const box = h(
-      'div',
-      {
-        style: elStyles.contentStyle
-      },
-      items
-    )
-
-    return { box, reDefineIcon }
-  }
-
-  const showHosts = (): Promise<boolean> => {
-    const hostList = 
parseHostNamesAsPatternExpression(formValue.value.hostname as string)
-    const { box: content, reDefineIcon: icon } = createHostNameEls(hostList)
-    return new Promise((resolve) => {
-      Modal.confirm({
-        title: '',
-        icon,
-        centered: true,
-        width: '31.25rem',
-        content,
-        onOk() {
-          formValue.value.hostnames = hostList
-          resolve(true)
-        },
-        onCancel() {
-          resolve(false)
-          Modal.destroyAll()
-        }
-      })
-    })
   }
 
   const handleCancel = () => {
-    formValue.value = {
-      authType: '1',
-      inputType: '1'
-    }
+    formValue.value = { authType: '1', inputType: '1' }
     fileName.value = ''
     open.value = false
   }
 
   const beforeUpload: UploadProps['beforeUpload'] = (file) => {
-    // const isText = file.type === 'text/plain'
-    const checkLimitSize = file.size / 1024 <= 10
-    // if (!isText) {
-    //   message.error(t('common.file_type_error'))
-    //   return false
-    // }
-    if (!checkLimitSize) {
+    if (file.size / 1024 > 10) {
       message.error(t('common.file_size_error'))
       return false
     }
@@ -533,6 +450,25 @@
     }
   }
 
+  const handleOpen = async (type: keyof typeof Mode, payload?: HostParams) => {
+    open.value = true
+    mode.value = type
+
+    formValue.value = payload
+      ? { ...payload, authType: `${payload?.authType ?? 1}`, inputType: 
`${payload?.inputType ?? 1}` }
+      : { authType: '1', inputType: '1' }
+
+    if (props.isPublic) {
+      await getClusterSelectOptions()
+    }
+  }
+
+  const getClusterSelectOptions = async () => {
+    await nextTick()
+    const formatClusters = Object.values(clusterStore.clusterMap).map((v) => 
({ ...v, clusterId: v.id }))
+    autoFormRef.value?.setOptions('clusterId', formatClusters)
+  }
+
   defineExpose({
     handleOpen
   })
@@ -591,13 +527,11 @@
         </footer>
       </template>
     </a-modal>
+    <parsed-preview ref="previewRef" :is-public="$props.isPublic" 
:data="formValue" @parsed="handleParsed" />
   </div>
 </template>
 
 <style lang="scss" scoped>
-  .question {
-    cursor: pointer;
-  }
   .filename {
     color: $color-primary;
     padding-inline: $space-sm;
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue 
b/bigtop-manager-ui/src/components/create-host/install-dependencies.vue
similarity index 98%
rename from 
bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
rename to bigtop-manager-ui/src/components/create-host/install-dependencies.vue
index adc60a9f..820d3c2b 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
+++ b/bigtop-manager-ui/src/components/create-host/install-dependencies.vue
@@ -26,8 +26,10 @@
   import { useClusterStore } from '@/store/cluster'
   import { generateRandomId } from '@/utils/tools'
   import { execCommand } from '@/api/command'
-  import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
+
+  import HostCreate from './create.vue'
   import useBaseTable from '@/composables/use-base-table'
+
   import { type InstalledStatusVO, Status } from '@/api/hosts/types'
   import type { FilterConfirmProps, FilterResetProps } from 
'ant-design-vue/es/table/interface'
   import type { GroupItem } from '@/components/common/button-group/types'
@@ -49,10 +51,12 @@
   const emits = defineEmits<Emits>()
   const clusterStore = useClusterStore()
   const { clusterMap } = storeToRefs(clusterStore)
+
   const installing = ref(false)
   const open = ref(false)
   const searchInputRef = ref()
   const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
+
   const installStatus = shallowRef<InstalledStatusVO[]>([])
   const state = reactive<TableState>({
     searchText: '',
@@ -336,7 +340,7 @@
         </template>
       </a-table>
     </a-modal>
-    <host-create ref="hostCreateRef" :is-public="true" @on-ok="handleOk" />
+    <host-create ref="hostCreateRef" :current-hosts="dataSource" 
:is-public="true" @on-ok="handleOk" />
   </div>
 </template>
 
diff --git a/bigtop-manager-ui/src/layouts/sider.vue 
b/bigtop-manager-ui/src/layouts/sider.vue
index 1a70130d..adbe5d77 100644
--- a/bigtop-manager-ui/src/layouts/sider.vue
+++ b/bigtop-manager-ui/src/layouts/sider.vue
@@ -31,6 +31,7 @@
   const clusterStore = useClusterStore()
   const routeParamsLen = ref(0)
   const openKeys = ref<string[]>(['clusters'])
+
   const { siderMenuSelectedKey, headerSelectedKey, siderMenus, 
routePathFromClusters } = storeToRefs(menuStore)
   const { clusterCount, clusterMap } = storeToRefs(clusterStore)
 
@@ -49,29 +50,27 @@
    */
   const handleRouteChange = async (newRoute: typeof route) => {
     const token = localStorage.getItem('Token') ?? 
sessionStorage.getItem('Token') ?? undefined
+    const { params, path, meta, name: RouteName } = newRoute
 
     if (!token) {
       return
     }
 
-    const { params, path, meta, name } = newRoute
-    const clusterId = params.id
-    const isClusterPath = path.includes(routePathFromClusters.value)
-    const isNotCreatingCluster = name !== 'CreateCluster'
-
     routeParamsLen.value = Object.keys(params).length
 
-    if (isClusterPath && clusterCount.value > 0 && isNotCreatingCluster) {
-      if (clusterId && clusterMap.value[`${clusterId}`]) {
-        siderMenuSelectedKey.value = 
`${routePathFromClusters.value}/${clusterId}`
+    if (path.includes(routePathFromClusters.value) && RouteName !== 
'CreateCluster') {
+      if (params.id && clusterMap.value[`${params.id}`]) {
+        siderMenuSelectedKey.value = 
`${routePathFromClusters.value}/${params.id}`
         openKeys.value.push(siderMenuSelectedKey.value)
-      } else {
-        siderMenuSelectedKey.value = ''
-        menuStore.setupSider()
+        return
       }
-    } else {
-      siderMenuSelectedKey.value = meta.activeMenu ?? path
+
+      siderMenuSelectedKey.value = ''
+      menuStore.setupSider()
+      return
     }
+
+    siderMenuSelectedKey.value = meta.activeMenu ?? path
   }
 
   /**
@@ -89,8 +88,8 @@
     router.push({ name: 'CreateCluster' })
   }
 
-  onMounted(async () => {
-    await clusterStore.loadClusters()
+  onMounted(() => {
+    clusterStore.loadClusters()
   })
 
   watch(
diff --git a/bigtop-manager-ui/src/locales/en_US/cluster.ts 
b/bigtop-manager-ui/src/locales/en_US/cluster.ts
index 11979d82..d3af59c6 100644
--- a/bigtop-manager-ui/src/locales/en_US/cluster.ts
+++ b/bigtop-manager-ui/src/locales/en_US/cluster.ts
@@ -36,5 +36,9 @@ export default {
   view_log: 'View Log',
   source: 'Repository',
   show_hosts_resolved: 'Resolved Hostnames',
-  cluster_unavailable_message: 'Sorry, you need to create cluster to use this 
feature.'
+  duplicate_hostname: '{0} host name(s) are duplicated. Please choose how to 
handle them.',
+  exist_duplicate_hostname: 'Duplicate host name found. Choose an action.',
+  cluster_unavailable_message: 'Sorry, you need to create cluster to use this 
feature.',
+  keep: 'keep',
+  override: 'override'
 }
diff --git a/bigtop-manager-ui/src/locales/zh_CN/cluster.ts 
b/bigtop-manager-ui/src/locales/zh_CN/cluster.ts
index 29a9cd70..a37580ff 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/cluster.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/cluster.ts
@@ -35,5 +35,9 @@ export default {
   view_log: '查看日志',
   source: '配置源',
   show_hosts_resolved: '解析后的主机名列表',
-  cluster_unavailable_message: '抱歉,你还没有集群,无法使用集群管理能力。'
+  duplicate_hostname: '有 {0} 个主机名重复,请选择处理方式',
+  exist_duplicate_hostname: '主机名重复,请选择处理方式',
+  cluster_unavailable_message: '抱歉,你还没有集群,无法使用集群管理能力。',
+  keep: '保留',
+  override: '覆盖'
 }
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 1a2253af..bafe924b 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -26,8 +26,8 @@
   import { useRouter } from 'vue-router'
 
   import useBaseTable from '@/composables/use-base-table'
-  import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
-  import InstallDependencies from 
'@/pages/cluster-manage/hosts/install-dependencies.vue'
+  import HostCreate from '@/components/create-host/create.vue'
+  import InstallDependencies from 
'@/components/create-host/install-dependencies.vue'
 
   import type { FilterConfirmProps, FilterResetProps } from 
'ant-design-vue/es/table/interface'
   import type { GroupItem } from '@/components/common/button-group/types'
@@ -279,7 +279,12 @@
         </template>
       </template>
     </a-table>
-    <host-create ref="hostCreateRef" :api-edit-caller="true" 
@on-ok="afterSetupHostConfig" />
+    <host-create
+      ref="hostCreateRef"
+      :current-hosts="dataSource"
+      :api-edit-caller="true"
+      @on-ok="afterSetupHostConfig"
+    />
     <install-dependencies ref="installRef" 
@on-install-success="getHostList(true)" />
   </div>
 </template>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
index f0cee396..1d651221 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
@@ -22,11 +22,14 @@
   import { computed, onMounted, onUnmounted, reactive, ref, shallowRef } from 
'vue'
   import { useRouter } from 'vue-router'
   import { useI18n } from 'vue-i18n'
+
   import { useClusterStore } from '@/store/cluster'
   import * as hostApi from '@/api/hosts'
+
   import useBaseTable from '@/composables/use-base-table'
-  import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
-  import InstallDependencies from 
'@/pages/cluster-manage/hosts/install-dependencies.vue'
+  import HostCreate from '@/components/create-host/create.vue'
+  import InstallDependencies from 
'@/components/create-host/install-dependencies.vue'
+
   import type { FilterConfirmProps, FilterResetProps, TableRowSelection } from 
'ant-design-vue/es/table/interface'
   import type { HostReq } from '@/api/command/types'
   import type { GroupItem } from '@/components/common/button-group/types'
@@ -346,7 +349,13 @@
         </template>
       </template>
     </a-table>
-    <host-create ref="hostCreateRef" :api-edit-caller="true" :is-public="true" 
@on-ok="afterSetupHostConfig" />
+    <host-create
+      ref="hostCreateRef"
+      :current-hosts="dataSource"
+      :api-edit-caller="true"
+      :is-public="true"
+      @on-ok="afterSetupHostConfig"
+    />
     <install-dependencies ref="installRef" 
@on-install-success="getHostList(true)" />
   </div>
 </template>
diff --git a/bigtop-manager-ui/src/router/guard.ts 
b/bigtop-manager-ui/src/router/guard.ts
index 61824c91..7fce803c 100644
--- a/bigtop-manager-ui/src/router/guard.ts
+++ b/bigtop-manager-ui/src/router/guard.ts
@@ -18,20 +18,20 @@
  */
 
 import type { Router } from 'vue-router'
+
 function setCommonGuard(router: Router) {
   const token = localStorage.getItem('Token') ?? 
sessionStorage.getItem('Token') ?? undefined
 
   router.beforeEach(async (to, _from, next) => {
     if (!token) {
-      next()
-      return
+      return next()
     }
 
     if (to.name === 'Clusters' && token) {
-      next({ name: 'Default' })
-    } else {
-      next()
+      return next({ name: 'Default' })
     }
+
+    return next()
   })
 }
 
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts 
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index 0651679e..beecba9d 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -178,7 +178,7 @@ const routes: RouteRecordRaw[] = [
           {
             name: 'HostCreation',
             path: 'add',
-            component: () => import('@/pages/cluster-manage/hosts/create.vue'),
+            component: () => import('@/components/create-host/create.vue'),
             meta: {
               hidden: true,
               activeMenu: '/cluster-manage/hosts/list'
diff --git a/bigtop-manager-ui/src/store/cluster/index.ts 
b/bigtop-manager-ui/src/store/cluster/index.ts
index 6301711e..461047e7 100644
--- a/bigtop-manager-ui/src/store/cluster/index.ts
+++ b/bigtop-manager-ui/src/store/cluster/index.ts
@@ -21,14 +21,12 @@ import { computed, ref } from 'vue'
 import { defineStore } from 'pinia'
 import { getCluster, getClusterList } from '@/api/cluster'
 import { useServiceStore } from '@/store/service'
-import { useMenuStore } from '@/store/menu'
 import type { ClusterVO } from '@/api/cluster/types.ts'
 
 export const useClusterStore = defineStore(
   'cluster',
   () => {
     const serviceStore = useServiceStore()
-    const menuStore = useMenuStore()
     const loading = ref(false)
     const clusters = ref<ClusterVO[]>([])
     const currCluster = ref<ClusterVO>({})
@@ -46,9 +44,7 @@ export const useClusterStore = defineStore(
           {} as Record<string, ClusterVO>
         )
       } catch (error) {
-        const token = localStorage.getItem('Token') ?? 
sessionStorage.getItem('Token') ?? undefined
         clusterMap.value = {}
-        token && menuStore.setupMenu()
         console.error('Failed to get clusters:', error)
       }
     }

Reply via email to