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 bab75900 BIGTOP-4452: Fix broken layout on login page (#234)
bab75900 is described below

commit bab759009f183096fbed25fd7dfd96207d01b34a
Author: Fdefined <[email protected]>
AuthorDate: Thu Jun 26 22:22:52 2025 +0800

    BIGTOP-4452: Fix broken layout on login page (#234)
---
 .../src/assets/images/svg/carbon-language.svg      |  23 ++++
 .../charts}/category-chart.vue                     |  99 +++++++-------
 .../src/components/charts/gauge-chart.vue          | 123 +++++++++++++++++
 .../src/components/common/svg-icon/index.vue       |   3 +-
 .../create-cluster}/components/check-workflow.vue  |   0
 .../create-cluster}/components/cluster-base.vue    |   0
 .../create-cluster}/components/component-info.vue  |   0
 .../create-cluster/components/host-manage.vue}     |   0
 .../create-cluster}/components/set-source.vue      |   0
 .../create-cluster}/create.vue                     |   6 +-
 .../src/components/create-service/create.vue       |   4 +-
 .../src/components/select-lang/login-lang.vue      |  70 ++++++++++
 .../src/components/service-management/overview.vue |   4 +-
 bigtop-manager-ui/src/composables/use-chart.ts     |  85 ++++++++++++
 bigtop-manager-ui/src/layouts/index.vue            |  21 ---
 bigtop-manager-ui/src/layouts/sider.vue            |  63 ++++++---
 .../cluster/components/gauge-chart.vue             | 148 ---------------------
 .../src/pages/cluster-manage/cluster/host.vue      |  31 ++---
 .../src/pages/cluster-manage/cluster/index.vue     |  91 ++++++-------
 .../src/pages/cluster-manage/cluster/overview.vue  | 115 +++++++---------
 .../src/pages/cluster-manage/cluster/service.vue   |  28 ++--
 .../src/pages/cluster-manage/cluster/user.vue      |   4 +
 .../src/pages/cluster-manage/components/index.vue  |   2 +-
 .../src/pages/cluster-manage/hosts/overview.vue    |   4 +-
 bigtop-manager-ui/src/pages/login/index.vue        |  13 +-
 bigtop-manager-ui/src/router/guard.ts              |  17 ++-
 .../src/router/routes/modules/clusters.ts          |   2 +-
 bigtop-manager-ui/src/store/cluster/index.ts       |  19 +--
 bigtop-manager-ui/src/store/menu/index.ts          |  89 ++++++++++---
 bigtop-manager-ui/src/store/stack/index.ts         |  27 +++-
 bigtop-manager-ui/src/store/user/index.ts          |   5 +-
 bigtop-manager-ui/src/styles/index.scss            |  29 ++++
 32 files changed, 669 insertions(+), 456 deletions(-)

diff --git a/bigtop-manager-ui/src/assets/images/svg/carbon-language.svg 
b/bigtop-manager-ui/src/assets/images/svg/carbon-language.svg
new file mode 100644
index 00000000..33988058
--- /dev/null
+++ b/bigtop-manager-ui/src/assets/images/svg/carbon-language.svg
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   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"; width="1em" height="1em" viewBox="0 0 
32 32">
+  <path fill="currentColor"
+    d="M27.85 29H30l-6-15h-2.35l-6 15h2.15l1.6-4h6.85zm-7.65-6l2.62-6.56L25.45 
23zM18 7V5h-7V2H9v3H2v2h10.74a14.71 14.71 0 0 1-3.19 6.18A13.5 13.5 0 0 1 7.26 
9h-2.1a16.47 16.47 0 0 0 3 5.58A16.84 16.84 0 0 1 3 18l.75 1.86A18.47 18.47 0 0 
0 9.53 16a16.92 16.92 0 0 0 5.76 3.84L16 18a14.48 14.48 0 0 1-5.12-3.37A17.64 
17.64 0 0 0 14.8 7z" />
+</svg>
\ No newline at end of file
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/category-chart.vue
 b/bigtop-manager-ui/src/components/charts/category-chart.vue
similarity index 54%
rename from 
bigtop-manager-ui/src/pages/cluster-manage/cluster/components/category-chart.vue
rename to bigtop-manager-ui/src/components/charts/category-chart.vue
index 6c07487f..c7efe490 100644
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/category-chart.vue
+++ b/bigtop-manager-ui/src/components/charts/category-chart.vue
@@ -18,42 +18,22 @@
 -->
 
 <script setup lang="ts">
-  import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
-  import * as echarts from 'echarts/core'
-  import { GridComponent, GridComponentOption, TooltipComponent, 
TooltipComponentOption } from 'echarts/components'
-  import { LineChart, LineSeriesOption } from 'echarts/charts'
-  import { UniversalTransition } from 'echarts/features'
-  import { CanvasRenderer } from 'echarts/renderers'
-
-  echarts.use([GridComponent, LineChart, TooltipComponent, CanvasRenderer, 
UniversalTransition])
-
-  type EChartsOption = echarts.ComposeOption<GridComponentOption | 
TooltipComponentOption | LineSeriesOption>
+  import dayjs from 'dayjs'
+  import { computed, onMounted, toRefs, watchEffect } from 'vue'
+  import { type EChartsOption, useChart } from '@/composables/use-chart'
 
   const props = defineProps<{
     chartId: string
     title: string
+    data?: any[]
+    timeDistance?: string
   }>()
 
-  const myChart = shallowRef<echarts.ECharts | null>(null)
-
-  const generateTimeLabels = () => {
-    const times = [] as Array<string>
-    for (let h = 0; h < 24; h++) {
-      for (let m = 0; m < 60; m += 15) {
-        const hour = h.toString().padStart(2, '0')
-        const minute = m.toString().padStart(2, '0')
-        times.push(`${hour}:${minute}`)
-      }
-    }
-    return times
-  }
+  const { data, chartId, title, timeDistance } = toRefs(props)
+  const { initChart, setOptions } = useChart()
 
-  const initCharts = () => {
-    const chartDom = document.getElementById(`${props.chartId}`)
-    myChart.value = echarts.init(chartDom, null, {
-      devicePixelRatio: window.devicePixelRatio
-    })
-    const option: EChartsOption = {
+  const option = computed(
+    (): EChartsOption => ({
       grid: {
         top: '20px',
         left: '40px',
@@ -64,10 +44,6 @@
         trigger: 'axis',
         backgroundColor: '#000',
         borderColor: 'rgba(236,236,236,0.1)',
-        // formatter: (params) => {
-        //   console.log('params :>> ', params)
-        //   return ''
-        // },
         textStyle: {
           color: '#fff'
         },
@@ -81,13 +57,12 @@
       xAxis: [
         {
           type: 'category',
-          data: generateTimeLabels(),
+          data: [],
           axisPointer: {
             type: 'line'
           },
           axisLabel: {
-            interval: 14,
-            fontSize: 8
+            fontSize: 10
           }
         }
       ],
@@ -111,37 +86,67 @@
       ],
       series: [
         {
-          name: props.title,
+          name: title.value,
           type: 'line',
-          data: Array.from({ length: generateTimeLabels().length }, () => 
Math.floor(Math.random() * 0)),
+          data: [],
           lineStyle: {
             color: '#49A4FF',
             width: 2
           }
         }
       ]
+    })
+  )
+
+  const intervalToMs = (interval: string): number => {
+    const unit = interval.replace(/\d+/g, '')
+    const value = parseInt(interval)
+
+    switch (unit) {
+      case 'm':
+        return value * 60 * 1000
+      case 'h':
+        return value * 60 * 60 * 1000
+      default:
+        throw new Error('Unsupported interval: ' + interval)
     }
-    option && myChart.value.setOption(option)
   }
 
-  const resizeChart = () => {
-    myChart.value?.resize()
+  const getTimePoints = (interval: string = '15m'): string[] => {
+    const now = dayjs()
+    const gap = intervalToMs(interval)
+    const result: string[] = []
+
+    for (let i = 5; i >= 0; i--) {
+      const time = now.subtract(i * gap, 'millisecond')
+      result.push(time.format('HH:mm'))
+    }
+
+    return result
   }
 
   onMounted(() => {
-    initCharts()
-    window.addEventListener('resize', resizeChart, true)
+    const selector = document.getElementById(`${chartId.value}`)
+    selector && initChart(selector!, option.value)
   })
 
-  onBeforeUnmount(() => {
-    window.removeEventListener('resize', resizeChart, true)
+  watchEffect(() => {
+    setOptions({
+      xAxis: [{ data: getTimePoints(timeDistance.value) || [] }]
+    })
+  })
+
+  watchEffect(() => {
+    setOptions({
+      series: [{ data: [{ value: data.value ?? [] }] }]
+    })
   })
 </script>
 
 <template>
   <div class="chart">
-    <div class="chart-title">{{ $props.title }}</div>
-    <div :id="$props.chartId" style="height: 260px; width: 100%"></div>
+    <div class="chart-title">{{ title }}</div>
+    <div :id="chartId" style="height: 260px; width: 100%"></div>
   </div>
 </template>
 
diff --git a/bigtop-manager-ui/src/components/charts/gauge-chart.vue 
b/bigtop-manager-ui/src/components/charts/gauge-chart.vue
new file mode 100644
index 00000000..946da83c
--- /dev/null
+++ b/bigtop-manager-ui/src/components/charts/gauge-chart.vue
@@ -0,0 +1,123 @@
+<!--
+  ~ 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 { onMounted, shallowRef, toRefs, watchEffect } from 'vue'
+  import { type EChartsOption, useChart } from '@/composables/use-chart'
+
+  interface GaugeChartProps {
+    chartId: string
+    title: string
+    percent?: number
+  }
+
+  const props = withDefaults(defineProps<GaugeChartProps>(), { percent: 0 })
+  const { percent, chartId, title } = toRefs(props)
+  const { initChart, setOptions } = useChart()
+
+  const option = shallowRef<EChartsOption>({
+    series: [
+      {
+        type: 'gauge',
+        radius: '92%',
+        center: ['50%', '50%'],
+        axisLine: {
+          lineStyle: {
+            width: 14,
+            color: [
+              [0.3, '#67e0e3'],
+              [0.7, '#37a2da'],
+              [1, '#fd666d']
+            ]
+          }
+        },
+        pointer: {
+          width: 3,
+          length: '58%',
+          itemStyle: {
+            color: 'auto'
+          }
+        },
+        axisTick: {
+          distance: -50,
+          length: 40,
+          lineStyle: {
+            color: '#fff',
+            width: 1
+          }
+        },
+        splitLine: {
+          distance: -28,
+          length: 28,
+          lineStyle: {
+            color: '#fff',
+            width: 4
+          }
+        },
+        axisLabel: {
+          color: 'inherit',
+          distance: 22,
+          fontSize: 12
+        },
+        detail: {
+          valueAnimation: true,
+          formatter: '{value}%',
+          color: 'inherit',
+          fontSize: 18
+        },
+        data: [
+          {
+            value: 0 * 100
+          }
+        ]
+      }
+    ]
+  })
+
+  onMounted(() => {
+    const selector = document.getElementById(`${chartId.value}`)
+    selector && initChart(document.getElementById(`${chartId.value}`)!, 
option.value)
+  })
+
+  watchEffect(() => {
+    setOptions({
+      series: [{ data: [{ value: percent.value.toFixed(2) === 'NaN' ? 0 : 
percent.value.toFixed(2) }] }]
+    })
+  })
+</script>
+
+<template>
+  <div class="chart">
+    <div class="chart-title">{{ title }}</div>
+    <div :id="chartId" style="height: 260px; width: 100%"></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+  .chart {
+    height: 300px;
+    box-sizing: border-box;
+    padding: 10px;
+    &-title {
+      text-align: start;
+      font-size: 12px;
+      font-weight: 600;
+    }
+  }
+</style>
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 42d2d0b2..0d3fc9d6 100644
--- a/bigtop-manager-ui/src/components/common/svg-icon/index.vue
+++ b/bigtop-manager-ui/src/components/common/svg-icon/index.vue
@@ -69,7 +69,8 @@
     start: IconSvgStart,
     stop: IconSvgStop,
     success: IconSvgSuccess,
-    unknown: IconSvgUnknown
+    unknown: IconSvgUnknown,
+    'carbon-language': IconSvgCarbonLanguage
   }
 
   const props = defineProps<{ name: string | undefined }>()
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
 b/bigtop-manager-ui/src/components/create-cluster/components/check-workflow.vue
similarity index 100%
rename from 
bigtop-manager-ui/src/pages/cluster-manage/cluster/components/check-workflow.vue
rename to 
bigtop-manager-ui/src/components/create-cluster/components/check-workflow.vue
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/cluster-base.vue
 b/bigtop-manager-ui/src/components/create-cluster/components/cluster-base.vue
similarity index 100%
rename from 
bigtop-manager-ui/src/pages/cluster-manage/cluster/components/cluster-base.vue
rename to 
bigtop-manager-ui/src/components/create-cluster/components/cluster-base.vue
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/component-info.vue
 b/bigtop-manager-ui/src/components/create-cluster/components/component-info.vue
similarity index 100%
rename from 
bigtop-manager-ui/src/pages/cluster-manage/cluster/components/component-info.vue
rename to 
bigtop-manager-ui/src/components/create-cluster/components/component-info.vue
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue 
b/bigtop-manager-ui/src/components/create-cluster/components/host-manage.vue
similarity index 100%
rename from 
bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
rename to 
bigtop-manager-ui/src/components/create-cluster/components/host-manage.vue
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/set-source.vue 
b/bigtop-manager-ui/src/components/create-cluster/components/set-source.vue
similarity index 100%
rename from 
bigtop-manager-ui/src/pages/cluster-manage/cluster/components/set-source.vue
rename to 
bigtop-manager-ui/src/components/create-cluster/components/set-source.vue
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue 
b/bigtop-manager-ui/src/components/create-cluster/create.vue
similarity index 99%
rename from bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
rename to bigtop-manager-ui/src/components/create-cluster/create.vue
index 416323af..b1863257 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/create.vue
+++ b/bigtop-manager-ui/src/components/create-cluster/create.vue
@@ -25,12 +25,11 @@
   import { getInstalledStatus, installDependencies } from '@/api/hosts'
   import { execCommand } from '@/api/command'
   import { onBeforeRouteLeave } from 'vue-router'
-
   import SvgIcon from '@/components/common/svg-icon/index.vue'
   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'
+  import HostManage from './components/host-manage.vue'
   import CheckWorkflow from './components/check-workflow.vue'
 
   import { type InstalledStatusVO, Status } from '@/api/hosts/types'
@@ -47,8 +46,7 @@
   })
 
   const installStatus = shallowRef<InstalledStatusVO[]>([])
-  const components = shallowRef<any[]>([ClusterBase, ComponentInfo, 
HostConfig, CheckWorkflow])
-
+  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)
 
diff --git a/bigtop-manager-ui/src/components/create-service/create.vue 
b/bigtop-manager-ui/src/components/create-service/create.vue
index fe188ef6..596327b0 100644
--- a/bigtop-manager-ui/src/components/create-service/create.vue
+++ b/bigtop-manager-ui/src/components/create-service/create.vue
@@ -23,15 +23,13 @@
   import { useI18n } from 'vue-i18n'
   import { storeToRefs } from 'pinia'
   import { onBeforeRouteLeave, useRoute } from 'vue-router'
-
+  import { StepContext, useCreateServiceStore } from '@/store/create-service'
   import ServiceSelector from './components/service-selector.vue'
   import ComponentAssigner from './components/component-assigner.vue'
   import ServiceConfigurator from './components/service-configurator.vue'
   import ComponentInstaller from './components/component-installer.vue'
   import SvgIcon from '@/components/common/svg-icon/index.vue'
 
-  import { type StepContext, useCreateServiceStore } from 
'@/store/create-service'
-
   const { t } = useI18n()
   const route = useRoute()
   const createStore = useCreateServiceStore()
diff --git a/bigtop-manager-ui/src/components/select-lang/login-lang.vue 
b/bigtop-manager-ui/src/components/select-lang/login-lang.vue
new file mode 100644
index 00000000..9f29b768
--- /dev/null
+++ b/bigtop-manager-ui/src/components/select-lang/login-lang.vue
@@ -0,0 +1,70 @@
+<!--
+  ~ 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 { useLocaleStore } from '@/store/locale'
+  import { Locale } from '@/store/locale/types.ts'
+  import { storeToRefs } from 'pinia'
+
+  const localeStore = useLocaleStore()
+  const { locale } = storeToRefs(localeStore)
+
+  const handleClick = ({ key }: { key: Locale }) => {
+    localeStore.setLocale(key)
+  }
+</script>
+
+<template>
+  <a-dropdown placement="bottom">
+    <div class="icon">
+      <svg-icon name="carbon-language" />
+    </div>
+    <template #overlay>
+      <a-menu :selected-keys="[locale]" @click="handleClick">
+        <a-menu-item key="en_US">
+          <template #icon>
+            <span>🇺🇸</span>
+          </template>
+          English
+        </a-menu-item>
+        <a-menu-item key="zh_CN">
+          <template #icon>
+            <span>🇨🇳</span>
+          </template>
+          简体中文
+        </a-menu-item>
+      </a-menu>
+    </template>
+  </a-dropdown>
+</template>
+
+<style lang="scss" scoped>
+  .icon {
+    @include flexbox($justify: center, $align: center);
+    font-size: 16px;
+    cursor: pointer;
+    border-radius: 50%;
+    height: 36px;
+    width: 36px;
+
+    &:hover {
+      background-color: var(--hover-color);
+    }
+  }
+</style>
diff --git a/bigtop-manager-ui/src/components/service-management/overview.vue 
b/bigtop-manager-ui/src/components/service-management/overview.vue
index 6fe460d5..3cabfb30 100644
--- a/bigtop-manager-ui/src/components/service-management/overview.vue
+++ b/bigtop-manager-ui/src/components/service-management/overview.vue
@@ -21,8 +21,8 @@
   import { computed, ref, shallowRef, useAttrs } from 'vue'
   import { useI18n } from 'vue-i18n'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
-  import GaugeChart from 
'@/pages/cluster-manage/cluster/components/gauge-chart.vue'
-  import CategoryChart from 
'@/pages/cluster-manage/cluster/components/category-chart.vue'
+  import GaugeChart from '@/components/charts/gauge-chart.vue'
+  import CategoryChart from '@/components/charts/category-chart.vue'
   import type { ServiceVO, ServiceStatusType } from '@/api/service/types'
 
   type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
diff --git a/bigtop-manager-ui/src/composables/use-chart.ts 
b/bigtop-manager-ui/src/composables/use-chart.ts
new file mode 100644
index 00000000..c0c61aad
--- /dev/null
+++ b/bigtop-manager-ui/src/composables/use-chart.ts
@@ -0,0 +1,85 @@
+/*
+ * 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 * as echarts from 'echarts/core'
+import { GaugeChart, GaugeSeriesOption, LineChart, LineSeriesOption } from 
'echarts/charts'
+import { GridComponent, GridComponentOption, TooltipComponent, 
TooltipComponentOption } from 'echarts/components'
+import { UniversalTransition } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
+
+export type EChartsOption = echarts.ComposeOption<
+  GaugeSeriesOption | GridComponentOption | TooltipComponentOption | 
LineSeriesOption
+>
+
+echarts.use([
+  GaugeChart,
+  CanvasRenderer,
+  GridComponent,
+  LineChart,
+  TooltipComponent,
+  CanvasRenderer,
+  UniversalTransition
+])
+
+export const useChart = () => {
+  let resizeTimer: number | null = null
+  const chartsRef = shallowRef<echarts.ECharts | null>(null)
+  const opts = shallowRef({ devicePixelRatio: window.devicePixelRatio })
+
+  const initChart = (selector: HTMLElement, option: EChartsOption) => {
+    if (!selector) {
+      new Error('Selector is required to initialize the chart.')
+    }
+    chartsRef.value && chartsRef.value.dispose()
+    chartsRef.value = echarts.init(selector, null, opts.value)
+    chartsRef.value?.setOption(option)
+  }
+
+  const setOptions = (options: EChartsOption) => {
+    chartsRef.value?.setOption(options)
+  }
+
+  const resizeChart = () => {
+    if (resizeTimer) clearTimeout(resizeTimer)
+    resizeTimer = window.setTimeout(() => {
+      chartsRef.value?.resize()
+    }, 300)
+  }
+
+  const disposeChart = () => {
+    chartsRef.value?.dispose()
+    chartsRef.value = null
+  }
+
+  onMounted(() => {
+    window.addEventListener('resize', resizeChart, true)
+  })
+
+  onBeforeUnmount(() => {
+    window.removeEventListener('resize', resizeChart, true)
+    disposeChart()
+  })
+
+  return {
+    chartsRef,
+    initChart,
+    setOptions
+  }
+}
diff --git a/bigtop-manager-ui/src/layouts/index.vue 
b/bigtop-manager-ui/src/layouts/index.vue
index 1b1d26ff..d97dee2c 100644
--- a/bigtop-manager-ui/src/layouts/index.vue
+++ b/bigtop-manager-ui/src/layouts/index.vue
@@ -21,27 +21,6 @@
   import LayoutFooter from '@/layouts/footer.vue'
   import LayoutHeader from '@/layouts/header.vue'
   import LayoutSider from '@/layouts/sider.vue'
-
-  import { useUserStore } from '@/store/user'
-  import { useMenuStore } from '@/store/menu'
-  import { useClusterStore } from '@/store/cluster'
-  import { useStackStore } from '@/store/stack'
-  import { useServiceStore } from '@/store/service'
-  import { onMounted } from 'vue'
-
-  const userStore = useUserStore()
-  const menuStore = useMenuStore()
-  const serviceStore = useServiceStore()
-  const stackStore = useStackStore()
-  const clusterStore = useClusterStore()
-
-  onMounted(async () => {
-    await stackStore.loadStacks()
-    await clusterStore.loadClusters()
-    await serviceStore.getServicesOfInfra()
-    userStore.getUserInfo()
-    menuStore.setupMenu()
-  })
 </script>
 
 <template>
diff --git a/bigtop-manager-ui/src/layouts/sider.vue 
b/bigtop-manager-ui/src/layouts/sider.vue
index 3e1991c6..1a70130d 100644
--- a/bigtop-manager-ui/src/layouts/sider.vue
+++ b/bigtop-manager-ui/src/layouts/sider.vue
@@ -18,10 +18,10 @@
 -->
 
 <script setup lang="ts">
+  import { computed, onMounted, ref, watch } from 'vue'
   import { useRouter, useRoute } from 'vue-router'
   import { useMenuStore } from '@/store/menu/index'
   import { storeToRefs } from 'pinia'
-  import { computed, ref, watch } from 'vue'
   import { useClusterStore } from '@/store/cluster'
   import type { ClusterStatusType } from '@/api/cluster/types'
 
@@ -44,27 +44,39 @@
     })
   )
 
-  watch(
-    () => route,
-    (newRoute) => {
-      const { params, path, meta } = newRoute
-      const targetCluster = clusterMap.value[`${params.id}`]
-      routeParamsLen.value = Object.keys(params).length
-      if (path.includes(routePathFromClusters.value) && routeParamsLen.value > 
0 && clusterCount.value > 0) {
-        if (targetCluster) {
-          // siderMenuSelectedKey.value = 
`${routePathFromClusters.value}/${targetCluster.name}/${targetCluster.id}`
-          siderMenuSelectedKey.value = 
`${routePathFromClusters.value}/${targetCluster.id}`
-        }
+  /**
+   * Handles route changes and updates the selected menu key.
+   */
+  const handleRouteChange = async (newRoute: typeof route) => {
+    const token = localStorage.getItem('Token') ?? 
sessionStorage.getItem('Token') ?? undefined
+
+    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}`
+        openKeys.value.push(siderMenuSelectedKey.value)
       } else {
-        siderMenuSelectedKey.value = meta.activeMenu ?? path
+        siderMenuSelectedKey.value = ''
+        menuStore.setupSider()
       }
-    },
-    {
-      deep: true,
-      immediate: true
+    } else {
+      siderMenuSelectedKey.value = meta.activeMenu ?? path
     }
-  )
+  }
 
+  /**
+   * Toggles the activated icon for menu items.
+   */
   const toggleActivatedIcon = (menuItem: { key: string; icon: string }) => {
     const { key, icon } = menuItem
     const matchedRouteFromClusters = selectMenuKeyFromClusters.value && 
routeParamsLen.value > 0
@@ -76,12 +88,25 @@
   const addCluster = () => {
     router.push({ name: 'CreateCluster' })
   }
+
+  onMounted(async () => {
+    await clusterStore.loadClusters()
+  })
+
+  watch(
+    () => [route, clusterCount],
+    (val) => {
+      const [newRoute] = val
+      handleRouteChange(newRoute as typeof route)
+    },
+    { deep: true, immediate: true }
+  )
 </script>
 
 <template>
   <a-layout-sider class="sider">
     <a-menu
-      v-model="openKeys"
+      :open-keys="openKeys"
       :selected-keys="[siderMenuSelectedKey]"
       mode="inline"
       @click="({ key }) => menuStore.onSiderClick(key)"
diff --git 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/gauge-chart.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/gauge-chart.vue
deleted file mode 100644
index d2d2dcaa..00000000
--- 
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/gauge-chart.vue
+++ /dev/null
@@ -1,148 +0,0 @@
-<!--
-  ~ Licensed to the Apache Software Foundation (ASF) under one
-  ~ or more contributor license agreements.  See the NOTICE file
-  ~ distributed with this work for additional information
-  ~ regarding copyright ownership.  The ASF licenses this file
-  ~ to you under the Apache License, Version 2.0 (the
-  ~ "License"); you may not use this file except in compliance
-  ~ with the License.  You may obtain a copy of the License at
-  ~
-  ~   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 { onBeforeUnmount, onMounted, shallowRef } from 'vue'
-  import * as echarts from 'echarts/core'
-  import { GaugeChart, GaugeSeriesOption } from 'echarts/charts'
-  import { CanvasRenderer } from 'echarts/renderers'
-
-  echarts.use([GaugeChart, CanvasRenderer])
-
-  type EChartsOption = echarts.ComposeOption<GaugeSeriesOption>
-
-  const props = defineProps<{
-    chartId: string
-    title: string
-  }>()
-
-  const myChart = shallowRef<echarts.ECharts | null>(null)
-
-  const initCharts = () => {
-    const chartDom = document.getElementById(`${props.chartId}`)
-    myChart.value = echarts.init(chartDom, null, {
-      devicePixelRatio: window.devicePixelRatio
-    })
-    const option: EChartsOption = {
-      series: [
-        {
-          type: 'gauge',
-          radius: '90%',
-          center: ['50%', '50%'],
-          axisLine: {
-            lineStyle: {
-              width: 14,
-              color: [
-                [0.3, '#67e0e3'],
-                [0.7, '#37a2da'],
-                [1, '#fd666d']
-              ]
-            }
-          },
-          pointer: {
-            itemStyle: {
-              color: 'auto'
-            }
-          },
-          axisTick: {
-            distance: -50,
-            length: 40,
-            lineStyle: {
-              color: '#fff',
-              width: 1
-            }
-          },
-          splitLine: {
-            distance: -28,
-            length: 28,
-            lineStyle: {
-              color: '#fff',
-              width: 4
-            }
-          },
-          axisLabel: {
-            color: 'inherit',
-            distance: 28,
-            fontSize: 14
-          },
-          detail: {
-            valueAnimation: true,
-            formatter: '{value} %',
-            color: 'inherit',
-            fontSize: 18
-          },
-          data: [
-            {
-              value: 80
-            }
-          ]
-        }
-      ]
-    }
-
-    myChart.value?.setOption<echarts.EChartsCoreOption>({
-      series: [
-        {
-          data: [
-            {
-              value: +(Math.random() * 100).toFixed(2)
-            }
-          ]
-        }
-      ]
-    })
-
-    option && myChart.value.setOption(option)
-  }
-
-  const resizeChart = async () => {
-    setTimeout(() => {
-      myChart.value?.resize()
-    })
-  }
-
-  onMounted(() => {
-    initCharts()
-    window.addEventListener('resize', resizeChart, true)
-  })
-
-  onBeforeUnmount(() => {
-    window.removeEventListener('resize', resizeChart, true)
-  })
-</script>
-
-<template>
-  <div class="chart">
-    <div class="chart-title">{{ $props.title }}</div>
-    <div :id="$props.chartId" style="height: 260px; width: 100%"></div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-  .chart {
-    height: 300px;
-    box-sizing: border-box;
-    padding: 10px;
-    &-title {
-      text-align: start;
-      font-size: 12px;
-      font-weight: 600;
-    }
-  }
-</style>
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 a2b212d4..1a2253af 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -24,9 +24,11 @@
   import { getHosts } from '@/api/hosts'
   import * as hostApi from '@/api/hosts'
   import { useRouter } from 'vue-router'
+
   import useBaseTable from '@/composables/use-base-table'
   import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
   import InstallDependencies from 
'@/pages/cluster-manage/hosts/install-dependencies.vue'
+
   import type { FilterConfirmProps, FilterResetProps } from 
'ant-design-vue/es/table/interface'
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { HostVO } from '@/api/hosts/types'
@@ -34,6 +36,7 @@
   import type { HostReq } from '@/api/command/types'
 
   type Key = string | number
+
   interface TableState {
     selectedRowKeys: Key[]
     searchText: string
@@ -43,10 +46,12 @@
   const { t } = useI18n()
   const router = useRouter()
   const attrs = useAttrs() as ClusterVO
+
   const searchInputRef = ref()
   const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
   const installRef = ref<InstanceType<typeof InstallDependencies> | null>(null)
   const hostStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
+
   const state = reactive<TableState>({
     searchText: '',
     searchedColumn: '',
@@ -93,18 +98,9 @@
       ellipsis: true,
       filterMultiple: false,
       filters: [
-        {
-          text: t('common.success'),
-          value: 1
-        },
-        {
-          text: t('common.failed'),
-          value: 2
-        },
-        {
-          text: t('common.unknown'),
-          value: 3
-        }
+        { text: t('common.success'), value: 1 },
+        { text: t('common.failed'), value: 2 },
+        { text: t('common.unknown'), value: 3 }
       ]
     },
     {
@@ -121,15 +117,8 @@
   })
 
   const operations = computed((): GroupItem[] => [
-    {
-      text: 'edit',
-      clickEvent: (_item, args) => handleEdit(args)
-    },
-    {
-      text: 'remove',
-      danger: true,
-      clickEvent: (_item, args) => deleteHost([args.id])
-    }
+    { text: 'edit', clickEvent: (_item, args) => handleEdit(args) },
+    { text: 'remove', danger: true, clickEvent: (_item, args) => 
deleteHost([args.id]) }
   ])
 
   const onFilterDropdownOpenChange = (visible: boolean) => {
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 5f056391..fd056ec5 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/index.vue
@@ -18,130 +18,113 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onMounted, ref, shallowRef } from 'vue'
+  import { computed, ref, shallowRef } from 'vue'
   import { useI18n } from 'vue-i18n'
   import { useClusterStore } from '@/store/cluster'
   import { storeToRefs } from 'pinia'
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import { useRouter } from 'vue-router'
   import { useJobProgress } from '@/store/job-progress'
+
   import Overview from './overview.vue'
   import Service from './service.vue'
   import Host from './host.vue'
   import User from './user.vue'
   import Job from '@/components/job/index.vue'
+
   import type { Command } from '@/api/command/types'
   import type { TabItem } from '@/components/common/main-card/types'
   import type { GroupItem } from '@/components/common/button-group/types'
-  import type { ClusterStatusType } from '@/api/cluster/types'
+  import type { ClusterStatusType, ClusterVO } from '@/api/cluster/types'
 
   const { t } = useI18n()
   const router = useRouter()
   const jobProgressStore = useJobProgress()
   const clusterStore = useClusterStore()
-  const { currCluster, loading } = storeToRefs(clusterStore)
+  const { loading } = storeToRefs(clusterStore)
+
   const activeKey = ref('1')
+  const clusterInfo = ref<ClusterVO>({})
+
   const statusColors = shallowRef<Record<ClusterStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
     2: 'unhealthy',
     3: 'unknown'
   })
+
+  /**
+   * Determines the component to render based on the active tab.
+   */
+  const getCompName = computed(() => [Overview, Service, Host, User, 
Job][parseInt(activeKey.value) - 1])
+
   const tabs = computed((): TabItem[] => [
-    {
-      key: '1',
-      title: t('common.overview')
-    },
-    {
-      key: '2',
-      title: t('common.service')
-    },
-    {
-      key: '3',
-      title: t('common.host')
-    },
-    {
-      key: '4',
-      title: t('common.user')
-    },
-    {
-      key: '5',
-      title: t('common.job')
-    }
+    { key: '1', title: t('common.overview') },
+    { key: '2', title: t('common.service') },
+    { key: '3', title: t('common.host') },
+    { key: '4', title: t('common.user') },
+    { key: '5', title: t('common.job') }
   ])
+
   const actionGroup = computed<GroupItem[]>(() => [
     {
       shape: 'default',
       type: 'primary',
       text: t('common.add', [t('common.service')]),
-      clickEvent: () => addService && addService()
+      clickEvent: () => addService!()
     },
     {
       shape: 'default',
       type: 'default',
       text: t('common.more_operations'),
       dropdownMenu: [
-        {
-          action: 'Start',
-          text: t('common.start', [t('common.cluster')])
-        },
-        {
-          action: 'Restart',
-          text: t('common.restart', [t('common.cluster')])
-        },
-        {
-          action: 'Stop',
-          text: t('common.stop', [t('common.cluster')])
-        }
+        { action: 'Start', text: t('common.start', [t('common.cluster')]) },
+        { action: 'Restart', text: t('common.restart', [t('common.cluster')]) 
},
+        { action: 'Stop', text: t('common.stop', [t('common.cluster')]) }
       ],
-      dropdownMenuClickEvent: (info) => dropdownMenuClick && 
dropdownMenuClick(info)
+      dropdownMenuClickEvent: (info) => dropdownMenuClick!(info)
     }
   ])
 
-  const getCompName = computed(() => {
-    const components = [Overview, Service, Host, User, Job]
-    return components[parseInt(activeKey.value) - 1]
-  })
-
+  /**
+   * Handles dropdown menu click events for cluster operations.
+   */
   const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key 
}) => {
     try {
       await jobProgressStore.processCommand(
         {
           command: key as keyof typeof Command,
-          clusterId: currCluster.value.id,
+          clusterId: clusterInfo.value.id,
           commandLevel: 'cluster'
         },
         async () => {
           await clusterStore.loadClusters()
-          await clusterStore.getClusterDetail()
         }
       )
     } catch (error) {
-      console.log('error :>> ', error)
+      console.error('Error processing command:', error)
+    } finally {
+      await clusterStore.getClusterDetail(clusterInfo.value.id!)
     }
   }
 
   const addService: GroupItem['clickEvent'] = () => {
-    router.push({ name: 'CreateService', params: { id: currCluster.value.id, 
creationMode: 'internal' } })
+    router.push({ name: 'CreateService', params: { id: clusterInfo.value.id, 
creationMode: 'internal' } })
   }
-
-  onMounted(() => {
-    clusterStore.getClusterDetail()
-  })
 </script>
 
 <template>
   <a-spin :spinning="loading">
     <header-card
-      :title="currCluster.displayName"
+      :title="clusterInfo.displayName"
       avatar="cluster"
-      :status="CommonStatus[statusColors[currCluster.status as 
ClusterStatusType]]"
-      :desc="currCluster.desc"
+      :status="CommonStatus[statusColors[clusterInfo.status as 
ClusterStatusType]]"
+      :desc="clusterInfo.desc"
       :action-groups="actionGroup"
     />
     <main-card v-model:active-key="activeKey" :tabs="tabs">
       <template #tab-item>
         <keep-alive>
-          <component :is="getCompName" v-bind="currCluster"></component>
+          <component :is="getCompName" v-bind="clusterInfo" 
v-model:payload="clusterInfo"></component>
         </keep-alive>
       </template>
     </main-card>
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 05b7df00..3a20ab30 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
@@ -18,7 +18,7 @@
 -->
 
 <script setup lang="ts">
-  import { computed, onActivated, ref, shallowRef, useAttrs, watch, 
watchEffect } from 'vue'
+  import { computed, onActivated, ref, shallowRef, toRefs, watchEffect } from 
'vue'
   import { useI18n } from 'vue-i18n'
   import { storeToRefs } from 'pinia'
   import { formatFromByte } from '@/utils/storage'
@@ -27,13 +27,15 @@
   import { useServiceStore } from '@/store/service'
   import { useJobProgress } from '@/store/job-progress'
   import { useStackStore } from '@/store/stack'
+  import { useClusterStore } from '@/store/cluster'
   import { Empty } from 'ant-design-vue'
+  import { useRoute } from 'vue-router'
 
-  import GaugeChart from './components/gauge-chart.vue'
-  import CategoryChart from './components/category-chart.vue'
+  import GaugeChart from '@/components/charts/gauge-chart.vue'
+  import CategoryChart from '@/components/charts/category-chart.vue'
 
   import type { ClusterStatusType, ClusterVO } from '@/api/cluster/types'
-  import type { ServiceListParams, ServiceVO } from '@/api/service/types'
+  import type { ServiceVO } from '@/api/service/types'
   import type { MenuItem } from '@/store/menu/types'
   import type { StackVO } from '@/api/stack/types'
   import type { Command } from '@/api/command/types'
@@ -44,11 +46,15 @@
     time: string
   }
 
+  const props = defineProps<{ payload: ClusterVO }>()
+  const emits = defineEmits<{ (event: 'update:payload', value: ClusterVO): 
void }>()
+
   const { t } = useI18n()
-  const attrs = useAttrs() as ClusterVO
+  const route = useRoute()
   const jobProgressStore = useJobProgress()
   const stackStore = useStackStore()
   const serviceStore = useServiceStore()
+  const clusterStore = useClusterStore()
   const currTimeRange = ref<TimeRangeText>('15m')
   const chartData = ref({
     chart1: [],
@@ -64,40 +70,27 @@
   const { serviceNames } = storeToRefs(serviceStore)
   const locateStackWithService = shallowRef<StackVO[]>([])
 
+  const { payload } = toRefs(props)
+
   const clusterDetail = computed(() => ({
-    ...attrs,
-    totalMemory: formatFromByte(attrs.totalMemory as number),
-    totalDisk: formatFromByte(attrs.totalDisk as number)
+    ...payload.value,
+    totalMemory: formatFromByte(payload.value.totalMemory as number),
+    totalDisk: formatFromByte(payload.value.totalDisk as number)
   }))
+
+  const clusterId = computed(() => route.params.id as unknown as number)
   const noChartData = computed(() => Object.values(chartData.value).every((v) 
=> v.length === 0))
   const timeRanges = computed((): TimeRangeItem[] => [
-    {
-      text: '1m',
-      time: ''
-    },
-    {
-      text: '15m',
-      time: ''
-    },
-    {
-      text: '30m',
-      time: ''
-    },
-    {
-      text: '1h',
-      time: ''
-    },
-    {
-      text: '6h',
-      time: ''
-    },
-    {
-      text: '30h',
-      time: ''
-    }
+    { text: '1m', time: '' },
+    { text: '15m', time: '' },
+    { text: '30m', time: '' },
+    { text: '1h', time: '' },
+    { text: '6h', time: '' },
+    { text: '30h', time: '' }
   ])
-  const baseConfig = computed((): Partial<Record<keyof ClusterVO, string>> => {
-    return {
+
+  const baseConfig = computed(
+    (): Partial<Record<keyof ClusterVO, string>> => ({
       status: t('overview.cluster_status'),
       displayName: t('overview.cluster_name'),
       desc: t('overview.cluster_desc'),
@@ -107,36 +100,29 @@
       totalProcessor: t('overview.core_count'),
       totalDisk: t('overview.disk_size'),
       createUser: t('overview.creator')
-    }
-  })
-  const unitOfBaseConfig = computed((): Partial<Record<keyof ClusterVO, 
string>> => {
-    return {
+    })
+  )
+
+  const unitOfBaseConfig = computed(
+    (): Partial<Record<keyof ClusterVO, string>> => ({
       totalHost: t('overview.unit_host'),
       totalService: t('overview.unit_service'),
       totalProcessor: t('overview.unit_core')
-    }
-  })
+    })
+  )
+
   const detailKeys = computed(() => Object.keys(baseConfig.value) as (keyof 
ClusterVO)[])
   const serviceOperates = computed(() => [
-    {
-      action: 'Start',
-      text: t('common.start', [t('common.service')])
-    },
-    {
-      action: 'Restart',
-      text: t('common.restart', [t('common.service')])
-    },
-    {
-      action: 'Stop',
-      text: t('common.stop', [t('common.service')])
-    }
+    { action: 'Start', text: t('common.start', [t('common.service')]) },
+    { action: 'Restart', text: t('common.restart', [t('common.service')]) },
+    { action: 'Stop', text: t('common.stop', [t('common.service')]) }
   ])
 
   const handleServiceOperate = async (item: MenuItem, service: ServiceVO) => {
     try {
       await jobProgressStore.processCommand({
         command: item.key as keyof typeof Command,
-        clusterId: attrs.id,
+        clusterId: clusterId.value,
         commandLevel: 'service',
         serviceCommands: [{ serviceName: service.name!, installed: true }]
       })
@@ -149,16 +135,13 @@
     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()
+  onActivated(async () => {
+    await clusterStore.getClusterDetail(clusterId.value)
+    emits('update:payload', clusterStore.currCluster)
   })
 
   watchEffect(() => {
@@ -166,14 +149,6 @@
       item.services.some((service) => service.name && 
serviceNames.value.includes(service.name))
     )
   })
-
-  watch(
-    () => attrs,
-    () => {
-      getServices()
-    },
-    { immediate: true, deep: true }
-  )
 </script>
 
 <template>
@@ -199,7 +174,7 @@
                         <a-typography-text
                           class="desc-sub-item-desc-column"
                           type="secondary"
-                          :content="baseConfig[base]"
+                          :content="baseConfig[base] ?? '--'"
                         />
                         <a-tag
                           v-if="base === 'status'"
@@ -217,8 +192,8 @@
                           class="desc-sub-item-desc-column"
                           :content="
                             Object.keys(unitOfBaseConfig).includes(base)
-                              ? `${clusterDetail[base]} 
${unitOfBaseConfig[base]}`
-                              : `${clusterDetail[base]}`
+                              ? `${clusterDetail[base] ?? '--'} 
${unitOfBaseConfig[base]}`
+                              : `${clusterDetail[base] ?? '--'}`
                           "
                         />
                       </div>
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 c45178e6..b9cb7922 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -26,7 +26,9 @@
   import { CommonStatus, CommonStatusTexts } from '@/enums/state'
   import { useRouter } from 'vue-router'
   import { useJobProgress } from '@/store/job-progress'
+
   import FilterForm from '@/components/common/filter-form/index.vue'
+
   import type { GroupItem } from '@/components/common/button-group/types'
   import type { FilterFormItem } from '@/components/common/filter-form/types'
   import type { ServiceListParams, ServiceStatusType, ServiceVO } from 
'@/api/service/types'
@@ -41,6 +43,7 @@
   const jobProgressStore = useJobProgress()
   const serviceStore = useServiceStore()
   const { services, loading } = toRefs(serviceStore)
+
   const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof 
CommonStatusTexts>>({
     1: 'healthy',
     2: 'unhealthy',
@@ -89,14 +92,8 @@
       key: 'restartFlag',
       label: t('service.required_restart'),
       options: [
-        {
-          label: t('common.required'),
-          value: true
-        },
-        {
-          label: t('common.not_required'),
-          value: false
-        }
+        { label: t('common.required'), value: true },
+        { label: t('common.not_required'), value: false }
       ]
     },
     {
@@ -104,18 +101,9 @@
       key: 'status',
       label: t('common.status'),
       options: [
-        {
-          label: t(`common.${statusColors.value[1]}`),
-          value: 1
-        },
-        {
-          label: t(`common.${statusColors.value[2]}`),
-          value: 2
-        },
-        {
-          label: t(`common.${statusColors.value[3]}`),
-          value: 3
-        }
+        { label: t(`common.${statusColors.value[1]}`), value: 1 },
+        { label: t(`common.${statusColors.value[2]}`), value: 2 },
+        { label: t(`common.${statusColors.value[3]}`), value: 3 }
       ]
     }
   ])
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
index fc967256..62d73e6f 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/user.vue
@@ -21,12 +21,15 @@
   import { computed, onActivated, useAttrs } from 'vue'
   import { useI18n } from 'vue-i18n'
   import { getUserListOfService } from '@/api/cluster'
+
   import useBaseTable from '@/composables/use-base-table'
+
   import type { TableColumnType } from 'ant-design-vue'
   import type { ClusterVO, ServiceUserVO } from '@/api/cluster/types'
 
   const { t } = useI18n()
   const attrs = useAttrs() as ClusterVO
+
   const columns = computed((): TableColumnType[] => [
     {
       title: '#',
@@ -60,6 +63,7 @@
       ellipsis: true
     }
   ])
+
   const { dataSource, loading, filtersParams, paginationProps, onChange } = 
useBaseTable<ServiceUserVO>({
     columns: columns.value,
     rows: []
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/components/index.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/components/index.vue
index e020f244..e7b40c22 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/components/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/components/index.vue
@@ -19,7 +19,7 @@
 <script setup lang="ts">
   import { ref, reactive, computed, onMounted, watchEffect } from 'vue'
   import useBaseTable from '@/composables/use-base-table'
-  import SetSource from 
'@/pages/cluster-manage/cluster/components/set-source.vue'
+  import SetSource from '@/components/create-cluster/components/set-source.vue'
   import { useI18n } from 'vue-i18n'
   import { useStackStore } from '@/store/stack'
   import { storeToRefs } from 'pinia'
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue 
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
index 54fece85..ce1b8059 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -29,8 +29,8 @@
   import { useStackStore } from '@/store/stack'
   import { getComponentsByHost } from '@/api/hosts'
   import { Command } from '@/api/command/types'
-  import CategoryChart from 
'@/pages/cluster-manage/cluster/components/category-chart.vue'
-  import GaugeChart from 
'@/pages/cluster-manage/cluster/components/gauge-chart.vue'
+  import GaugeChart from '@/components/charts/gauge-chart.vue'
+  import CategoryChart from '@/components/charts/category-chart.vue'
   import type { HostStatusType, HostVO } from '@/api/hosts/types.ts'
   import type { ClusterStatusType } from '@/api/cluster/types.ts'
   import type { ComponentVO } from '@/api/component/types.ts'
diff --git a/bigtop-manager-ui/src/pages/login/index.vue 
b/bigtop-manager-ui/src/pages/login/index.vue
index 907857e2..5719b2c7 100644
--- a/bigtop-manager-ui/src/pages/login/index.vue
+++ b/bigtop-manager-ui/src/pages/login/index.vue
@@ -24,10 +24,17 @@
   import { getSalt, getNonce, login } from '@/api/login'
   import { message } from 'ant-design-vue'
   import { useI18n } from 'vue-i18n'
-  import SelectLang from '@/components/select-lang/index.vue'
+  import SelectLang from '@/components/select-lang/login-lang.vue'
   import { deriveKey } from '@/utils/pbkdf2.ts'
+  import { useStackStore } from '@/store/stack'
+  import { useUserStore } from '@/store/user'
+  import { useMenuStore } from '@/store/menu'
 
   const i18n = useI18n()
+  const stackStore = useStackStore()
+  const userStore = useUserStore()
+  const menuStore = useMenuStore()
+
   const router = useRouter()
 
   const formRef = shallowRef()
@@ -67,6 +74,10 @@
       } else {
         sessionStorage.setItem('Token', res.token)
       }
+      userStore.getUserInfo()
+      stackStore.loadStacks()
+      menuStore.setupMenu()
+
       message.success(i18n.t('login.login_success'))
       router.push('/')
     } catch (e) {
diff --git a/bigtop-manager-ui/src/router/guard.ts 
b/bigtop-manager-ui/src/router/guard.ts
index e907185e..61824c91 100644
--- a/bigtop-manager-ui/src/router/guard.ts
+++ b/bigtop-manager-ui/src/router/guard.ts
@@ -17,25 +17,24 @@
  * under the License.
  */
 
-import type { NavigationGuardNext, Router } from 'vue-router'
-import { useClusterStore } from '@/store/cluster'
+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
+    }
+
     if (to.name === 'Clusters' && token) {
-      checkClusterSelect(next)
+      next({ name: 'Default' })
     } else {
       next()
     }
   })
 }
 
-function checkClusterSelect(next: NavigationGuardNext) {
-  const clusterStore = useClusterStore()
-  clusterStore.clusterCount === 0 && clusterStore.loadClusters()
-  clusterStore.clusterCount === 0 && next({ name: 'Default' })
-}
-
 function createRouterGuard(router: Router) {
   /** common guard */
   setCommonGuard(router)
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts 
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index be915a89..0651679e 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -59,7 +59,7 @@ const routes: RouteRecordRaw[] = [
           {
             name: 'CreateCluster',
             path: 'create-cluster',
-            component: () => 
import('@/pages/cluster-manage/cluster/create.vue'),
+            component: () => import('@/components/create-cluster/create.vue'),
             meta: {
               hidden: true
             }
diff --git a/bigtop-manager-ui/src/store/cluster/index.ts 
b/bigtop-manager-ui/src/store/cluster/index.ts
index c42048dd..6301711e 100644
--- a/bigtop-manager-ui/src/store/cluster/index.ts
+++ b/bigtop-manager-ui/src/store/cluster/index.ts
@@ -19,21 +19,20 @@
 
 import { computed, ref } from 'vue'
 import { defineStore } from 'pinia'
-import { useRoute } from 'vue-router'
 import { getCluster, getClusterList } from '@/api/cluster'
 import { useServiceStore } from '@/store/service'
+import { useMenuStore } from '@/store/menu'
 import type { ClusterVO } from '@/api/cluster/types.ts'
 
 export const useClusterStore = defineStore(
   'cluster',
   () => {
-    const route = useRoute()
     const serviceStore = useServiceStore()
+    const menuStore = useMenuStore()
     const loading = ref(false)
     const clusters = ref<ClusterVO[]>([])
     const currCluster = ref<ClusterVO>({})
     const clusterMap = ref<Record<string, ClusterVO>>({})
-    const clusterId = computed(() => (route.params.id as string) || undefined)
     const clusterCount = computed(() => Object.values(clusterMap.value).length)
 
     const loadClusters = async () => {
@@ -47,23 +46,25 @@ export const useClusterStore = defineStore(
           {} as Record<string, ClusterVO>
         )
       } catch (error) {
+        const token = localStorage.getItem('Token') ?? 
sessionStorage.getItem('Token') ?? undefined
         clusterMap.value = {}
-        console.log('error :>> ', error)
+        token && menuStore.setupMenu()
+        console.error('Failed to get clusters:', error)
       }
     }
 
-    const getClusterDetail = async () => {
-      if (clusterId.value == undefined) {
+    const getClusterDetail = async (clusterId: number) => {
+      if (clusterId == undefined) {
         currCluster.value = {}
         return
       }
       try {
         loading.value = true
-        currCluster.value = await getCluster(Number(clusterId.value))
-        await serviceStore.getServices(Number(clusterId.value))
+        currCluster.value = await getCluster(clusterId)
+        await serviceStore.getServices(clusterId)
       } catch (error) {
         currCluster.value = {}
-        console.log('error :>> ', error)
+        console.error('Failed to get cluster detail:', error)
       } finally {
         loading.value = false
       }
diff --git a/bigtop-manager-ui/src/store/menu/index.ts 
b/bigtop-manager-ui/src/store/menu/index.ts
index 1f0ed985..b25dd069 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -29,14 +29,19 @@ export const useMenuStore = defineStore(
     const router = useRouter()
     const route = useRoute()
     const clusterStore = useClusterStore()
+
     const baseRoutesMap = ref<Record<string, RouteRecordRaw>>({})
     const headerMenus = ref<RouteRecordRaw[]>([])
     const siderMenus = ref<RouteRecordRaw[]>([])
     const headerSelectedKey = ref()
     const siderMenuSelectedKey = ref()
     const routePathFromClusters = shallowRef('/cluster-manage/clusters')
+
     const clusterList = computed(() => Object.values(clusterStore.clusterMap))
 
+    /**
+     * Build the mapping of base routes from dynamic routes.
+     */
     const buildMenuMap = () => {
       baseRoutesMap.value = dr.reduce((buildRes, { path, name, meta, children 
}) => {
         !meta?.hidden && (buildRes[path] = { path, name, meta, children })
@@ -44,24 +49,45 @@ export const useMenuStore = defineStore(
       }, {})
     }
 
+    /**
+     * Setup the header menu based on the current route.
+     */
     const setupHeader = () => {
-      headerSelectedKey.value = route.matched[0].path ?? '/cluster-manage'
+      if (route.matched[0].path === '/login') {
+        headerSelectedKey.value = '/cluster-manage'
+      } else {
+        headerSelectedKey.value = route.matched[0].path ?? '/cluster-manage'
+      }
       headerMenus.value = Object.values(baseRoutesMap.value)
       siderMenus.value = 
baseRoutesMap.value[headerSelectedKey.value]?.children || []
     }
 
-    const setupSider = () => {
+    /**
+     * Setup the sider menu and handle cluster-based navigation.
+     */
+    const setupSider = async () => {
       siderMenus.value = 
baseRoutesMap.value[headerSelectedKey.value]?.children || []
+
+      // If the first sider menu has a redirect, set it as the selected key
       if (siderMenus.value[0]?.redirect) {
         siderMenuSelectedKey.value = siderMenus.value[0].redirect
-      } else {
-        if (clusterList.value.length > 0) {
-          const { id } = clusterList.value[0]
-          onSiderClick(`${routePathFromClusters.value}/${id}`)
-        } else {
-          onSiderClick(`${routePathFromClusters.value}/default`)
-        }
+        return
       }
+
+      const targetPath =
+        clusterList.value.length > 0
+          ? `${routePathFromClusters.value}/${clusterList.value[0].id}`
+          : `${routePathFromClusters.value}/default`
+
+      navigateToSider(targetPath)
+    }
+
+    /**
+     * Navigate to a specific sider menu path.
+     * @param path - The path to navigate to.
+     */
+    const navigateToSider = (path: string) => {
+      router.push({ path })
     }
 
     const onHeaderClick = (key: string) => {
@@ -74,35 +100,66 @@ export const useMenuStore = defineStore(
       router.push({ path: key })
     }
 
+    /**
+     * Update the sider menu based on the last cluster in the list.
+     */
     const updateSider = async () => {
-      await clusterStore.loadClusters()
-      const { id } = clusterList.value[clusterList.value.length - 1]
-      await nextTick()
-      onSiderClick(`${routePathFromClusters.value}/${id}`)
+      try {
+        await clusterStore.loadClusters()
+        const lastClusterId = clusterList.value[clusterList.value.length - 
1]?.id
+        if (lastClusterId) {
+          await nextTick()
+          navigateToSider(`${routePathFromClusters.value}/${lastClusterId}`)
+        }
+      } catch (error) {
+        console.error('Error updating sider menu:', error)
+      }
     }
 
-    const setupMenu = async () => {
+    /**
+     * Initialize the menu by setting up the header and sider menus.
+     */
+    const setupMenu = () => {
       buildMenuMap()
       setupHeader()
       setupSider()
     }
 
+    const $reset = () => {
+      headerMenus.value = []
+      siderMenus.value = []
+      headerSelectedKey.value = undefined
+      siderMenuSelectedKey.value = undefined
+      baseRoutesMap.value = {}
+      clusterStore.$reset()
+    }
+
     return {
       headerMenus,
       siderMenus,
       headerSelectedKey,
       siderMenuSelectedKey,
       routePathFromClusters,
+      baseRoutesMap,
       setupMenu,
+      setupSider,
       updateSider,
       onHeaderClick,
-      onSiderClick
+      onSiderClick,
+      reset: $reset
     }
   },
   {
     persist: {
       storage: localStorage,
-      paths: ['headerMenus', 'siderMenus']
+      paths: [
+        'headerMenus',
+        'siderMenus',
+        'siderMenuSelectedKey',
+        'headerSelectedKey',
+        'routePathFromClusters',
+        'baseRoutesMap'
+      ]
     }
   }
 )
diff --git a/bigtop-manager-ui/src/store/stack/index.ts 
b/bigtop-manager-ui/src/store/stack/index.ts
index 30aeda34..32646c41 100644
--- a/bigtop-manager-ui/src/store/stack/index.ts
+++ b/bigtop-manager-ui/src/store/stack/index.ts
@@ -32,10 +32,12 @@ export type ServiceMap = {
   configs: ServiceConfig
   requiredServices: string[]
 }
+
 export type ComponentMap = {
   service: string
   stack: string
 } & ComponentVO
+
 export interface StackRelationMap {
   services: ServiceMap
   components: ComponentMap
@@ -47,20 +49,29 @@ export const useStackStore = defineStore(
     const stacks = shallowRef<StackVO[]>([])
     const stackRelationMap = shallowRef<StackRelationMap>()
 
-    const loadStacks = async () => {
+    const loadStacks = async (): Promise<void> => {
       try {
-        stacks.value = await getStacks()
-        stackRelationMap.value = setupStackRelationMap(stacks.value)
+        const fetchedStacks = await getStacks()
+        stacks.value = fetchedStacks
+        stackRelationMap.value = setupStackRelationMap(fetchedStacks)
       } catch (error) {
-        console.log('error :>> ', error)
+        console.error('Error loading stacks:', error)
       }
     }
 
-    const setupStackRelationMap = (stacks: StackVO[]) => {
+    /**
+     * Set up the relation map for services and components based on stack data.
+     * @param stacks - Array of stack data.
+     * @returns A relation map containing services and components.
+     */
+    const setupStackRelationMap = (stacks: StackVO[]): StackRelationMap => {
       const relationMap = { services: {}, components: {} } as StackRelationMap
+
       for (const { stackName, services } of stacks) {
         for (const service of services) {
           const { name, displayName, components, configs } = service
+
+          // Map service data
           relationMap.services[name!] = {
             displayName,
             stack: stackName,
@@ -68,6 +79,8 @@ export const useStackStore = defineStore(
             configs,
             requiredServices: service.requiredServices
           }
+
+          // Map component data
           for (const component of components!) {
             relationMap.components[component.name!] = {
               ...component,
@@ -82,6 +95,7 @@ export const useStackStore = defineStore(
 
     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
     }
 
@@ -94,7 +108,8 @@ export const useStackStore = defineStore(
   },
   {
     persist: {
-      storage: sessionStorage
+      storage: localStorage,
+      paths: ['stacks', 'stackRelationMap']
     }
   }
 )
diff --git a/bigtop-manager-ui/src/store/user/index.ts 
b/bigtop-manager-ui/src/store/user/index.ts
index 29ef16b3..4d38f0ee 100644
--- a/bigtop-manager-ui/src/store/user/index.ts
+++ b/bigtop-manager-ui/src/store/user/index.ts
@@ -50,6 +50,9 @@ export const useUserStore = defineStore(
     }
   },
   {
-    persist: false
+    persist: {
+      storage: localStorage,
+      paths: ['userVO']
+    }
   }
 )
diff --git a/bigtop-manager-ui/src/styles/index.scss 
b/bigtop-manager-ui/src/styles/index.scss
index 4bcb02ea..4f319c23 100644
--- a/bigtop-manager-ui/src/styles/index.scss
+++ b/bigtop-manager-ui/src/styles/index.scss
@@ -19,6 +19,35 @@
 
 @import './variables.scss';
 
+* {
+  outline: 0;
+}
+
+html {
+  --text-color: rgba(0, 0, 0, 0.85);
+  --text-color-desc: rgba(0, 0, 0, 0.45);
+  --bg-color: #fff;
+  --hover-color: rgba(0, 0, 0, 0.05);
+  --bg-color-container: #f0f2f5;
+  --c-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
+}
+
+.m-0 {
+  margin: 0;
+}
+
+.p-0 {
+  padding: 0;
+}
+
+.hidden {
+  display: none;
+}
+
+.ant-table-body {
+  overflow-y: auto !important;
+}
+
 #app {
   width: 100%;
   box-sizing: border-box;

Reply via email to