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