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 036a85db BIGTOP-4395: Add service details page (#195)
036a85db is described below
commit 036a85dbfe9c4326a669cd50b597c9f0a2b86d77
Author: Fdefined <[email protected]>
AuthorDate: Fri Mar 28 17:58:40 2025 +0800
BIGTOP-4395: Add service details page (#195)
---
bigtop-manager-ui/src/api/component/index.ts | 15 +
bigtop-manager-ui/src/api/component/types.ts | 19 +
bigtop-manager-ui/src/api/service/index.ts | 2 +-
bigtop-manager-ui/src/api/service/types.ts | 5 +-
.../src/components/common/button-group/index.vue | 56 +--
.../src/components/common/button-group/types.ts | 5 +-
.../src/components/common/header-card/index.vue | 13 +-
bigtop-manager-ui/src/locales/en_US/cluster.ts | 1 +
bigtop-manager-ui/src/locales/en_US/common.ts | 16 +-
bigtop-manager-ui/src/locales/en_US/overview.ts | 9 +-
bigtop-manager-ui/src/locales/en_US/service.ts | 7 +-
bigtop-manager-ui/src/locales/zh_CN/common.ts | 16 +-
bigtop-manager-ui/src/locales/zh_CN/overview.ts | 9 +-
bigtop-manager-ui/src/locales/zh_CN/service.ts | 7 +-
.../cluster/components/host-config.vue | 2 +-
.../src/pages/cluster-manage/cluster/host.vue | 2 +-
.../cluster/service-management/components.vue | 412 +++++++++++++++++++++
.../components/capture-snapshot.vue | 124 +++++++
.../components/snapshot-management.vue | 184 +++++++++
.../cluster/service-management/configs.vue | 244 ++++++++++++
.../cluster/service-management/index.vue | 129 +++++++
.../service-management}/overview.vue | 201 +++-------
.../src/pages/cluster-manage/cluster/service.vue | 21 +-
.../src/pages/cluster-manage/hosts/index.vue | 2 +-
.../cluster-manage/hosts/install-dependencies.vue | 2 +-
.../src/pages/cluster-manage/hosts/overview.vue | 2 +-
.../pages/cluster-manage/infrastructures/index.vue | 21 +-
.../src/router/routes/modules/clusters.ts | 17 +
bigtop-manager-ui/src/store/menu/index.ts | 3 +
29 files changed, 1335 insertions(+), 211 deletions(-)
diff --git a/bigtop-manager-ui/src/api/component/index.ts
b/bigtop-manager-ui/src/api/component/index.ts
index 60cba9a8..5166ad10 100644
--- a/bigtop-manager-ui/src/api/component/index.ts
+++ b/bigtop-manager-ui/src/api/component/index.ts
@@ -16,3 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+import { get, del } from '../request-util'
+import type { ComponentParams, ComponentVO, ComponentList } from './types'
+
+export const getComponents = (params: ComponentParams) => {
+ return get<ComponentList>(`/clusters/${params.clusterId}/components`, params)
+}
+
+export const getComponent = (pathParams: { clusterId: number; id: number }) =>
{
+ return
get<ComponentVO>(`/clusters/${pathParams.clusterId}/components/${pathParams.id}`)
+}
+
+export const deleteComponent = (pathParams: { clusterId: number; id: number })
=> {
+ return
del<boolean>(`/clusters/${pathParams.clusterId}/components/${pathParams.id}`)
+}
diff --git a/bigtop-manager-ui/src/api/component/types.ts
b/bigtop-manager-ui/src/api/component/types.ts
index 86e619dc..be9c1c86 100644
--- a/bigtop-manager-ui/src/api/component/types.ts
+++ b/bigtop-manager-ui/src/api/component/types.ts
@@ -17,6 +17,25 @@
* under the License.
*/
+import type { PageVO } from '../types'
+
+export type ComponentList = PageVO<ComponentVO>
+
+export interface ComponentParams {
+ clusterId?: number
+ hostId?: number
+ hostname?: string
+ hostnames?: string[]
+ name?: string
+ orderBy?: string
+ pageNum?: number
+ pageSize?: number
+ serviceId?: number
+ serviceNames?: string[]
+ sort?: string
+ [property: string]: any
+}
+
/**
* ComponentVO
*/
diff --git a/bigtop-manager-ui/src/api/service/index.ts
b/bigtop-manager-ui/src/api/service/index.ts
index 507061e7..1e4b11cd 100644
--- a/bigtop-manager-ui/src/api/service/index.ts
+++ b/bigtop-manager-ui/src/api/service/index.ts
@@ -42,7 +42,7 @@ export const getServiceConfigs = (pathParams: ServiceParams)
=> {
return
get<ServiceConfig[]>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/configs`)
}
-export const updateServiceConfigs = (pathParams: ServiceParams, data:
ServiceConfig) => {
+export const updateServiceConfigs = (pathParams: ServiceParams, data:
ServiceConfig[]) => {
return
post<ServiceConfig[]>(`/clusters/${pathParams.clusterId}/services/${pathParams.id}/configs`,
data)
}
diff --git a/bigtop-manager-ui/src/api/service/types.ts
b/bigtop-manager-ui/src/api/service/types.ts
index 7d81c6aa..f244e206 100644
--- a/bigtop-manager-ui/src/api/service/types.ts
+++ b/bigtop-manager-ui/src/api/service/types.ts
@@ -23,7 +23,7 @@ import type { PageVO } from '@/api/types'
export type ServiceList = PageVO<ServiceVO>
export type ServiceParams = { clusterId: number; id: number }
export type SnapshotData = { desc?: string; name?: string }
-export type SnapshotRecovery = ServiceParams & { snapshotId: string }
+export type SnapshotRecovery = ServiceParams & { snapshotId: number }
export type ServiceStatusType = 1 | 2 | 3
export type ServiceListParams = {
name?: string
@@ -45,6 +45,8 @@ export interface ServiceVO {
user?: string
version?: string
isInstalled?: boolean
+ metrics?: boolean
+ kerberos?: boolean
}
export interface ServiceConfig {
@@ -59,6 +61,7 @@ export interface Property {
displayName?: string
name: string
value?: string
+ [property: string]: any
}
export interface Attr {
diff --git a/bigtop-manager-ui/src/components/common/button-group/index.vue
b/bigtop-manager-ui/src/components/common/button-group/index.vue
index b2dd417f..43ee306a 100644
--- a/bigtop-manager-ui/src/components/common/button-group/index.vue
+++ b/bigtop-manager-ui/src/components/common/button-group/index.vue
@@ -29,7 +29,7 @@
groupType: 'text',
space: 8,
groups: () => {
- return []
+ return [] as any[]
}
})
@@ -52,13 +52,38 @@
<template>
<a-space :size="space" :wrap="true" :class="{ 'text-compact':
$props.textCompact, 'btn-auto': $props.auto }">
<template v-for="(item, _index) in groups" :key="_index">
- <a-dropdown v-if="item.dropdownMenu">
+ <template v-if="!item.hidden">
+ <a-dropdown v-if="item.dropdownMenu">
+ <a-button
+ :disabled="item.disabled"
+ :shape="item.shape || groupShape || 'default'"
+ :type="item.type || groupType || 'default'"
+ :title="checkTitle(item)"
+ @click="item.clickEvent ? item.clickEvent(item) : () => {}"
+ >
+ <template #icon>
+ <slot name="icon" :item="item" />
+ </template>
+ <span v-if="item.text">
+ {{ $props.i18n && item.text ? $t(`${$props.i18n}.${item.text}`)
: item.text }}
+ </span>
+ </a-button>
+ <template #overlay>
+ <a-menu v-if="!item.disabled" @click="item.dropdownMenuClickEvent">
+ <a-menu-item v-for="actionItem in item.dropdownMenu"
v-bind="actionItem" :key="actionItem.action">
+ {{ actionItem.text }}
+ </a-menu-item>
+ </a-menu>
+ </template>
+ </a-dropdown>
<a-button
+ v-else
+ :danger="item.danger"
:disabled="item.disabled"
:shape="item.shape || groupShape || 'default'"
:type="item.type || groupType || 'default'"
:title="checkTitle(item)"
- @click="item.clickEvent ? item.clickEvent(item) : () => {}"
+ @click="item.clickEvent ? item.clickEvent(item, args) : () => {}"
>
<template #icon>
<slot name="icon" :item="item" />
@@ -67,30 +92,7 @@
{{ $props.i18n && item.text ? $t(`${$props.i18n}.${item.text}`) :
item.text }}
</span>
</a-button>
- <template #overlay>
- <a-menu v-if="!item.disabled" @click="item.dropdownMenuClickEvent">
- <a-menu-item v-for="actionItem in item.dropdownMenu"
:key="actionItem.action">
- {{ actionItem.text }}
- </a-menu-item>
- </a-menu>
- </template>
- </a-dropdown>
- <a-button
- v-else
- :danger="item.danger"
- :disabled="item.disabled"
- :shape="item.shape || groupShape || 'default'"
- :type="item.type || groupType || 'default'"
- :title="checkTitle(item)"
- @click="item.clickEvent ? item.clickEvent(item, args) : () => {}"
- >
- <template #icon>
- <slot name="icon" :item="item" />
- </template>
- <span v-if="item.text">
- {{ $props.i18n && item.text ? $t(`${$props.i18n}.${item.text}`) :
item.text }}
- </span>
- </a-button>
+ </template>
</template>
</a-space>
</template>
diff --git a/bigtop-manager-ui/src/components/common/button-group/types.ts
b/bigtop-manager-ui/src/components/common/button-group/types.ts
index 7f2d17d4..4125e867 100644
--- a/bigtop-manager-ui/src/components/common/button-group/types.ts
+++ b/bigtop-manager-ui/src/components/common/button-group/types.ts
@@ -26,17 +26,18 @@ export interface DropdownMenu extends MenuItemProps {
action: string
text: string
}
-export interface GroupItem<T = string> {
+export interface GroupItem<T = any> {
icon?: string
tip?: string
text?: string
action?: T
+ hidden?: boolean
type?: BtnType
shape?: ShapeType
disabled?: boolean
danger?: boolean
dropdownMenu?: DropdownMenu[]
- clickEvent?: (item?: GroupItem, ...args: any[]) => void
+ clickEvent?: (item?: GroupItem<T>, ...args: any[]) => void
dropdownMenuClickEvent?: MenuProps['onClick']
}
diff --git a/bigtop-manager-ui/src/components/common/header-card/index.vue
b/bigtop-manager-ui/src/components/common/header-card/index.vue
index d6fd6274..9c1d88ae 100644
--- a/bigtop-manager-ui/src/components/common/header-card/index.vue
+++ b/bigtop-manager-ui/src/components/common/header-card/index.vue
@@ -60,7 +60,9 @@
<div class="header-card">
<slot>
<div class="header-card-info">
- <a-avatar v-if="showAvatar" :src="avatarSrc" shape="square" :size="54"
/>
+ <slot name="avatar">
+ <a-avatar v-if="showAvatar" :src="avatarSrc" shape="square"
:size="54" />
+ </slot>
<div class="card-info-title">
<div class="card-info-status">
<a-typography-title :level="5">{{ $props.title
}}</a-typography-title>
@@ -106,4 +108,13 @@
}
}
}
+
+ :deep(.ant-avatar) {
+ flex-shrink: 0;
+ border-radius: 4px;
+
+ img {
+ object-fit: contain !important;
+ }
+ }
</style>
diff --git a/bigtop-manager-ui/src/locales/en_US/cluster.ts
b/bigtop-manager-ui/src/locales/en_US/cluster.ts
index 05612202..ad9de32b 100644
--- a/bigtop-manager-ui/src/locales/en_US/cluster.ts
+++ b/bigtop-manager-ui/src/locales/en_US/cluster.ts
@@ -21,6 +21,7 @@ export default {
cluster_config: 'Cluster Configuration',
cluster_info: 'Cluster',
component_info: 'Stack',
+ component: 'Component',
host_config: 'Hosts',
create: 'Create',
name: 'Name',
diff --git a/bigtop-manager-ui/src/locales/en_US/common.ts
b/bigtop-manager-ui/src/locales/en_US/common.ts
index 28441444..89aeb245 100644
--- a/bigtop-manager-ui/src/locales/en_US/common.ts
+++ b/bigtop-manager-ui/src/locales/en_US/common.ts
@@ -21,8 +21,10 @@ export default {
prev: 'Previous',
next: 'Next',
done: 'Done',
+ save: 'Save',
exit: 'Exit',
action: 'Action',
+ operation: 'Operation',
status: 'Status',
healthy: 'Healthy',
unhealthy: 'Unhealthy',
@@ -32,8 +34,9 @@ export default {
edit: 'Edit',
rename: 'Rename',
delete: 'Delete',
- remove: 'Remove',
+ remove: 'Remove {0}',
bulk_remove: 'Bulk Remove',
+ batch_operation: 'Batch Operation',
disable: 'Disable',
enable: 'Enable',
submit: 'Submit',
@@ -58,6 +61,9 @@ export default {
service: 'Service',
stack: 'Stack',
component: 'Component',
+ componentname: 'Name',
+ quick_link: 'Quick Link',
+ no_link: 'No Link',
stage: 'Stage',
search: 'Search',
reset: 'Reset',
@@ -92,6 +98,7 @@ export default {
add_error: 'Please add {0}',
delete_msg: 'Are you sure you want to delete this?',
delete_msgs: 'Are you sure you want to delete these?',
+ restore_msg: 'Are you sure you want to restore this?',
delete_empty: 'Please select the records you want to delete.',
total: 'Total {0} items',
note: 'Note: Changes to the source address will take effect for all
clusters.',
@@ -108,9 +115,14 @@ export default {
restart: 'Restart {0}',
stop: 'Stop {0}',
add: 'Add {0}',
+ success_msg: 'Added successfully',
+ configs: 'Configs',
more_operations: 'More Operations',
ok: 'Ok',
yes: 'Yes',
no: 'No',
- all: 'All'
+ all: 'All',
+ disabled: 'Disabled',
+ restore: 'Restore',
+ selected: 'Selected'
}
diff --git a/bigtop-manager-ui/src/locales/en_US/overview.ts
b/bigtop-manager-ui/src/locales/en_US/overview.ts
index 6c2dd09c..4446a038 100644
--- a/bigtop-manager-ui/src/locales/en_US/overview.ts
+++ b/bigtop-manager-ui/src/locales/en_US/overview.ts
@@ -38,11 +38,16 @@ export default {
unit_component: 'Components',
component_info: 'Component Information',
host_status: 'Status',
- host_name: 'Name',
+ hostname: 'Name',
host_desc: 'Description',
component_count: 'Component Count',
os: 'OS',
ip_v4: 'IPv4',
ip_v6: 'IPv6',
- arch: 'Architecture'
+ arch: 'Architecture',
+ service_status: 'Status',
+ service_name: 'Name',
+ service_version: 'Version',
+ metrics: 'Metrics',
+ kerberos: 'Kerberos'
}
diff --git a/bigtop-manager-ui/src/locales/en_US/service.ts
b/bigtop-manager-ui/src/locales/en_US/service.ts
index 9e281799..b34603db 100644
--- a/bigtop-manager-ui/src/locales/en_US/service.ts
+++ b/bigtop-manager-ui/src/locales/en_US/service.ts
@@ -32,5 +32,10 @@ export default {
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?'
+ dependencies_msg: '{0} requires service {1}, add it also?',
+ capture_snapshot: 'Capture Snapshot',
+ snapshot_management: 'Snapshot Management',
+ snapshot_name: 'Name',
+ snapshot_description: 'Description',
+ snapshot_notes: 'Snapshot Notes'
}
diff --git a/bigtop-manager-ui/src/locales/zh_CN/common.ts
b/bigtop-manager-ui/src/locales/zh_CN/common.ts
index 3493fe8c..0fdfffec 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/common.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/common.ts
@@ -21,8 +21,10 @@ export default {
prev: '上一步',
next: '下一步',
done: '完成',
+ save: '保存',
exit: '退出',
action: '操作',
+ operation: '操作',
status: '状态',
healthy: '健康',
unhealthy: '不健康',
@@ -31,8 +33,9 @@ export default {
failed: '异常',
edit: '编辑',
delete: '删除',
- remove: '移除',
+ remove: '移除{0}',
bulk_remove: '批量删除',
+ batch_operation: '批量操作',
rename: '重命名',
disable: '停用',
enable: '使用',
@@ -58,6 +61,9 @@ export default {
service: '服务',
stack: '组件栈',
component: '组件',
+ componentname: '组件名',
+ quick_link: '快速链接',
+ no_link: '无链接',
stage: '阶段',
search: '搜索',
required: '需重启',
@@ -90,6 +96,7 @@ export default {
select_error: '请选择{0}',
add_error: '请添加{0}',
delete_msg: '确认要删除该记录吗?',
+ restore_msg: '确认要恢复该记录吗?',
delete_msgs: '确认要删除这些记录吗?',
delete_empty: '请选择需要删除的记录',
total: '共{0}条数据',
@@ -107,10 +114,15 @@ export default {
restart: '重启{0}',
stop: '停止{0}',
add: '添加{0}',
+ success_msg: '添加成功',
reset: '重置',
+ configs: '配置',
more_operations: '其他操作',
ok: '确认',
yes: '是',
no: '否',
- all: '全部'
+ all: '全部',
+ disabled: '关闭',
+ restore: '恢复',
+ selected: '选中'
}
diff --git a/bigtop-manager-ui/src/locales/zh_CN/overview.ts
b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
index c1be8803..9e6ddd83 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/overview.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
@@ -38,11 +38,16 @@ export default {
unit_component: '个组件',
component_info: '组件信息',
host_status: '主机状态',
- host_name: '主机名',
+ hostname: '主机名',
host_desc: '主机备注',
component_count: '组件数',
os: '操作系统',
ip_v4: 'IPv4',
ip_v6: 'IPv6',
- arch: '架构'
+ arch: '架构',
+ service_status: '服务状态',
+ service_name: '服务名',
+ service_version: '服务版本',
+ metrics: '指标监控',
+ kerberos: 'Kerberos'
}
diff --git a/bigtop-manager-ui/src/locales/zh_CN/service.ts
b/bigtop-manager-ui/src/locales/zh_CN/service.ts
index db6692b5..515c839e 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/service.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/service.ts
@@ -32,5 +32,10 @@ export default {
component_host_assignment: '每个组件至少分配一个主机',
service_selection: '请选择需要安装的服务',
dependencies_conflict_msg: '{0} 依赖于基础服务 {1},请先前往安装',
- dependencies_msg: '{0} 依赖于服务 {1}, 是否一起安装?'
+ dependencies_msg: '{0} 依赖于服务 {1}, 是否一起安装?',
+ capture_snapshot: '拍摄快照',
+ snapshot_management: '快照管理',
+ snapshot_name: '快照名',
+ snapshot_description: '快照描述',
+ snapshot_notes: '快照备注'
}
diff --git
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
index d0ea468e..7c0f7266 100644
---
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
+++
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/components/host-config.vue
@@ -79,7 +79,7 @@
ellipsis: true
},
{
- title: t('common.action'),
+ title: t('common.operation'),
key: 'operation',
width: '160px',
fixed: 'right'
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 2dddd13e..e0e2ba9f 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/host.vue
@@ -100,7 +100,7 @@
]
},
{
- title: t('common.action'),
+ title: t('common.operation'),
key: 'operation',
width: '160px',
fixed: 'right'
diff --git
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
new file mode 100644
index 00000000..89ed8599
--- /dev/null
+++
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components.vue
@@ -0,0 +1,412 @@
+<!--
+ ~ 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, onDeactivated, reactive, ref, shallowRef,
useAttrs } from 'vue'
+ import { storeToRefs } from 'pinia'
+ import { useI18n } from 'vue-i18n'
+ import { useRoute } from 'vue-router'
+ import { deleteComponent, getComponents } from '@/api/component'
+ import { execCommand } from '@/api/command'
+ import { useStackStore } from '@/store/stack'
+ import useBaseTable from '@/composables/use-base-table'
+ import type { GroupItem } from '@/components/common/button-group/types'
+ import { message, Modal, type TableColumnType, type TableProps } from
'ant-design-vue'
+ import type { ComponentVO } from '@/api/component/types'
+ import type { FilterConfirmProps, FilterResetProps } from
'ant-design-vue/es/table/interface'
+ import type { Command, CommandRequest } from '@/api/command/types'
+
+ type Key = string | number
+ interface TableState {
+ selectedRowKeys: Key[]
+ searchText: string
+ searchedColumn: keyof ComponentVO
+ selectedRows: ComponentVO[]
+ }
+
+ interface ServieceInfo {
+ cluster: string
+ id: number
+ service: string
+ serviceId: number
+ }
+
+ const POLLING_INTERVAL = 3000
+ const { t } = useI18n()
+ const stackStore = useStackStore()
+ const route = useRoute()
+ const attrs = useAttrs()
+ const { stacks } = storeToRefs(stackStore)
+ const searchInputRef = ref()
+ const pollingIntervalId = ref<any>(null)
+ const componentStatus = ref(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
+ const commandRequest = shallowRef<CommandRequest>({
+ command: 'Add',
+ commandLevel: 'cluster',
+ componentCommands: []
+ })
+ const state = reactive<TableState>({
+ searchText: '',
+ searchedColumn: '',
+ selectedRowKeys: [],
+ selectedRows: []
+ })
+
+ const currServiceInfo = computed(() => route.params as unknown as
ServieceInfo)
+ const componentsFromStack = computed(
+ () =>
+ new Map(
+ stacks.value.flatMap(
+ (stack) =>
+ stack.services?.flatMap((service) =>
+ service.name && service.components ? [[service.name,
service.components]] : []
+ ) ?? []
+ )
+ )
+ )
+
+ const columns = computed((): TableColumnType<ComponentVO>[] => [
+ {
+ title: t('common.componentname'),
+ dataIndex: 'displayName',
+ key: 'name',
+ ellipsis: true,
+ filterMultiple: false,
+ filters:
[...(componentsFromStack.value.get(currServiceInfo.value.service)?.values() ||
[])]?.map((v) => ({
+ text: v?.displayName || '',
+ value: v?.name || ''
+ }))
+ },
+ {
+ title: t('host.hostname'),
+ dataIndex: 'hostname',
+ key: 'hostname',
+ ellipsis: true,
+ customFilterDropdown: true
+ },
+ {
+ title: t('common.status'),
+ dataIndex: 'status',
+ key: 'status',
+ width: 160,
+ ellipsis: true,
+ filterMultiple: false,
+ filters: [
+ {
+ text: t('common.success'),
+ value: 1
+ },
+ {
+ text: t('common.failed'),
+ value: 2
+ },
+ {
+ text: t('common.unknown'),
+ value: 3
+ }
+ ]
+ },
+ {
+ title: t('common.quick_link'),
+ key: 'quickLink',
+ dataIndex: 'quickLink',
+ ellipsis: true
+ },
+ {
+ title: t('common.operation'),
+ width: 280,
+ key: 'operation',
+ fixed: 'right'
+ }
+ ])
+
+ const { loading, dataSource, paginationProps, filtersParams, onChange } =
useBaseTable<ComponentVO>({
+ columns: columns.value,
+ rows: []
+ })
+
+ const operations = shallowRef<GroupItem<keyof typeof Command>[]>([
+ {
+ text: 'start',
+ action: 'Start',
+ clickEvent: (item, args) => handleTableOperation(item!, args)
+ },
+ {
+ text: 'stop',
+ action: 'Stop',
+ clickEvent: (item, args) => handleTableOperation(item!, args)
+ },
+ {
+ text: 'restart',
+ action: 'Restart',
+ clickEvent: (item, args) => handleTableOperation(item!, args)
+ },
+ {
+ text: 'remove',
+ disabled: true,
+ danger: true,
+ clickEvent: (_, args) => handleDelete(args)
+ }
+ ])
+
+ const batchOperations = computed((): GroupItem[] => [
+ {
+ text: t('common.batch_operation'),
+ type: 'primary',
+ dropdownMenu: [
+ {
+ action: 'Start',
+ text: t('common.start', [t('common.selected')])
+ },
+ {
+ action: 'Restart',
+ text: t('common.restart', [t('common.selected')])
+ },
+ {
+ action: 'Stop',
+ text: t('common.stop', [t('common.selected')])
+ }
+ ],
+ dropdownMenuClickEvent: (info) => dropdownMenuClick &&
dropdownMenuClick(info)
+ }
+ ])
+
+ const onSelectChange = (selectedRowKeys: Key[], selectedRows: ComponentVO[])
=> {
+ state.selectedRowKeys = selectedRowKeys
+ state.selectedRows = selectedRows
+ }
+
+ const handleSearch = (selectedKeys: Key[], confirm: (param?:
FilterConfirmProps) => void, dataIndex: string) => {
+ confirm()
+ state.searchText = selectedKeys[0] as string
+ state.searchedColumn = dataIndex
+ stopPolling()
+ startPolling()
+ }
+
+ const handleReset = (clearFilters: (param?: FilterResetProps) => void) => {
+ clearFilters({ confirm: true })
+ state.searchText = ''
+ stopPolling()
+ startPolling()
+ }
+
+ const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key
}) => {
+ if (state.selectedRows.length === 0) {
+ message.error(t('common.select_error',
[`${t('common.component')}`.toLowerCase()]))
+ return
+ }
+ commandRequest.value.command = key as keyof typeof Command
+ const map = state.selectedRows.reduce((map, v) => {
+ if (!map.has(v.name)) {
+ map.set(v.name, { componentName: v.name, hostnames: [v.hostname] })
+ } else {
+ map.get(v.name).hostnames.push(v.hostname)
+ }
+ return map
+ }, new Map())
+ commandRequest.value.componentCommands = [...map.values()]
+ await execOperation()
+ getComponentList(true, true)
+ }
+
+ const handleTableOperation = async (item: GroupItem<keyof typeof Command>,
row: ComponentVO) => {
+ commandRequest.value.command = item.action!
+ commandRequest.value.componentCommands?.push({
+ componentName: row.name!,
+ hostnames: [row.hostname!]
+ })
+ await execOperation()
+ getComponentList(true, true)
+ }
+
+ const execOperation = async () => {
+ try {
+ await execCommand({ ...commandRequest.value, clusterId:
currServiceInfo.value.id })
+ state.selectedRowKeys = []
+ state.selectedRows = []
+ } catch (error) {
+ console.log('error :>> ', error)
+ }
+ }
+
+ const handleDelete = async (row: ComponentVO) => {
+ Modal.confirm({
+ title: t('common.delete_msg'),
+ async onOk() {
+ try {
+ const data = await deleteComponent({ clusterId:
currServiceInfo.value.id, id: row.id! })
+ if (data) {
+ message.success(t('common.delete_success'))
+ getComponentList(true, true)
+ }
+ } catch (error) {
+ console.log('error :>> ', error)
+ }
+ }
+ })
+ }
+
+ const getComponentList = async (isReset = false, isFirstCall = false) => {
+ const { id: clusterId, serviceId } = currServiceInfo.value
+ if (attrs.id == undefined || !paginationProps.value) {
+ loading.value = false
+ return
+ }
+ if (isReset) {
+ paginationProps.value.current = 1
+ }
+ try {
+ if (isFirstCall) {
+ loading.value = true
+ }
+ const res = await getComponents({ ...filtersParams.value, clusterId,
serviceId })
+ dataSource.value = res.content
+ paginationProps.value.total = res.total
+ loading.value = false
+ } catch (error) {
+ console.log('error :>> ', error)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const tableChange: TableProps['onChange'] = (...args) => {
+ onChange(...args)
+ stopPolling()
+ startPolling()
+ }
+
+ const startPolling = () => {
+ getComponentList(true, true)
+ pollingIntervalId.value = setInterval(() => {
+ getComponentList()
+ }, POLLING_INTERVAL)
+ }
+
+ const stopPolling = () => {
+ if (pollingIntervalId.value) {
+ clearInterval(pollingIntervalId.value)
+ pollingIntervalId.value = null
+ }
+ }
+
+ onActivated(() => {
+ startPolling()
+ })
+
+ onDeactivated(() => {
+ stopPolling()
+ })
+</script>
+
+<template>
+ <div class="component">
+ <header>
+ <div class="header-title">{{ $t('common.component') }}</div>
+ <div class="list-operation">
+ <a-button type="primary" :disabled="true">{{ $t('common.add',
[`${$t('common.component')}`]) }}</a-button>
+ <button-group :groups="batchOperations" group-shape="default" />
+ </div>
+ </header>
+ <a-table
+ row-key="id"
+ :loading="loading"
+ :data-source="dataSource"
+ :columns="columns"
+ :pagination="paginationProps"
+ :scroll="{ x: 900, y: 1000 }"
+ :row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange:
onSelectChange }"
+ @change="tableChange"
+ >
+ <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>
+ <template #customFilterIcon="{ filtered, column }">
+ <svg-icon v-if="!['name', 'status'].includes(column.key)"
:name="filtered ? 'search_activated' : 'search'" />
+ <svg-icon v-else :name="filtered ? 'filter_activated' : 'filter'" />
+ </template>
+ <template #bodyCell="{ record, column }">
+ <template v-if="['status'].includes(column.key)">
+ <svg-icon style="margin-left: 0"
:name="componentStatus[record.status].toLowerCase()" />
+ <span>{{
$t(`common.${componentStatus[record.status].toLowerCase()}`) }}</span>
+ </template>
+ <template v-if="column.key === 'quickLink'">
+ <span v-if="!record.quickLink">{{ $t('common.no_link') }}</span>
+ <a-typography-link v-else :href="record.quickLink.url"
target="_blank">
+ {{ record.quickLink.displayName }}
+ </a-typography-link>
+ </template>
+ <template v-if="column.key === 'operation'">
+ <button-group
+ i18n="common"
+ :text-compact="true"
+ :space="24"
+ :groups="operations"
+ :args="record"
+ group-shape="default"
+ group-type="link"
+ />
+ </template>
+ </template>
+ </a-table>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+ .component {
+ .list-operation {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+ header {
+ margin-bottom: $space-md;
+ }
+ .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/service-management/components/capture-snapshot.vue
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/capture-snapshot.vue
new file mode 100644
index 00000000..41fb6957
--- /dev/null
+++
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/capture-snapshot.vue
@@ -0,0 +1,124 @@
+<!--
+ ~ 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 { computed, ref, shallowRef } from 'vue'
+ import { takeServiceConfigSnapshot } from '@/api/service'
+ import type { FormItemState } from '@/components/common/auto-form/types'
+ import type { ServiceParams, SnapshotData } from '@/api/service/types'
+ import { message } from 'ant-design-vue'
+
+ const { t } = useI18n()
+ const open = ref(false)
+ const loading = ref(false)
+ const serviceInfo = shallowRef<ServiceParams>()
+ const autoFormRef = ref<Comp.AutoFormInstance | null>(null)
+ const formValue = ref<SnapshotData>({})
+ const emits = defineEmits(['success'])
+ const formItems = computed((): FormItemState[] => [
+ {
+ type: 'input',
+ field: 'name',
+ formItemProps: {
+ name: 'name',
+ label: t('service.snapshot_name'),
+ rules: [
+ {
+ required: true,
+ message: t('common.enter_error',
[`${t('service.snapshot_name')}`.toLowerCase()]),
+ trigger: 'blur'
+ }
+ ]
+ },
+ controlProps: {
+ placeholder: t('common.enter_error',
[`${t('service.snapshot_name')}`.toLowerCase()])
+ }
+ },
+ {
+ type: 'textarea',
+ field: 'desc',
+ formItemProps: {
+ name: 'desc',
+ label: t('service.snapshot_description'),
+ rules: [
+ {
+ required: true,
+ message: t('common.enter_error',
[`${t('service.snapshot_description')}`.toLowerCase()]),
+ trigger: 'blur'
+ }
+ ]
+ },
+ controlProps: {
+ placeholder: t('common.enter_error',
[`${t('service.snapshot_description')}`.toLowerCase()])
+ }
+ }
+ ])
+
+ const handleOpen = (data: ServiceParams) => {
+ serviceInfo.value = data
+ open.value = true
+ }
+
+ const handleOk = async () => {
+ const validate = await autoFormRef.value?.getFormValidation()
+ if (!validate || !serviceInfo.value) return
+ loading.value = true
+ try {
+ await takeServiceConfigSnapshot(serviceInfo.value, formValue.value)
+ message.success(t('common.success_msg'))
+ emits('success')
+ handleCancel()
+ } catch (e) {
+ console.log(e)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const handleCancel = () => {
+ formValue.value = {}
+ open.value = false
+ }
+
+ defineExpose({
+ handleOpen
+ })
+</script>
+
+<template>
+ <div class="set-source">
+ <a-modal
+ v-model:open="open"
+ width="50%"
+ :centered="true"
+ :mask="false"
+ :title="$t('service.capture_snapshot')"
+ :mask-closable="false"
+ :destroy-on-close="true"
+ :confirm-loading="loading"
+ @ok="handleOk"
+ @cancel="handleCancel"
+ >
+ <auto-form ref="autoFormRef" v-model:form-value="formValue"
:show-button="false" :form-items="formItems" />
+ </a-modal>
+ </div>
+</template>
+
+<style lang="scss" scoped></style>
diff --git
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
new file mode 100644
index 00000000..3e220a38
--- /dev/null
+++
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/components/snapshot-management.vue
@@ -0,0 +1,184 @@
+<!--
+ ~ 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, ref, shallowRef } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { message, Modal, TableColumnType } from 'ant-design-vue'
+ import useBaseTable from '@/composables/use-base-table'
+ import {
+ deleteServiceConfigSnapshot,
+ getServiceConfigSnapshotsList,
+ recoveryServiceConfigSnapshot
+ } from '@/api/service'
+ import type { GroupItem } from '@/components/common/button-group/types'
+ import type { ServiceConfigSnapshot, ServiceParams, SnapshotRecovery } from
'@/api/service/types'
+
+ type OperationType = 'Restore' | 'Remove'
+
+ const { t } = useI18n()
+ const open = ref(false)
+ const serviceInfo = shallowRef<ServiceParams>()
+
+ const columns = computed((): TableColumnType[] => [
+ {
+ title: t('service.snapshot_name'),
+ dataIndex: 'name',
+ key: 'name',
+ ellipsis: true
+ },
+ {
+ title: t('service.snapshot_notes'),
+ dataIndex: 'desc',
+ key: 'desc',
+ ellipsis: true
+ },
+ {
+ title: t('common.create_time'),
+ dataIndex: 'createTime',
+ key: 'createTime',
+ ellipsis: true
+ },
+ {
+ title: t('common.operation'),
+ dataIndex: 'operation',
+ width: '160px',
+ key: 'operation',
+ ellipsis: true
+ }
+ ])
+
+ const { loading, dataSource, resetState } =
useBaseTable<ServiceConfigSnapshot>({
+ columns: columns.value,
+ rows: [],
+ pagination: false
+ })
+
+ const operations = computed((): GroupItem<OperationType>[] => [
+ {
+ text: 'restore',
+ action: 'Restore',
+ clickEvent: (item, args) => handleTableOperation(item!, args)
+ },
+ {
+ text: 'remove',
+ action: 'Remove',
+ danger: true,
+ clickEvent: (item, args) => handleTableOperation(item!, args)
+ }
+ ])
+
+ const operationMap = computed(() => ({
+ Restore: {
+ modalTitle: t('common.restore_msg'),
+ api: recoveryServiceConfigSnapshot,
+ successMsg: t('common.update_success')
+ },
+ Remove: {
+ modalTitle: t('common.delete_msg'),
+ api: deleteServiceConfigSnapshot,
+ successMsg: t('common.delete_success')
+ }
+ }))
+
+ const handleTableOperation = (item: GroupItem<OperationType>, payLoad:
ServiceConfigSnapshot) => {
+ const currOperation = operationMap.value[item.action!]
+ Modal.confirm({
+ title: currOperation.modalTitle,
+ async onOk() {
+ try {
+ const data = await currOperation.api({ ...serviceInfo.value,
snapshotId: payLoad.id } as SnapshotRecovery)
+ if (data) {
+ message.success(currOperation.successMsg)
+ resetState()
+ await getSnapshotList()
+ }
+ } catch (error) {
+ console.log('error :>> ', error)
+ }
+ }
+ })
+ }
+
+ const handleOpen = (data: ServiceParams) => {
+ serviceInfo.value = data
+ open.value = true
+ getSnapshotList()
+ }
+
+ const getSnapshotList = async () => {
+ if (!serviceInfo.value) {
+ return
+ }
+ try {
+ loading.value = true
+ dataSource.value = await getServiceConfigSnapshotsList(serviceInfo.value)
+ } catch (error) {
+ console.log('error :>> ', error)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const handleOk = () => {
+ handleCancel()
+ }
+
+ const handleCancel = () => {
+ open.value = false
+ }
+
+ defineExpose({
+ handleOpen
+ })
+</script>
+
+<template>
+ <div>
+ <a-modal
+ v-model:open="open"
+ width="800px"
+ :centered="true"
+ :mask="false"
+ :title="$t('service.snapshot_management')"
+ :mask-closable="false"
+ :destroy-on-close="true"
+ @ok="handleOk"
+ @cancel="handleCancel"
+ >
+ <a-table :loading="loading" :scroll="{ y: 340 }"
:data-source="dataSource" :columns="columns">
+ <template #bodyCell="{ record, column }">
+ <template v-if="column.key === 'operation'">
+ <button-group
+ i18n="common"
+ :text-compact="true"
+ :space="24"
+ :groups="operations"
+ :args="record"
+ group-shape="default"
+ group-type="link"
+ />
+ </template>
+ </template>
+ </a-table>
+ </a-modal>
+ </div>
+</template>
+
+<style lang="scss" scoped></style>
diff --git
a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/configs.vue
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/configs.vue
new file mode 100644
index 00000000..66c9b094
--- /dev/null
+++
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/configs.vue
@@ -0,0 +1,244 @@
+<!--
+ ~ 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, inject, useAttrs, shallowRef, ref, onDeactivated,
ComputedRef } from 'vue'
+ import { debounce } from 'lodash'
+ import { useI18n } from 'vue-i18n'
+ import { Empty, message } from 'ant-design-vue'
+ import { updateServiceConfigs } from '@/api/service'
+ import CaptureSnapshot from './components/capture-snapshot.vue'
+ import SnapshotManagement from './components/snapshot-management.vue'
+ import type { ServiceConfig, ServiceVO } from '@/api/service/types'
+
+ interface ServieceInfo {
+ cluster: string
+ id: number
+ service: string
+ serviceId: number
+ }
+
+ const { routeParams }: { routeParams: ComputedRef<ServieceInfo> } =
inject('service') as any
+ const attrs = useAttrs() as unknown as ServiceVO
+ const { t } = useI18n()
+ const getServiceDetail = inject('getServiceDetail') as () => any
+ const searchStr = ref('')
+ const loading = ref(false)
+ const activeKey = ref<number[]>([])
+ const configs = ref<ServiceConfig[]>([])
+ const filterConfigs = ref<ServiceConfig[]>([])
+ const captureRef = ref<InstanceType<typeof CaptureSnapshot>>()
+ const snapshotRef = ref<InstanceType<typeof SnapshotManagement>>()
+ const debouncedOnSearch = ref()
+ 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 }
+ }
+ })
+
+ const createNewConfigItem = () => {
+ return {
+ name: '',
+ displayName: '',
+ value: '',
+ isManual: true
+ }
+ }
+
+ const manualAddConfig = (config: ServiceConfig) => {
+ config.properties?.push(createNewConfigItem())
+ }
+
+ 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 saveConfigs = async () => {
+ try {
+ const { serviceId, id: clusterId } = routeParams.value
+ loading.value = true
+ const data = await updateServiceConfigs({ id: serviceId, clusterId },
[...configs.value])
+ if (data) {
+ message.success(t('common.update_success'))
+ getServiceDetail()
+ }
+ } catch (error) {
+ console.log('error :>> ', error)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const onCaptureSnapshot = () => {
+ const { serviceId, id: clusterId } = routeParams.value
+ captureRef.value?.handleOpen({ id: serviceId, clusterId })
+ }
+
+ const openSnapshotManagement = () => {
+ const { serviceId, id: clusterId } = routeParams.value
+ snapshotRef.value?.handleOpen({ id: serviceId, clusterId })
+ }
+
+ onActivated(() => {
+ configs.value = attrs.configs as ServiceConfig[]
+ filterConfigs.value = [...configs.value]
+ activeKey.value = filterConfigs.value.map((v) => v.id!)
+ debouncedOnSearch.value = debounce(filterConfigurations, 300)
+ })
+
+ onDeactivated(() => {
+ debouncedOnSearch.value.cancel()
+ })
+</script>
+
+<template>
+ <div class="service-configurator">
+ <header>
+ <div class="header-title">{{ $t('common.configs') }}</div>
+ <div class="list-operation">
+ <a-space>
+ <a-button type="primary" @click="onCaptureSnapshot">{{
$t('service.capture_snapshot') }}</a-button>
+ <a-button @click="openSnapshotManagement">{{
$t('service.snapshot_management') }}</a-button>
+ </a-space>
+ <a-input
+ v-model:value="searchStr"
+ :placeholder="$t('service.please_enter_search_keyword')"
+ @input="debouncedOnSearch"
+ />
+ </div>
+ </header>
+ <section>
+ <a-empty v-if="filterConfigs.length === 0"
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
+ <a-form v-else :label-wrap="true" :disabled="loading">
+ <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>
+ </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>
+ <div style="text-align: end">
+ <a-button type="primary" :loading="loading" @click="saveConfigs">
+ {{ $t('common.save') }}
+ </a-button>
+ </div>
+ <capture-snapshot ref="captureRef" @success="getServiceDetail" />
+ <snapshot-management ref="snapshotRef" />
+ </div>
+</template>
+
+<style lang="scss" scoped>
+ .highlight {
+ background-color: rgb(255, 192, 105);
+ padding: 0px;
+ }
+
+ .service-configurator {
+ display: grid;
+ gap: 16px;
+ .list-operation {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ .ant-input {
+ flex: 0 1 160px;
+ }
+ }
+ :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/pages/cluster-manage/cluster/service-management/index.vue
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
new file mode 100644
index 00000000..92491d5f
--- /dev/null
+++
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/index.vue
@@ -0,0 +1,129 @@
+<!--
+ ~ 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, onMounted, provide, ref, shallowRef } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { storeToRefs } from 'pinia'
+ // import { execCommand } from '@/api/command'
+ // import { Command } from '@/api/command/types'
+ // import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+ import { useServiceStore } from '@/store/service'
+ import { useRoute } from 'vue-router'
+ import Overview from './overview.vue'
+ import Components from './components.vue'
+ import Configs from './configs.vue'
+ import type { TabItem } from '@/components/common/main-card/types'
+ import type { GroupItem } from '@/components/common/button-group/types'
+ import type { ServiceVO } from '@/api/service/types'
+
+ interface ServieceInfo {
+ cluster: string
+ id: number
+ service: string
+ serviceId: number
+ }
+
+ const { t } = useI18n()
+ const route = useRoute()
+ const serviceStore = useServiceStore()
+ const { loading } = storeToRefs(serviceStore)
+ const activeKey = ref('1')
+ const serviceDetail = shallowRef<ServiceVO>()
+ const routeParams = computed(() => route.params as unknown as ServieceInfo)
+ const tabs = computed((): TabItem[] => [
+ {
+ key: '1',
+ title: t('common.overview')
+ },
+ {
+ key: '2',
+ title: t('common.component')
+ },
+ {
+ key: '3',
+ title: t('common.configs')
+ }
+ ])
+ const actionGroup = computed<GroupItem[]>(() => [
+ {
+ shape: 'default',
+ type: 'primary',
+ text: t('common.operation'),
+ dropdownMenu: [
+ {
+ 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 getCompName = computed(() => {
+ const components = [Overview, Components, Configs]
+ return components[parseInt(activeKey.value) - 1]
+ })
+
+ const getServiceDetail = async () => {
+ try {
+ loading.value = true
+ const { id: clusterId, serviceId } = routeParams.value
+ serviceDetail.value = await serviceStore.getServiceDetail(clusterId,
serviceId)
+ } catch (error) {
+ console.log('error :>> ', error)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ provide('getServiceDetail', getServiceDetail)
+ provide('service', { routeParams })
+
+ onMounted(() => {
+ getServiceDetail()
+ })
+</script>
+
+<template>
+ <a-spin :spinning="loading">
+ <header-card
+ :title="serviceDetail?.displayName || serviceDetail?.name"
+ :avatar="serviceDetail?.name"
+ :desc="serviceDetail?.desc"
+ :action-groups="actionGroup"
+ />
+ <main-card v-model:active-key="activeKey" :tabs="tabs">
+ <template #tab-item>
+ <keep-alive>
+ <component :is="getCompName" v-bind="serviceDetail"></component>
+ </keep-alive>
+ </template>
+ </main-card>
+ </a-spin>
+</template>
+
+<style lang="scss" scoped></style>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/overview.vue
similarity index 53%
copy from bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
copy to
bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/overview.vue
index 689847ae..ee322efc 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++
b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service-management/overview.vue
@@ -18,18 +18,12 @@
-->
<script setup lang="ts">
- import { computed, ref, shallowRef, toRefs, watch } from 'vue'
+ import { computed, ref, shallowRef, useAttrs } from 'vue'
import { useI18n } from 'vue-i18n'
- import { Empty, MenuProps } from 'ant-design-vue'
- import { formatFromByte } from '@/utils/storage.ts'
- import { usePngImage } from '@/utils/tools'
- import { CommonStatus, CommonStatusTexts } from '@/enums/state.ts'
- import CategoryChart from
'@/pages/cluster-manage/cluster/components/category-chart.vue'
- import GaugeChart from
'@/pages/cluster-manage/cluster/components/gauge-chart.vue'
- import { getComponentsByHost } from '@/api/hosts'
- 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'
+ import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+ import GaugeChart from '../components/gauge-chart.vue'
+ import CategoryChart from '../components/category-chart.vue'
+ import type { ServiceVO, ServiceStatusType } from '@/api/service/types'
type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
type TimeRangeItem = {
@@ -37,28 +31,22 @@
time: string
}
- interface Props {
- hostInfo: HostVO
- }
-
- const props = defineProps<Props>()
- const { hostInfo } = toRefs(props)
const { t } = useI18n()
- const currExecuteComponent = shallowRef<ComponentVO>({})
+ const attrs = useAttrs()
const currTimeRange = ref<TimeRangeText>('15m')
- const statusColors = shallowRef<Record<HostStatusType, keyof typeof
CommonStatusTexts>>({
- 1: 'healthy',
- 2: 'unhealthy',
- 3: 'unknown'
- })
const chartData = ref({
chart1: [],
chart2: [],
chart3: [],
chart4: []
})
- const componentsFromCurrentHost = shallowRef<Map<string, ComponentVO[]>>(new
Map())
- const needFormatFormByte = computed(() => ['totalMemorySize', 'totalDisk'])
+ const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof
CommonStatusTexts>>({
+ 1: 'healthy',
+ 2: 'unhealthy',
+ 3: 'unknown'
+ })
+ const serviceDetail = computed(() => attrs as unknown as ServiceVO)
+ const serviceKeys = computed(() => Object.keys(baseConfig.value) as (keyof
ServiceVO)[])
const noChartData = computed(() => Object.values(chartData.value).every((v)
=> v.length === 0))
const timeRanges = computed((): TimeRangeItem[] => [
{
@@ -86,77 +74,21 @@
time: ''
}
])
- const baseConfig = computed((): Partial<Record<keyof HostVO, string>> => {
+ const baseConfig = computed((): Partial<Record<keyof ServiceVO, string>> => {
return {
- status: t('overview.host_status'),
- hostname: t('overview.host_name'),
- desc: t('overview.host_desc'),
- clusterDisplayName: t('common.cluster'),
- componentNum: t('overview.component_count'),
- os: t('overview.os'),
- ipv4: t('overview.ip_v4'),
- ipv6: t('overview.ip_v6'),
- arch: t('overview.arch'),
- availableProcessors: t('overview.core_count'),
- totalMemorySize: t('overview.memory'),
- totalDisk: t('overview.disk_size')
+ status: t('overview.service_status'),
+ displayName: t('overview.service_name'),
+ version: t('overview.service_version'),
+ stack: t('common.stack'),
+ restartFlag: t('service.required_restart'),
+ metrics: t('overview.metrics'),
+ kerberos: t('overview.kerberos')
}
})
- const unitOfBaseConfig = computed(
- (): Partial<Record<keyof HostVO, string>> => ({
- componentNum: t('overview.unit_component'),
- availableProcessors: t('overview.unit_core')
- })
- )
- const detailKeys = computed((): (keyof HostVO)[] =>
Object.keys(baseConfig.value))
- const componentOperates = computed(() => [
- {
- action: 'start',
- text: t('common.start', [t('common.component')])
- },
- {
- action: 'restart',
- text: t('common.restart', [t('common.component')])
- },
- {
- action: 'stop',
- text: t('common.stop', [t('common.component')])
- }
- ])
-
- const handleHostOperate: MenuProps['onClick'] = (item) => {
- console.log('item :>> ', item.key)
- console.log(currExecuteComponent.value)
- }
const handleTimeRange = (time: TimeRangeItem) => {
currTimeRange.value = time.text
}
-
- const recordClickComp = (comp: ComponentVO) => {
- currExecuteComponent.value = comp
- }
-
- watch(
- () => hostInfo.value,
- async (val) => {
- if (val.id) {
- try {
- const data = await getComponentsByHost({ id: val.id })
- componentsFromCurrentHost.value = data.reduce((pre, val) => {
- if (!pre.has(val.stack!)) {
- pre.set(val.stack!, [val])
- } else {
- ;(pre.get(val.stack!) as ComponentVO[]).push(val)
- }
- return pre
- }, new Map<string, ComponentVO[]>())
- } catch (error) {
- console.log(error)
- }
- }
- }
- )
</script>
<template>
@@ -177,35 +109,37 @@
</template>
<div class="desc-sub-item-wrp">
<div class="desc-sub-item">
- <template v-for="base in detailKeys" :key="base">
+ <template v-for="key in serviceKeys" :key="key">
<div class="desc-sub-item-desc">
<a-typography-text
class="desc-sub-item-desc-column"
type="secondary"
- :content="baseConfig[base]"
+ :content="baseConfig[`${key}`]"
/>
<a-tag
- v-if="base === 'status'"
+ v-if="key === 'status'"
class="reset-tag"
- :color="CommonStatus[statusColors[hostInfo[base] as
ClusterStatusType]]"
+
:color="CommonStatus[statusColors[serviceDetail[key]]]"
>
- <status-dot
:color="CommonStatus[statusColors[hostInfo[base] as ClusterStatusType]]" />
- {{ hostInfo[base] &&
$t(`common.${statusColors[hostInfo[base] as ClusterStatusType]}`) }}
+ <status-dot
:color="CommonStatus[statusColors[serviceDetail[key]]]" />
+ {{ serviceDetail[key] &&
$t(`common.${statusColors[serviceDetail[key]]}`) }}
</a-tag>
- <a-typography-text v-else
class="desc-sub-item-desc-column">
- <span
v-if="Object.keys(unitOfBaseConfig).includes(base as string)">
- {{ `${hostInfo[base]} ${unitOfBaseConfig[base]}` }}
- </span>
- <span v-else>
- {{
- `${
- needFormatFormByte.includes(base as string)
- ? formatFromByte(hostInfo[base])
- : hostInfo[base] || '--'
- }`
- }}
- </span>
- </a-typography-text>
+ <a-typography-text
+ v-else-if="key === 'stack'"
+ class="desc-sub-item-desc-column"
+ :content="serviceDetail[key]?.toLowerCase()"
+ />
+ <a-typography-text
+ v-else-if="key === 'restartFlag'"
+ class="desc-sub-item-desc-column"
+ :content="serviceDetail[key] ? $t('common.yes') :
$t('common.no')"
+ />
+ <a-typography-text
+ v-else-if="['kerberos', 'metrics'].includes(key)"
+ class="desc-sub-item-desc-column"
+ :content="$t('common.disabled')"
+ />
+ <a-typography-text v-else
class="desc-sub-item-desc-column" :content="serviceDetail[key]" />
</div>
</template>
</div>
@@ -214,46 +148,6 @@
</a-descriptions>
</div>
</div>
-
- <template v-if="componentsFromCurrentHost.size === 0">
- <div class="component-info">
- <div class="box-title">
- <a-typography-text strong
:content="$t('overview.component_info')" />
- </div>
- <div class="box-empty">
- <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
- </div>
- </div>
- </template>
- <a-descriptions v-else layout="vertical" bordered :column="1">
- <template #title>
- <a-typography-text strong :content="$t('overview.component_info')"
/>
- </template>
- <a-descriptions-item v-for="stack in
componentsFromCurrentHost.keys()" :key="stack">
- <template #label>
- <div class="desc-sub-label">
- <a-typography-text strong :content="stack.split('-')[0] + '
Stack'" />
- <a-typography-text type="secondary"
:content="`${stack.toLowerCase()}`" />
- </div>
- </template>
- <div v-for="comp in componentsFromCurrentHost.get(stack)"
:key="comp.id" class="component-item">
- <a-avatar v-if="comp.serviceName"
:src="usePngImage(comp.serviceName.toLowerCase())" :size="22" />
- <a-typography-text
:content="`${comp.serviceDisplayName}/${comp.displayName}`" />
- <a-dropdown :trigger="['click']">
- <a-button type="text" shape="circle" size="small"
@click="recordClickComp(comp)">
- <svg-icon name="more" style="margin: 0" />
- </a-button>
- <template #overlay>
- <a-menu @click="handleHostOperate">
- <a-menu-item v-for="operate in componentOperates"
:key="operate.action">
- <span>{{ operate.text }}</span>
- </a-menu-item>
- </a-menu>
- </template>
- </a-dropdown>
- </div>
- </a-descriptions-item>
- </a-descriptions>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="14" :xl="17">
<div class="box-title">
@@ -352,15 +246,6 @@
}
}
- .component-item {
- display: grid;
- grid-template-columns: auto 1fr auto;
- gap: $space-md;
- align-items: center;
- padding: 12px 16px;
- border-top: 1px solid #f0f0f0;
- }
-
.chart-item-wrp {
border: 1px solid $color-border;
margin-right: -1px;
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 8e0c606e..0f513c3e 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/service.vue
@@ -24,14 +24,16 @@
import { useI18n } from 'vue-i18n'
import { useServiceStore } from '@/store/service'
import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+ import { useRouter } from 'vue-router'
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 } from
'@/api/service/types'
+ import type { ServiceListParams, ServiceStatusType, ServiceVO } from
'@/api/service/types'
import type { ClusterVO } from '@/api/cluster/types'
const { t } = useI18n()
const attrs = useAttrs() as ClusterVO
+ const router = useRouter()
const serviceStore = useServiceStore()
const { services, loading } = toRefs(serviceStore)
const statusColors = shallowRef<Record<ServiceStatusType, keyof typeof
CommonStatusTexts>>({
@@ -117,6 +119,13 @@
attrs.id != undefined && serviceStore.getServices(attrs.id, filters)
}
+ const viewServiceDetail = (payload: ServiceVO) => {
+ router.push({
+ name: 'ServiceDetail',
+ params: { service: payload.name, serviceId: payload.id }
+ })
+ }
+
onActivated(() => {
getServices()
})
@@ -127,7 +136,13 @@
<filter-form :filter-items="filterFormItems" @filter="getServices" />
<a-empty v-if="services.length == 0" style="width: 100%"
:image="Empty.PRESENTED_IMAGE_SIMPLE" />
<div v-else class="service-item-wrp">
- <a-card v-for="item in services" :key="item.id" :hoverable="true"
class="service-item">
+ <a-card
+ v-for="item in services"
+ :key="item.id"
+ :hoverable="true"
+ class="service-item"
+ @click="viewServiceDetail(item)"
+ >
<div class="header">
<div class="header-base-wrp">
<a-avatar v-if="item.name"
:src="usePngImage(item.name.toLowerCase())" :size="42" class="header-icon" />
@@ -150,7 +165,7 @@
<span class="small">{{ `${item.restartFlag ? $t('common.required')
: $t('common.not_required')}` }}</span>
</div>
</div>
- <div class="item-content">
+ <div class="item-content" @click.stop>
<button-group :auto="true" :space="0" :groups="actionGroups">
<template #icon="{ item: groupItem }">
<svg-icon :name="groupItem.icon || ''" />
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
index caf4eda8..1b2269a4 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
@@ -126,7 +126,7 @@
]
},
{
- title: t('common.action'),
+ title: t('common.operation'),
key: 'operation',
width: '160px',
fixed: 'right'
diff --git
a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
index a55797ac..7b161954 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
@@ -99,7 +99,7 @@
]
},
{
- title: t('common.action'),
+ title: t('common.operation'),
width: '180px',
key: 'operation',
fixed: 'right'
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 689847ae..e1c2f6be 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -89,7 +89,7 @@
const baseConfig = computed((): Partial<Record<keyof HostVO, string>> => {
return {
status: t('overview.host_status'),
- hostname: t('overview.host_name'),
+ hostname: t('overview.hostname'),
desc: t('overview.host_desc'),
clusterDisplayName: t('common.cluster'),
componentNum: t('overview.component_count'),
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 915374f4..1b5c0f75 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/infrastructures/index.vue
@@ -23,13 +23,15 @@
import { useI18n } from 'vue-i18n'
import { CommonStatus, CommonStatusTexts } from '@/enums/state'
import { useServiceStore } from '@/store/service'
- import type { ServiceListParams, ServiceStatusType } from
'@/api/service/types'
+ import { useRouter } from 'vue-router'
+ import type { ServiceListParams, ServiceStatusType, ServiceVO } from
'@/api/service/types'
import type { GroupItem } from '@/components/common/button-group/types'
import type { FilterFormItem } from '@/components/common/filter-form/types'
// import { execCommand } from '@/api/command';
import type { Command } from '@/api/command/types'
const { t } = useI18n()
+ const router = useRouter()
const serviceStore = useServiceStore()
const { services, loading } = toRefs(serviceStore)
@@ -109,6 +111,13 @@
await serviceStore.getServices(0, filters)
}
+ const viewServiceDetail = (payload: ServiceVO) => {
+ router.push({
+ name: 'InfraServiceDetail',
+ params: { id: 0, service: payload.name, serviceId: payload.id }
+ })
+ }
+
onMounted(async () => {
await getServices()
})
@@ -134,7 +143,13 @@
<filter-form :filter-items="filterFormItems" @filter="getServices" />
<a-empty v-if="services.length == 0" style="width: 100%" />
<template v-else>
- <a-card v-for="item in services" :key="item.name" :hoverable="true"
class="service-item">
+ <a-card
+ v-for="item in services"
+ :key="item.name"
+ :hoverable="true"
+ class="service-item"
+ @click="viewServiceDetail(item)"
+ >
<div class="header">
<div class="header-base-wrp">
<a-avatar v-if="item.name"
:src="usePngImage(item.name.toLowerCase())" :size="42" class="header-icon" />
@@ -157,7 +172,7 @@
<span class="small">{{ `${item.restartFlag ?
$t('common.required') : $t('common.not_required')}` }}</span>
</div>
</div>
- <div class="item-content">
+ <div class="item-content" @click.stop>
<button-group :auto="true" :space="0" :args="item"
:groups="actionGroups">
<template #icon="{ item: groupItem }">
<svg-icon :name="groupItem.icon || ''" />
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index 031bd82b..d226b313 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -70,6 +70,14 @@ const routes: RouteRecordRaw[] = [
meta: {
hidden: true
}
+ },
+ {
+ name: 'ServiceDetail',
+ path:
`${RouteExceptions.DYNAMIC_ROUTE_MATCH}/service-detail/:service/:serviceId`,
+ component: () =>
import('@/pages/cluster-manage/cluster/service-management/index.vue'),
+ meta: {
+ hidden: true
+ }
}
]
},
@@ -99,6 +107,15 @@ const routes: RouteRecordRaw[] = [
hidden: true,
activeMenu: '/cluster-manage/infrastructures'
}
+ },
+ {
+ name: 'InfraServiceDetail',
+ path:
'create-infra-service/service-detail/:id/:service/:serviceId',
+ component: () =>
import('@/pages/cluster-manage/cluster/service-management/index.vue'),
+ meta: {
+ hidden: true,
+ activeMenu: '/cluster-manage/infrastructures'
+ }
}
]
},
diff --git a/bigtop-manager-ui/src/store/menu/index.ts
b/bigtop-manager-ui/src/store/menu/index.ts
index eef4e6f7..6a31508d 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -85,6 +85,9 @@ export const useMenuStore = defineStore(
if (route.name === 'CreateService') {
activeMenu = route.path.split('/').slice(0, -2).join('/')
}
+ if (route.name === 'ServiceDetail') {
+ activeMenu = route.path.split('/').slice(0, -3).join('/')
+ }
const matchedNames = [RouteExceptions.SPECIAL_ROUTE_NAME,
RouteExceptions.DEFAULT_ROUTE_NAME] as string[]
headerSelectedKey.value = route.matched[0].path