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 c034eb78 BIGTOP-4394: Add host page (#193)
c034eb78 is described below
commit c034eb7830de1acc87fea1651e9d97e30cfef037
Author: Fdefined <[email protected]>
AuthorDate: Wed Mar 26 22:38:40 2025 +0800
BIGTOP-4394: Add host page (#193)
---
bigtop-manager-ui/src/api/hosts/index.ts | 30 ++
bigtop-manager-ui/src/api/hosts/types.ts | 2 +
bigtop-manager-ui/src/locales/en_US/common.ts | 6 +-
bigtop-manager-ui/src/locales/en_US/host.ts | 1 +
bigtop-manager-ui/src/locales/en_US/overview.ts | 14 +-
bigtop-manager-ui/src/locales/zh_CN/common.ts | 6 +-
bigtop-manager-ui/src/locales/zh_CN/host.ts | 1 +
bigtop-manager-ui/src/locales/zh_CN/overview.ts | 12 +-
.../cluster/components/host-config.vue | 2 +-
.../src/pages/cluster-manage/cluster/overview.vue | 14 +-
.../src/pages/cluster-manage/hosts/create.vue | 103 +++++-
.../src/pages/cluster-manage/hosts/detail.vue | 112 +++++++
.../src/pages/cluster-manage/hosts/index.vue | 364 ++++++++++++++++++++-
.../cluster-manage/hosts/install-dependencies.vue | 355 ++++++++++++++++++++
.../cluster-manage/{cluster => hosts}/overview.vue | 203 +++++++-----
.../src/router/routes/modules/clusters.ts | 9 +
bigtop-manager-ui/src/store/cluster/index.ts | 5 +-
bigtop-manager-ui/src/store/installed/index.ts | 1 +
bigtop-manager-ui/src/store/menu/index.ts | 35 +-
19 files changed, 1136 insertions(+), 139 deletions(-)
diff --git a/bigtop-manager-ui/src/api/hosts/index.ts
b/bigtop-manager-ui/src/api/hosts/index.ts
index 0f401364..80c7669c 100644
--- a/bigtop-manager-ui/src/api/hosts/index.ts
+++ b/bigtop-manager-ui/src/api/hosts/index.ts
@@ -19,6 +19,7 @@
import request from '@/api/request.ts'
import type { HostListParams, HostParams, HostVO, HostVOList,
InstalledStatusVO } from '@/api/hosts/types.ts'
+import type { ComponentVO } from '@/api/component/types.ts'
export const getHosts = (params?: HostListParams): Promise<HostVOList> => {
return request({
@@ -27,6 +28,12 @@ export const getHosts = (params?: HostListParams):
Promise<HostVOList> => {
params
})
}
+export const getHost = (pathParams: { id: number }): Promise<HostVO> => {
+ return request({
+ method: 'get',
+ url: `/hosts/${pathParams.id}`
+ })
+}
export const addHost = (data: HostParams): Promise<HostVO> => {
return request({
@@ -50,3 +57,26 @@ export const getInstalledStatus = ():
Promise<InstalledStatusVO[]> => {
url: '/hosts/installed-status'
})
}
+
+export const updateHost = (data: HostParams): Promise<HostVO[]> => {
+ return request({
+ method: 'put',
+ url: `/hosts/${data.id}`,
+ data
+ })
+}
+
+export const removeHost = (data: { ids: number[] }): Promise<boolean> => {
+ return request({
+ method: 'delete',
+ url: '/hosts/batch',
+ data
+ })
+}
+
+export const getComponentsByHost = (pathParams: { id: number }):
Promise<ComponentVO[]> => {
+ return request({
+ method: 'get',
+ url: `/hosts/${pathParams.id}/components`
+ })
+}
diff --git a/bigtop-manager-ui/src/api/hosts/types.ts
b/bigtop-manager-ui/src/api/hosts/types.ts
index dfbd53e6..2b3443a8 100644
--- a/bigtop-manager-ui/src/api/hosts/types.ts
+++ b/bigtop-manager-ui/src/api/hosts/types.ts
@@ -21,6 +21,8 @@ import type { PageVO } from '@/api/types'
export type HostVOList = PageVO<HostVO>
+export type HostStatusType = 1 | 2 | 3
+
/**
* HostVO
*/
diff --git a/bigtop-manager-ui/src/locales/en_US/common.ts
b/bigtop-manager-ui/src/locales/en_US/common.ts
index 61040ccf..28441444 100644
--- a/bigtop-manager-ui/src/locales/en_US/common.ts
+++ b/bigtop-manager-ui/src/locales/en_US/common.ts
@@ -57,6 +57,7 @@ export default {
host: 'Host',
service: 'Service',
stack: 'Stack',
+ component: 'Component',
stage: 'Stage',
search: 'Search',
reset: 'Reset',
@@ -90,6 +91,8 @@ export default {
select_error: 'Please select {0}',
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?',
+ 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.',
upload_file: 'Upload file',
@@ -108,5 +111,6 @@ export default {
more_operations: 'More Operations',
ok: 'Ok',
yes: 'Yes',
- no: 'No'
+ no: 'No',
+ all: 'All'
}
diff --git a/bigtop-manager-ui/src/locales/en_US/host.ts
b/bigtop-manager-ui/src/locales/en_US/host.ts
index 22ae699b..7bfd805d 100644
--- a/bigtop-manager-ui/src/locales/en_US/host.ts
+++ b/bigtop-manager-ui/src/locales/en_US/host.ts
@@ -35,6 +35,7 @@ export default {
key_auth: 'Key',
ip_address: 'IP Address',
component_count: 'Component Count',
+ component_count_quantifier: 'Components',
no_auth: 'No Authentication',
agent_path: 'Agent Path',
ssh_port: 'SSH Port',
diff --git a/bigtop-manager-ui/src/locales/en_US/overview.ts
b/bigtop-manager-ui/src/locales/en_US/overview.ts
index fde5bedc..6c2dd09c 100644
--- a/bigtop-manager-ui/src/locales/en_US/overview.ts
+++ b/bigtop-manager-ui/src/locales/en_US/overview.ts
@@ -27,12 +27,22 @@ export default {
service_count: 'Service Count',
memory: 'Memory',
chart: 'Chart',
- core_count: 'Processor Count',
+ core_count: 'Core Count',
disk_size: 'Disk Size',
creator: 'Creator',
memory_usage: 'Memory Usage',
cpu_usage: 'Cpu Usage',
unit_host: 'Hosts',
unit_service: 'Services',
- unit_processor: 'Processors'
+ unit_core: 'Cores',
+ unit_component: 'Components',
+ component_info: 'Component Information',
+ host_status: 'Status',
+ host_name: 'Name',
+ host_desc: 'Description',
+ component_count: 'Component Count',
+ os: 'OS',
+ ip_v4: 'IPv4',
+ ip_v6: 'IPv6',
+ arch: 'Architecture'
}
diff --git a/bigtop-manager-ui/src/locales/zh_CN/common.ts
b/bigtop-manager-ui/src/locales/zh_CN/common.ts
index cc7880f4..3493fe8c 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/common.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/common.ts
@@ -57,6 +57,7 @@ export default {
host: '主机',
service: '服务',
stack: '组件栈',
+ component: '组件',
stage: '阶段',
search: '搜索',
required: '需重启',
@@ -89,6 +90,8 @@ export default {
select_error: '请选择{0}',
add_error: '请添加{0}',
delete_msg: '确认要删除该记录吗?',
+ delete_msgs: '确认要删除这些记录吗?',
+ delete_empty: '请选择需要删除的记录',
total: '共{0}条数据',
note: '注: 源地址的改动对所有集群生效。',
upload_file: '上传文件',
@@ -108,5 +111,6 @@ export default {
more_operations: '其他操作',
ok: '确认',
yes: '是',
- no: '否'
+ no: '否',
+ all: '全部'
}
diff --git a/bigtop-manager-ui/src/locales/zh_CN/host.ts
b/bigtop-manager-ui/src/locales/zh_CN/host.ts
index 51b4d662..88ffefe3 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/host.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/host.ts
@@ -35,6 +35,7 @@ export default {
key_auth: '密钥',
ip_address: 'IP 地址',
component_count: '组件数',
+ component_count_quantifier: '个组件',
no_auth: '无认证',
agent_path: 'Agent路径',
ssh_port: 'SSH端口',
diff --git a/bigtop-manager-ui/src/locales/zh_CN/overview.ts
b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
index ced7f376..c1be8803 100644
--- a/bigtop-manager-ui/src/locales/zh_CN/overview.ts
+++ b/bigtop-manager-ui/src/locales/zh_CN/overview.ts
@@ -34,5 +34,15 @@ export default {
cpu_usage: 'CPU使用率',
unit_host: '个主机',
unit_service: '个服务',
- unit_processor: '个核心'
+ unit_core: '个核心',
+ unit_component: '个组件',
+ component_info: '组件信息',
+ host_status: '主机状态',
+ host_name: '主机名',
+ host_desc: '主机备注',
+ component_count: '组件数',
+ os: '操作系统',
+ ip_v4: 'IPv4',
+ ip_v6: 'IPv6',
+ arch: '架构'
}
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 a04074c3..d0ea468e 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
@@ -113,7 +113,7 @@
}
)
- const isContain = (source: string, target: string) => {
+ const isContain = (source: string, target: string): boolean => {
return source.toString().toLowerCase().includes(target.toLowerCase())
}
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 131ff884..714eb11e 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
@@ -103,7 +103,7 @@
return {
totalHost: t('overview.unit_host'),
totalService: t('overview.unit_service'),
- totalProcessor: t('overview.unit_processor')
+ totalProcessor: t('overview.unit_core')
}
})
const detailKeys = computed(() => Object.keys(baseConfig.value) as (keyof
ClusterVO)[])
@@ -222,7 +222,7 @@
</div>
</template>
<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-avatar v-if="service.name"
:src="usePngImage(service.name.toLowerCase())" :size="22" />
<a-typography-text :content="service.displayName" />
<a-dropdown :trigger="['click']">
<a-button type="text" shape="circle" size="small">
@@ -291,10 +291,12 @@
<style lang="scss" scoped>
:deep(.ant-avatar) {
border-radius: 4px;
+
img {
object-fit: contain !important;
}
}
+
.box {
&-title {
@include flexbox($justify: space-between);
@@ -329,6 +331,7 @@
&:hover {
color: $color-primary-text-hover;
}
+
&-activated {
color: $color-primary-text;
}
@@ -340,7 +343,7 @@
gap: $space-md;
align-items: center;
padding: 12px 16px;
- border-bottom: 1px solid #f0f0f0;
+ border-top: 1px solid #f0f0f0;
}
.chart-item-wrp {
@@ -367,9 +370,11 @@
:deep(.ant-descriptions-view) {
overflow: hidden;
border-color: $color-border;
+
.ant-descriptions-item-label {
padding: 0;
}
+
.ant-descriptions-item-content {
padding: 0;
}
@@ -378,6 +383,7 @@
.desc-sub-item-wrp {
@include flexbox($gap: $space-md);
padding: $space-md;
+
:deep(.ant-tag) {
width: fit-content;
@include flexbox($align: center, $gap: 4px);
@@ -388,8 +394,10 @@
display: grid;
grid-template-columns: max-content 1fr;
gap: 16px;
+
&-desc {
display: contents;
+
&-column {
text-align: left;
}
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
index 5354a8ae..a1e3d01a 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/create.vue
@@ -18,7 +18,7 @@
-->
<script setup lang="ts">
- import { computed, h, ref, watch } from 'vue'
+ import { computed, h, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { FormItemState } from '@/components/common/auto-form/types'
import { CheckCircleOutlined, UploadOutlined } from '@ant-design/icons-vue'
@@ -28,9 +28,11 @@
import { storeToRefs } from 'pinia'
import { uploadFile } from '@/api/upload-file'
import { Rule } from 'ant-design-vue/es/form'
- import { HostReq } from '@/api/command/types'
+ import { updateHost } from '@/api/hosts'
+ import { useClusterStore } from '@/store/cluster'
import type { UploadProps } from 'ant-design-vue'
- import type { HostParams } from '@/api/hosts/types'
+ import type { HostReq } from '@/api/command/types'
+ import type { HostParams, HostVO } from '@/api/hosts/types'
enum Mode {
EDIT = 'cluster.edit_host',
@@ -38,13 +40,20 @@
}
interface Emits {
- (event: 'onOk', type: keyof typeof Mode, value: HostReq): void
+ (event: 'onOk', type: keyof typeof Mode, value: HostReq | HostVO): void
}
+ const props = withDefaults(defineProps<{ isPublic?: boolean; apiEditCaller?:
boolean }>(), {
+ isPublic: false,
+ apiEditCaller: false
+ })
+
const { t } = useI18n()
const emits = defineEmits<Emits>()
const localeStore = useLocaleStore()
+ const clusterStore = useClusterStore()
const open = ref(false)
+ const loading = ref(false)
const mode = ref<keyof typeof Mode>('ADD')
const hiddenItems = ref<string[]>([])
const autoFormRef = ref<Comp.AutoFormInstance | null>(null)
@@ -193,6 +202,34 @@
}
}
])
+
+ const formItemsOfPublicHost = computed((): FormItemState[] => [
+ {
+ type: 'select',
+ field: 'clusterId',
+ defaultValue: '',
+ fieldMap: {
+ label: 'displayName',
+ value: 'clusterId'
+ },
+ formItemProps: {
+ name: 'clusterId',
+ label: t('common.cluster'),
+ rules: [
+ {
+ required: true,
+ message: t('common.select_error',
[`${t('common.cluster')}`.toLowerCase()]),
+ trigger: 'blur'
+ }
+ ]
+ },
+ controlProps: {
+ disabled: isEdit.value,
+ placeholder: t('common.select_error',
[`${t('common.cluster')}`.toLowerCase()])
+ }
+ }
+ ])
+
const formItems = computed((): FormItemState[] => [
{
type: 'input',
@@ -230,7 +267,6 @@
]
}
},
-
{
type: mode.value == 'ADD' ? 'textarea' : 'input',
field: 'hostname',
@@ -298,13 +334,13 @@
if (formValue.value.authType === '1') {
const data = [...formItems.value]
data.splice(2, 0, ...formItemsForSshPassword.value)
- return data
+ return props.isPublic ? [...formItemsOfPublicHost.value, ...data] : data
} else if (formValue.value.authType === '2') {
const data = [...formItems.value]
data.splice(2, 0, ...formItemsForSshKeyPassword.value)
- return data
+ return props.isPublic ? [...formItemsOfPublicHost.value, ...data] : data
}
- return [...formItems.value]
+ return props.isPublic ? [...formItemsOfPublicHost.value,
...formItems.value] : [...formItems.value]
})
watch(
@@ -327,13 +363,40 @@
open.value = true
mode.value = type
if (payload) {
- formValue.value = Object.assign(formValue.value, { ...payload })
+ formValue.value = Object.assign(formValue.value, {
+ ...payload,
+ authType: `${payload.authType}`
+ })
} else {
Object.assign(formValue.value, {
authType: '1',
inputType: '1'
})
}
+
+ props.isPublic && (await getClusterSelectOptions())
+ }
+
+ const getClusterSelectOptions = async () => {
+ await nextTick()
+ const formatClusters = clusterStore.clusters.map((v) => ({ ...v,
clusterId: v.id }))
+ autoFormRef.value?.setOptions('clusterId', formatClusters)
+ }
+
+ const editHost = async (hostConfig: HostReq) => {
+ try {
+ const data = await updateHost(hostConfig)
+ if (data) {
+ loading.value = false
+ message.success(t('common.update_success'))
+ emits('onOk', mode.value, formValue.value)
+ handleCancel()
+ }
+ } catch (error) {
+ console.log('error :>> ', error)
+ } finally {
+ loading.value = false
+ }
}
const handleOk = async () => {
@@ -347,8 +410,13 @@
handleCancel()
}
} else {
- emits('onOk', mode.value, formValue.value)
- handleCancel()
+ if (props.apiEditCaller) {
+ loading.value = true
+ await editHost(formValue.value)
+ } else {
+ emits('onOk', mode.value, formValue.value)
+ handleCancel()
+ }
}
} catch (e) {
console.log(e)
@@ -434,12 +502,12 @@
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
- const isText = file.type === 'text/plain'
+ // const isText = file.type === 'text/plain'
const checkLimitSize = file.size / 1024 <= 10
- if (!isText) {
- message.error(t('common.file_type_error'))
- return false
- }
+ // if (!isText) {
+ // message.error(t('common.file_type_error'))
+ // return false
+ // }
if (!checkLimitSize) {
message.error(t('common.file_size_error'))
return false
@@ -476,6 +544,7 @@
:width="600"
:title="Mode[mode] && $t(Mode[mode])"
:mask-closable="false"
+ :confirm-loading="loading"
:centered="true"
:destroy-on-close="true"
@ok="handleOk"
@@ -504,7 +573,7 @@
{{ $t('common.upload_file') }}
</a-button>
</a-upload>
- <span class="filename">{{ fileName }}</span>
+ <span class="filename">{{ fileName ? fileName : mode === 'EDIT' ?
formValue[item.field] : '' }}</span>
</a-form-item>
</template>
</auto-form>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/hosts/detail.vue
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/detail.vue
new file mode 100644
index 00000000..84d221e9
--- /dev/null
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/detail.vue
@@ -0,0 +1,112 @@
+<!--
+ ~ 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, ref, shallowRef } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { useRoute } from 'vue-router'
+ import { getHost } from '@/api/hosts'
+ import { CommonStatus, CommonStatusTexts } from '@/enums/state'
+ import Overview from './overview.vue'
+ import type { GroupItem } from '@/components/common/button-group/types'
+ import type { HostStatusType, HostVO } from '@/api/hosts/types.ts'
+
+ const { t } = useI18n()
+ const route = useRoute()
+ const hostInfo = shallowRef<HostVO>({})
+ const loading = ref(false)
+ const statusColors = shallowRef<Record<HostStatusType, keyof typeof
CommonStatusTexts>>({
+ 1: 'healthy',
+ 2: 'unhealthy',
+ 3: 'unknown'
+ })
+ const actionGroup = computed<GroupItem[]>(() => [
+ {
+ shape: 'default',
+ type: 'default',
+ text: t('common.more_operations'),
+ dropdownMenu: [
+ {
+ action: 'Start',
+ text: t('common.start', [t('common.all')])
+ },
+ {
+ action: 'Restart',
+ text: t('common.restart', [t('common.all')])
+ },
+ {
+ action: 'Stop',
+ text: t('common.stop', [t('common.all')])
+ }
+ ],
+ dropdownMenuClickEvent: (info) => dropdownMenuClick &&
dropdownMenuClick(info)
+ }
+ ])
+
+ const dropdownMenuClick: GroupItem['dropdownMenuClickEvent'] = async ({ key
}) => {
+ console.log(key)
+ // try {
+ // await execCommand({
+ // command: key as keyof typeof Command,
+ // clusterId: currCluster.value.id,
+ // commandLevel: 'cluster'
+ // })
+ // await clusterStore.loadClusters()
+ // await clusterStore.getClusterDetail()
+ // } catch (error) {
+ // console.log('error :>> ', error)
+ // }
+ }
+
+ const setupHostInfo = async () => {
+ try {
+ loading.value = true
+ const hostId = route.query.hostId
+ const data = await getHost({ id: Number(hostId) })
+ hostInfo.value = {
+ ...data
+ }
+ } catch (error) {
+ console.log(error)
+ } finally {
+ loading.value = false
+ }
+ }
+
+ onMounted(() => {
+ setupHostInfo()
+ })
+</script>
+
+<template>
+ <a-spin :spinning="loading">
+ <header-card
+ :title="hostInfo.hostname"
+ avatar="host"
+ :status="CommonStatus[statusColors[hostInfo.status as HostStatusType]]"
+ :desc="hostInfo.desc"
+ :action-groups="actionGroup"
+ />
+ <main-card>
+ <overview :host-info="hostInfo" />
+ </main-card>
+ </a-spin>
+</template>
+
+<style lang="scss" scoped></style>
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 8150b621..caf4eda8 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/index.vue
@@ -18,18 +18,372 @@
-->
<script setup lang="ts">
+ import { message, Modal, TableColumnType, TableProps } from 'ant-design-vue'
+ import { computed, onMounted, onUnmounted, reactive, ref, shallowRef } from
'vue'
import { useRouter } from 'vue-router'
+ import { useI18n } from 'vue-i18n'
+ import * as hostApi from '@/api/hosts'
+ import useBaseTable from '@/composables/use-base-table'
+ import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
+ import InstallDependencies from './install-dependencies.vue'
+ import { useClusterStore } from '@/store/cluster'
+ import type { FilterConfirmProps, FilterResetProps, TableRowSelection } from
'ant-design-vue/es/table/interface'
+ import type { HostReq } from '@/api/command/types'
+ import type { GroupItem } from '@/components/common/button-group/types'
+ import type { HostVO } from '@/api/hosts/types'
+
+ const POLLING_INTERVAL = 30000
+ type Key = string | number
+
+ interface TableState {
+ selectedRowKeys: Key[]
+ searchText: string
+ searchedColumn: string
+ }
+
+ const { t } = useI18n()
const router = useRouter()
+ const clusterStore = useClusterStore()
+ const searchInputRef = ref()
+ const pollingIntervalId = ref<any>(null)
+ const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
+ const installRef = ref<InstanceType<typeof InstallDependencies> | null>(null)
+ const hostStatus = shallowRef(['INSTALLING', 'SUCCESS', 'FAILED', 'UNKNOWN'])
+ const state = reactive<TableState>({
+ searchText: '',
+ searchedColumn: '',
+ selectedRowKeys: []
+ })
+ const filtersOfClusterDisplayName = computed(() =>
+ clusterStore.clusters.map((v) => ({
+ text: v.displayName || v.name,
+ value: v.id!
+ }))
+ )
+ const columns = computed((): TableColumnType<HostVO>[] => [
+ {
+ title: t('host.hostname'),
+ dataIndex: 'hostname',
+ key: 'hostname',
+ ellipsis: true,
+ customFilterDropdown: true,
+ onFilterDropdownOpenChange: (visible) =>
onFilterDropdownOpenChange(visible)
+ },
+ {
+ title: t('common.cluster'),
+ key: 'clusterDisplayName',
+ dataIndex: 'clusterDisplayName',
+ ellipsis: true,
+ filterMultiple: false,
+ filters: filtersOfClusterDisplayName.value
+ },
+ {
+ title: t('host.ip_address'),
+ dataIndex: 'ipv4',
+ key: 'ipv4',
+ ellipsis: true,
+ customFilterDropdown: true,
+ onFilterDropdownOpenChange: (visible) =>
onFilterDropdownOpenChange(visible)
+ },
+ {
+ key: 'os',
+ title: t('common.os'),
+ dataIndex: 'os',
+ ellipsis: true
+ },
+ {
+ title: t('common.arch'),
+ dataIndex: 'arch',
+ ellipsis: true
+ },
+ {
+ title: t('host.component_count'),
+ dataIndex: 'componentNum',
+ key: 'componentNum',
+ width: '180px',
+ ellipsis: true
+ },
+ {
+ title: t('common.status'),
+ dataIndex: 'status',
+ key: 'status',
+ width: '260px',
+ 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.action'),
+ key: 'operation',
+ width: '160px',
+ fixed: 'right'
+ }
+ ])
+
+ const operations = computed((): GroupItem[] => [
+ {
+ text: 'edit',
+ clickEvent: (_item, args) => editHost(args)
+ },
+ {
+ text: 'remove',
+ danger: true,
+ clickEvent: (_item, args) => deleteHost([args.id])
+ }
+ ])
+
+ const { loading, dataSource, filtersParams, paginationProps, onChange } =
useBaseTable<HostVO>({
+ columns: columns.value,
+ rows: []
+ })
+
+ const handleSearch = (selectedKeys: Key[], confirm: (param?:
FilterConfirmProps) => void, dataIndex: string) => {
+ confirm()
+ Object.assign(state, {
+ searchText: selectedKeys[0] as string,
+ searchedColumn: dataIndex
+ })
+ getHostList(true)
+ }
+
+ const handleReset = (clearFilters: (param?: FilterResetProps) => void) => {
+ clearFilters({ confirm: true })
+ state.searchText = ''
+ getHostList(true)
+ }
+
+ const getHostList = async (isFirstCall = false) => {
+ if (!paginationProps.value) {
+ loading.value = false
+ return
+ }
+ try {
+ if (isFirstCall) {
+ loading.value = true
+ paginationProps.value.current = 1
+ }
+ const res = await hostApi.getHosts({ ...filtersParams.value, clusterId:
0 })
+ dataSource.value = res.content
+ paginationProps.value.total = res.total
+ loading.value = false
+ } catch (error) {
+ console.log('error :>> ', error)
+ stopPolling()
+ } finally {
+ loading.value = false
+ }
+ }
+
+ const onSelectChange: TableRowSelection['onChange'] = (selectedRowKeys) => {
+ state.selectedRowKeys = selectedRowKeys
+ }
+
+ const onFilterDropdownOpenChange = (visible: boolean) => {
+ if (visible) {
+ setTimeout(() => {
+ searchInputRef.value.focus()
+ }, 100)
+ }
+ }
+
const addHost = () => {
- router.push({ name: 'HostCreate' })
+ hostCreateRef.value?.handleOpen('ADD')
}
+
+ const bulkRemove = () => {
+ if (state.selectedRowKeys.length === 0) {
+ message.error(t('common.delete_empty'))
+ return
+ }
+ deleteHost(state.selectedRowKeys as number[])
+ }
+
+ const editHost = (row: HostVO) => {
+ const cluster = clusterStore.clusters.find((v) => v.name ===
row.clusterName)
+ const formatHost = { ...row, displayName: row.clusterDisplayName,
clusterId: cluster?.id }
+ hostCreateRef.value?.handleOpen('EDIT', formatHost)
+ }
+
+ const deleteHost = (ids: number[]) => {
+ Modal.confirm({
+ title: ids.length > 1 ? t('common.delete_msgs') : t('common.delete_msg'),
+ async onOk() {
+ try {
+ const data = await hostApi.removeHost({ ids })
+ if (data) {
+ message.success(t('common.delete_success'))
+ await getHostList(true)
+ }
+ } catch (error) {
+ console.log('error :>> ', error)
+ }
+ }
+ })
+ }
+
+ const afterSetupHostConfig = async (type: 'ADD' | 'EDIT', item: HostReq) => {
+ type === 'ADD' ? installRef.value?.handleOpen(item) : await
getHostList(true)
+ }
+
+ onMounted(() => {
+ startPolling()
+ })
+
+ const startPolling = () => {
+ getHostList(true)
+ pollingIntervalId.value = setInterval(() => {
+ getHostList()
+ }, POLLING_INTERVAL)
+ }
+
+ const stopPolling = () => {
+ if (pollingIntervalId.value) {
+ clearInterval(pollingIntervalId.value)
+ pollingIntervalId.value = null
+ }
+ }
+
+ const tableChange: TableProps['onChange'] = (pagination, filters, ...args)
=> {
+ onChange(pagination, filters, ...args)
+ getHostList(true)
+ }
+
+ const viewHostDetail = (row: HostVO) => {
+ router.push({ name: 'HostDetail', query: { hostId: row.id } })
+ }
+
+ onUnmounted(() => {
+ stopPolling()
+ })
</script>
<template>
- <div>
- <a-button @click="addHost">create host</a-button>
- <div>host list</div>
+ <div class="host-list">
+ <div class="title">{{ $t('host.host_list') }}</div>
+ <a-space :size="16">
+ <a-button type="primary" @click="addHost">{{ $t('cluster.add_host')
}}</a-button>
+ <a-button type="primary" danger @click="bulkRemove">{{
$t('common.bulk_remove') }}</a-button>
+ </a-space>
+ <a-table
+ :loading="loading"
+ row-key="id"
+ :data-source="dataSource"
+ :columns="columns"
+ :pagination="paginationProps"
+ :scroll="{ x: 1300, 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="!['status', 'clusterDisplayName'].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="column.key === 'hostname'">
+ <a-typography-link underlin>
+ <span :title="record.hostname" @click="viewHostDetail(record)">{{
record.hostname }}</span>
+ </a-typography-link>
+ </template>
+ <template v-if="column.key === 'componentNum'">
+ <span :title="record.componentNum">
+ {{ `${record.componentNum}
${$t('host.component_count_quantifier')}` }}
+ </span>
+ </template>
+ <template v-if="column.key === 'status'">
+ <svg-icon style="margin-left: 0"
:name="hostStatus[record.status].toLowerCase()" />
+ <span>{{ $t(`common.${hostStatus[record.status].toLowerCase()}`)
}}</span>
+ </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"
+ ></button-group>
+ </template>
+ </template>
+ </a-table>
+ <host-create ref="hostCreateRef" :api-edit-caller="true" :is-public="true"
@on-ok="afterSetupHostConfig" />
+ <install-dependencies ref="installRef"
@on-install-success="getHostList(true)" />
</div>
</template>
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+ .host-list {
+ width: 100%;
+ height: 100vh;
+ overflow: auto;
+ background-color: #fff;
+ padding: 16px;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .title {
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 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%;
+ }
+ }
+ }
+
+ .highlight {
+ background-color: rgb(255, 192, 105);
+ padding: 0px;
+ }
+</style>
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
new file mode 100644
index 00000000..a55797ac
--- /dev/null
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/install-dependencies.vue
@@ -0,0 +1,355 @@
+<!--
+ ~ 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 { message, TableColumnType } from 'ant-design-vue'
+ import { storeToRefs } from 'pinia'
+ import { computed, reactive, ref, shallowRef } from 'vue'
+ import { useI18n } from 'vue-i18n'
+ import { getInstalledStatus, installDependencies } from '@/api/hosts'
+ import { useClusterStore } from '@/store/cluster'
+ import { generateRandomId } from '@/utils/tools'
+ import { execCommand } from '@/api/command'
+ import HostCreate from '@/pages/cluster-manage/hosts/create.vue'
+ import useBaseTable from '@/composables/use-base-table'
+ import { type InstalledStatusVO, Status } from '@/api/hosts/types'
+ import type { FilterConfirmProps, FilterResetProps } from
'ant-design-vue/es/table/interface'
+ import type { GroupItem } from '@/components/common/button-group/types'
+ import type { CommandRequest, HostReq } from '@/api/command/types'
+
+ type Key = string | number
+
+ interface TableState {
+ selectedRowKeys: Key[]
+ searchText: string
+ searchedColumn: string
+ }
+
+ interface Emits {
+ (event: 'onInstallSuccess'): void
+ }
+
+ const { t } = useI18n()
+ const emits = defineEmits<Emits>()
+ const clusterStore = useClusterStore()
+ const { clusters } = storeToRefs(clusterStore)
+ const installing = ref(false)
+ const open = ref(false)
+ const searchInputRef = ref()
+ const hostCreateRef = ref<InstanceType<typeof HostCreate> | null>(null)
+ const installStatus = shallowRef<InstalledStatusVO[]>([])
+ const state = reactive<TableState>({
+ searchText: '',
+ searchedColumn: '',
+ selectedRowKeys: []
+ })
+ const allInstallSuccess = computed(() => dataSource.value.every((v) =>
v.status === Status.Success))
+ const clusterNameMap = computed(() => new Map(clusters.value.map((v) =>
[v.id, v])))
+ const columns = computed((): TableColumnType<HostReq & { status: string }>[]
=> [
+ {
+ title: t('host.hostname'),
+ dataIndex: 'hostname',
+ key: 'hostname',
+ ellipsis: true,
+ customFilterDropdown: true,
+ onFilter: (value, record) => isContain(record.hostname, value as string)
+ },
+ {
+ title: t('common.cluster'),
+ key: 'clusterId',
+ dataIndex: 'clusterId',
+ ellipsis: true
+ },
+ {
+ title: t('common.status'),
+ dataIndex: 'status',
+ key: 'status',
+ ellipsis: true,
+ filterMultiple: false,
+ onFilter: (value, record: HostReq & { status: string }) =>
record.status.includes(value as string),
+ filters: [
+ {
+ text: t('common.unknown'),
+ value: Status.Unknown
+ },
+ {
+ text: t('common.success'),
+ value: Status.Success
+ },
+ {
+ text: t('common.failed'),
+ value: Status.Failed
+ }
+ ]
+ },
+ {
+ title: t('common.action'),
+ width: '180px',
+ key: 'operation',
+ fixed: 'right'
+ }
+ ])
+
+ const operations = computed((): GroupItem[] => [
+ {
+ text: 'edit',
+ disabled: installing.value,
+ clickEvent: (_item, args) => editPreAddedHost(args)
+ },
+ {
+ text: 'remove',
+ danger: true,
+ disabled: installing.value,
+ clickEvent: (_item, args) => deletePreAddedHost(args)
+ }
+ ])
+
+ const { loading, dataSource, paginationProps, onChange, resetState } =
useBaseTable<HostReq>({
+ columns: columns.value,
+ rows: []
+ })
+
+ const isContain = (source: string, target: string): boolean => {
+ return source.toString().toLowerCase().includes(target.toLowerCase())
+ }
+
+ const splitHostFormToHostList = (hostForm: HostReq) => {
+ dataSource.value = hostForm.hostnames?.map((v) => ({
+ ...hostForm,
+ hostname: v,
+ hostnames: [v],
+ status: 'UNKNOWN',
+ key: generateRandomId()
+ })) as HostReq[]
+ }
+
+ const setHostStatusToInstalling = () => {
+ return dataSource.value.map((item: HostReq) => ({
+ ...item,
+ status: Status.Installing
+ }))
+ }
+
+ const startInstallDependencies = async () => {
+ if (dataSource.value.length == 0) {
+ message.error(t('host.uninstallable'))
+ return
+ }
+ try {
+ installing.value = true
+ const data = await installDependencies((dataSource.value =
setHostStatusToInstalling()))
+ data && pollUntilInstalled()
+ } catch (error) {
+ installing.value = false
+ console.log('error :>> ', error)
+ }
+ }
+
+ const pollUntilInstalled = (interval: number = 1000): void => {
+ let isInitialized = false
+ const intervalId = setInterval(async () => {
+ if (!isInitialized) {
+ dataSource.value = setHostStatusToInstalling()
+ isInitialized = true
+ }
+ const result = await recordInstalledStatus()
+ if (result) {
+ installing.value = false
+ clearInterval(intervalId)
+ }
+ }, interval)
+ }
+
+ const recordInstalledStatus = async () => {
+ try {
+ const data = await getInstalledStatus()
+ installStatus.value = data
+ dataSource.value = mergeByHostname(dataSource.value, data)
+ return data.every((item) => item.status != Status.Installing)
+ } catch (error) {
+ console.log('error :>> ', error)
+ }
+ }
+
+ const mergeByHostname = (arr1: any[], arr2: any[]): any[] => {
+ const mergedMap = new Map<string, any>()
+ for (const item of arr1) {
+ mergedMap.set(item.hostname, { ...item })
+ }
+ for (const item of arr2) {
+ if (mergedMap.has(item.hostname)) {
+ mergedMap.set(item.hostname, { ...mergedMap.get(item.hostname),
...item })
+ } else {
+ mergedMap.set(item.hostname, { ...item })
+ }
+ }
+ return Array.from(mergedMap.values())
+ }
+
+ const startAddHosts = async () => {
+ const data = {
+ command: 'Add',
+ commandLevel: 'host',
+ hostCommands: dataSource.value
+ } as CommandRequest
+ installing.value = true
+ try {
+ await execCommand(data)
+ emits('onInstallSuccess')
+ handleCancel()
+ } catch (error) {
+ console.log('error :>> ', error)
+ } finally {
+ installing.value = false
+ }
+ }
+
+ const editPreAddedHost = (row: HostReq) => {
+ hostCreateRef.value?.handleOpen('EDIT', row)
+ }
+
+ const deletePreAddedHost = (row: HostReq) => {
+ dataSource.value = dataSource.value?.filter((v) => row.key !== v.key)
+ }
+
+ const handleInstalled = () => {
+ allInstallSuccess.value ? startAddHosts() : startInstallDependencies()
+ }
+
+ const handleOpen = (payLoad: HostReq) => {
+ open.value = true
+ splitHostFormToHostList(payLoad)
+ }
+
+ const handleCancel = () => {
+ open.value = false
+ resetState()
+ }
+
+ 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 handleOk = (_: string, item: HostReq) => {
+ const index = dataSource.value.findIndex((data) => data.key === item.key)
+ if (index !== -1) {
+ dataSource.value[index] = item
+ }
+ }
+
+ defineExpose({
+ handleOpen
+ })
+</script>
+
+<template>
+ <div class="install-dependencies">
+ <a-modal
+ :open="open"
+ width="60%"
+ :title="$t('cluster.install_dependencies')"
+ :mask-closable="false"
+ :centered="true"
+ :confirm-loading="installing"
+ :ok-text="allInstallSuccess ? $t('common.confirm') : installing ?
$t('common.installing') : t('common.install')"
+ :destroy-on-close="true"
+ @ok="handleInstalled"
+ @cancel="handleCancel"
+ >
+ <a-table
+ :loading="loading"
+ row-key="id"
+ :data-source="dataSource"
+ :columns="columns"
+ :pagination="paginationProps"
+ @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)"
+ />
+ <a-space :size="16">
+ <a-button type="primary" size="small"
@click="handleSearch(selectedKeys, confirm, column.dataIndex)">
+ {{ $t('common.search') }}
+ </a-button>
+ <a-button size="small" @click="handleReset(clearFilters)"> {{
$t('common.reset') }}</a-button>
+ </a-space>
+ </div>
+ </template>
+ <template #customFilterIcon="{ filtered, column }">
+ <svg-icon v-if="column.key === 'hostname'" :name="filtered ?
'search_activated' : 'search'" />
+ <svg-icon v-else :name="filtered ? 'filter_activated' : 'filter'" />
+ </template>
+ <template #bodyCell="{ record, column }">
+ <template v-if="column.key === 'clusterId'">
+ <span
:title="`${clusterNameMap.get(record.clusterId)?.displayName}`">
+ {{ clusterNameMap.get(record.clusterId)?.displayName }}
+ </span>
+ </template>
+ <template v-if="column.key === 'status'">
+ <svg-icon :name="record.status.toLowerCase()" />
+ <span :title="`${record.message ? record.message : ''}`">
+ {{ `${$t(`common.${record.status.toLowerCase()}`)}` }}
+ {{ record.message && record.status.toLowerCase() === 'failed' ?
`: ${record.message}` : '' }}
+ </span>
+ </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"
+ ></button-group>
+ </template>
+ </template>
+ </a-table>
+ </a-modal>
+ <host-create ref="hostCreateRef" :is-public="true" @on-ok="handleOk" />
+ </div>
+</template>
+
+<style lang="scss" scoped>
+ .search {
+ display: grid;
+ gap: $space-sm;
+ padding: $space-sm;
+ }
+
+ .highlight {
+ background-color: rgb(255, 192, 105);
+ padding: 0;
+ }
+</style>
diff --git a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
similarity index 64%
copy from bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
copy to bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
index 131ff884..689847ae 100644
--- a/bigtop-manager-ui/src/pages/cluster-manage/cluster/overview.vue
+++ b/bigtop-manager-ui/src/pages/cluster-manage/hosts/overview.vue
@@ -18,19 +18,18 @@
-->
<script setup lang="ts">
- import { computed, onActivated, ref, shallowRef, useAttrs } from 'vue'
+ import { computed, ref, shallowRef, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
- import { storeToRefs } from 'pinia'
- import { useServiceStore } from '@/store/service'
- import { formatFromByte } from '@/utils/storage'
+ import { Empty, MenuProps } from 'ant-design-vue'
+ import { formatFromByte } from '@/utils/storage.ts'
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, Empty } from 'ant-design-vue'
- import type { ServiceListParams } from '@/api/service/types'
- import type { StackVO } from '@/api/stack/types'
+ 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'
type TimeRangeText = '1m' | '15m' | '30m' | '1h' | '6h' | '30h'
type TimeRangeItem = {
@@ -38,27 +37,28 @@
time: string
}
+ interface Props {
+ hostInfo: HostVO
+ }
+
+ const props = defineProps<Props>()
+ const { hostInfo } = toRefs(props)
const { t } = useI18n()
- const attrs = useAttrs() as ClusterVO
- const serviceStore = useServiceStore()
+ const currExecuteComponent = shallowRef<ComponentVO>({})
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 statusColors = shallowRef<Record<ClusterStatusType, keyof typeof
CommonStatusTexts>>({
- 1: 'healthy',
- 2: 'unhealthy',
- 3: 'unknown'
- })
- const { locateStackWithService, serviceNames } = storeToRefs(serviceStore)
- const clusterDetail = computed(() => ({
- ...attrs,
- totalMemory: formatFromByte(attrs.totalMemory as number),
- totalDisk: formatFromByte(attrs.totalDisk as number)
- }))
+ const componentsFromCurrentHost = shallowRef<Map<string, ComponentVO[]>>(new
Map())
+ const needFormatFormByte = computed(() => ['totalMemorySize', 'totalDisk'])
const noChartData = computed(() => Object.values(chartData.value).every((v)
=> v.length === 0))
const timeRanges = computed((): TimeRangeItem[] => [
{
@@ -86,61 +86,77 @@
time: ''
}
])
- const baseConfig = computed((): Partial<Record<keyof ClusterVO, string>> => {
- return {
- status: t('overview.cluster_status'),
- displayName: t('overview.cluster_name'),
- desc: t('overview.cluster_desc'),
- totalHost: t('overview.host_count'),
- totalService: t('overview.service_count'),
- totalMemory: t('overview.memory'),
- totalProcessor: t('overview.core_count'),
- totalDisk: t('overview.disk_size'),
- createUser: t('overview.creator')
- }
- })
- const unitOfBaseConfig = computed((): Partial<Record<keyof ClusterVO,
string>> => {
+ const baseConfig = computed((): Partial<Record<keyof HostVO, string>> => {
return {
- totalHost: t('overview.unit_host'),
- totalService: t('overview.unit_service'),
- totalProcessor: t('overview.unit_processor')
+ 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')
}
})
- const detailKeys = computed(() => Object.keys(baseConfig.value) as (keyof
ClusterVO)[])
- const serviceOperates = computed(() => [
+ 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.service')])
+ text: t('common.start', [t('common.component')])
},
{
action: 'restart',
- text: t('common.restart', [t('common.service')])
+ text: t('common.restart', [t('common.component')])
},
{
action: 'stop',
- text: t('common.stop', [t('common.service')])
+ text: t('common.stop', [t('common.component')])
}
])
- const handleServiceOperate: MenuProps['onClick'] = (item) => {
+ const handleHostOperate: MenuProps['onClick'] = (item) => {
console.log('item :>> ', item.key)
+ console.log(currExecuteComponent.value)
}
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))
+ const recordClickComp = (comp: ComponentVO) => {
+ currExecuteComponent.value = comp
}
- onActivated(() => {
- getServices()
- })
+ 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>
@@ -171,23 +187,25 @@
<a-tag
v-if="base === 'status'"
class="reset-tag"
-
:color="CommonStatus[statusColors[clusterDetail[base] as ClusterStatusType]]"
+ :color="CommonStatus[statusColors[hostInfo[base] as
ClusterStatusType]]"
>
- <status-dot
:color="CommonStatus[statusColors[clusterDetail[base] as ClusterStatusType]]" />
- {{
- clusterDetail[base] &&
- $t(`common.${statusColors[clusterDetail[base] as
ClusterStatusType]}`)
- }}
+ <status-dot
:color="CommonStatus[statusColors[hostInfo[base] as ClusterStatusType]]" />
+ {{ hostInfo[base] &&
$t(`common.${statusColors[hostInfo[base] as ClusterStatusType]}`) }}
</a-tag>
- <a-typography-text
- v-else
- class="desc-sub-item-desc-column"
- :content="
- Object.keys(unitOfBaseConfig).includes(base)
- ? `${clusterDetail[base]}
${unitOfBaseConfig[base]}`
- : `${clusterDetail[base]}`
- "
- />
+ <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>
</div>
</template>
</div>
@@ -196,11 +214,11 @@
</a-descriptions>
</div>
</div>
- <!-- service info -->
- <template v-if="locateStackWithService.length == 0">
- <div class="service-info">
+
+ <template v-if="componentsFromCurrentHost.size === 0">
+ <div class="component-info">
<div class="box-title">
- <a-typography-text strong :content="$t('overview.service_info')"
/>
+ <a-typography-text strong
:content="$t('overview.component_info')" />
</div>
<div class="box-empty">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
@@ -209,28 +227,25 @@
</template>
<a-descriptions v-else layout="vertical" bordered :column="1">
<template #title>
- <a-typography-text strong :content="$t('overview.service_info')" />
+ <a-typography-text strong :content="$t('overview.component_info')"
/>
</template>
- <a-descriptions-item v-for="stack in locateStackWithService"
:key="stack.stackName">
+ <a-descriptions-item v-for="stack in
componentsFromCurrentHost.keys()" :key="stack">
<template #label>
<div class="desc-sub-label">
- <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}`" />
+ <a-typography-text strong :content="stack.split('-')[0] + '
Stack'" />
+ <a-typography-text type="secondary"
:content="`${stack.toLowerCase()}`" />
</div>
</template>
- <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" />
+ <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">
+ <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="handleServiceOperate">
- <a-menu-item v-for="operate in serviceOperates"
:key="operate.action">
+ <a-menu @click="handleHostOperate">
+ <a-menu-item v-for="operate in componentOperates"
:key="operate.action">
<span>{{ operate.text }}</span>
</a-menu-item>
</a-menu>
@@ -291,10 +306,12 @@
<style lang="scss" scoped>
:deep(.ant-avatar) {
border-radius: 4px;
+
img {
object-fit: contain !important;
}
}
+
.box {
&-title {
@include flexbox($justify: space-between);
@@ -329,18 +346,19 @@
&:hover {
color: $color-primary-text-hover;
}
+
&-activated {
color: $color-primary-text;
}
}
- .service-item {
+ .component-item {
display: grid;
grid-template-columns: auto 1fr auto;
gap: $space-md;
align-items: center;
padding: 12px 16px;
- border-bottom: 1px solid #f0f0f0;
+ border-top: 1px solid #f0f0f0;
}
.chart-item-wrp {
@@ -367,9 +385,11 @@
:deep(.ant-descriptions-view) {
overflow: hidden;
border-color: $color-border;
+
.ant-descriptions-item-label {
padding: 0;
}
+
.ant-descriptions-item-content {
padding: 0;
}
@@ -378,6 +398,7 @@
.desc-sub-item-wrp {
@include flexbox($gap: $space-md);
padding: $space-md;
+
:deep(.ant-tag) {
width: fit-content;
@include flexbox($align: center, $gap: 4px);
@@ -388,8 +409,10 @@
display: grid;
grid-template-columns: max-content 1fr;
gap: 16px;
+
&-desc {
display: contents;
+
&-column {
text-align: left;
}
diff --git a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
index 1c9ecd9f..031bd82b 100644
--- a/bigtop-manager-ui/src/router/routes/modules/clusters.ts
+++ b/bigtop-manager-ui/src/router/routes/modules/clusters.ts
@@ -137,6 +137,15 @@ const routes: RouteRecordRaw[] = [
hidden: true,
activeMenu: '/cluster-manage/hosts'
}
+ },
+ {
+ name: 'HostDetail',
+ path: 'detail',
+ component: () => import('@/pages/cluster-manage/hosts/detail.vue'),
+ meta: {
+ hidden: true,
+ activeMenu: '/cluster-manage/hosts'
+ }
}
]
}
diff --git a/bigtop-manager-ui/src/store/cluster/index.ts
b/bigtop-manager-ui/src/store/cluster/index.ts
index 365b9fb7..e71a22dc 100644
--- a/bigtop-manager-ui/src/store/cluster/index.ts
+++ b/bigtop-manager-ui/src/store/cluster/index.ts
@@ -83,6 +83,9 @@ export const useClusterStore = defineStore(
}
},
{
- persist: false
+ persist: {
+ storage: sessionStorage,
+ paths: ['clusters']
+ }
}
)
diff --git a/bigtop-manager-ui/src/store/installed/index.ts
b/bigtop-manager-ui/src/store/installed/index.ts
index 4a417345..e56a2475 100644
--- a/bigtop-manager-ui/src/store/installed/index.ts
+++ b/bigtop-manager-ui/src/store/installed/index.ts
@@ -57,6 +57,7 @@ export const useInstalledStore = defineStore(
}
return {
+ serviceStore,
installedMap,
getServicesOfInfra,
setInstalledMapKey,
diff --git a/bigtop-manager-ui/src/store/menu/index.ts
b/bigtop-manager-ui/src/store/menu/index.ts
index 492c4901..eef4e6f7 100644
--- a/bigtop-manager-ui/src/store/menu/index.ts
+++ b/bigtop-manager-ui/src/store/menu/index.ts
@@ -45,6 +45,24 @@ export const useMenuStore = defineStore(
const path = route.matched.at(-1)?.path
return path?.includes(RouteExceptions.DYNAMIC_ROUTE_MATCH)
})
+
+ const updateSiderItemByClusters = (formatSider: MenuItem[]) => {
+ formatSider.forEach((item) => {
+ if (item.name == RouteExceptions.SPECIAL_ROUTE_NAME) {
+ item.children = clusters.value.map((v) => {
+ return {
+ icon: '',
+ key: `${item.key}/${v.displayName}/${v.id}`,
+ label: v.displayName || '',
+ title: v.displayName || '',
+ activeMenu: item.key,
+ status: v.status || 3
+ }
+ })
+ }
+ })
+ }
+
const siderMenus = computed((): MenuItem[] => {
const siderMenuTemplate =
baseRoutesMap.value.get(headerSelectedKey.value) || []
const formatSider = cloneDeep(siderMenuTemplate)
@@ -86,23 +104,6 @@ export const useMenuStore = defineStore(
}
})
- const updateSiderItemByClusters = (formatSider: MenuItem[]) => {
- formatSider.forEach((item) => {
- if (item.name == RouteExceptions.SPECIAL_ROUTE_NAME) {
- item.children = clusters.value.map((v) => {
- return {
- icon: '',
- key: `${item.key}/${v.displayName}/${v.id}`,
- label: v.displayName || '',
- title: v.displayName || '',
- activeMenu: item.key,
- status: v.status || 3
- }
- })
- }
- })
- }
-
const formatRouteToMenu = (tree: any[], upPath = ''): MenuItem[] => {
return tree
.filter(({ meta }) => !meta?.hidden)