This is an automated email from the ASF dual-hosted git repository. nicholasjiang pushed a commit to branch branch-0.1 in repository https://gitbox.apache.org/repos/asf/paimon-webui.git
commit edbcac9bafe1dbfc9fb9a770778246dc3551ae7f Author: s7monk <[email protected]> AuthorDate: Wed Jul 10 11:33:19 2024 +0800 [Improvement] Add cluster heartbeat detection on cluster management page (#476) --- paimon-web-ui/src/api/models/cluster/index.ts | 10 +++ paimon-web-ui/src/api/models/cluster/types.ts | 4 ++ paimon-web-ui/src/locales/en/modules/system.ts | 5 ++ paimon-web-ui/src/locales/zh/modules/system.ts | 5 ++ .../cluster/components/cluster-form/index.tsx | 58 +++++++++++++++- paimon-web-ui/src/views/system/cluster/index.tsx | 77 ++++++++++++++++++++-- 6 files changed, 154 insertions(+), 5 deletions(-) diff --git a/paimon-web-ui/src/api/models/cluster/index.ts b/paimon-web-ui/src/api/models/cluster/index.ts index d14c112e..c6445196 100644 --- a/paimon-web-ui/src/api/models/cluster/index.ts +++ b/paimon-web-ui/src/api/models/cluster/index.ts @@ -66,3 +66,13 @@ export function updateCluster() { export function deleteCluster(userId: number) { return httpRequest.delete!<unknown, ClusterDTO>(`/cluster/${userId}`) } + +/** + * # Check Cluster Status + */ +export function checkClusterStatus() { + return httpRequest.createHooks!<unknown, ClusterDTO>({ + url: '/cluster/check', + method: 'post', + }) +} diff --git a/paimon-web-ui/src/api/models/cluster/types.ts b/paimon-web-ui/src/api/models/cluster/types.ts index 0a1dd559..17bc7aef 100644 --- a/paimon-web-ui/src/api/models/cluster/types.ts +++ b/paimon-web-ui/src/api/models/cluster/types.ts @@ -23,7 +23,9 @@ export interface Cluster { host: string port: number type: string + deploymentMode: string enabled: boolean + heartbeatStatus: string } export interface ClusterNameParams { @@ -38,7 +40,9 @@ export interface ClusterDTO { host: string port: number type: string + deploymentMode: string enabled: boolean + heartbeatStatus?: string createTime?: string updateTime?: string } diff --git a/paimon-web-ui/src/locales/en/modules/system.ts b/paimon-web-ui/src/locales/en/modules/system.ts index 8ac69135..24458a60 100644 --- a/paimon-web-ui/src/locales/en/modules/system.ts +++ b/paimon-web-ui/src/locales/en/modules/system.ts @@ -102,11 +102,16 @@ const roleKey = { const cluster = { cluster_name: 'Cluster Name', cluster_type: 'Cluster Type', + cluster_status: 'Cluster Status', + deployment_type: 'Deployment Mode', cluster_host: 'Cluster Host', cluster_port: 'Cluster Port', + test_cluster: 'Testing Cluster Connectivity', enabled: 'Enabled', create: 'Create Cluster', update: 'Update Cluster', + cluster_status_check_success: 'Cluster status checked successfully', + cluster_status_check_error: 'Error checking cluster status:', } export default { user, role, roleKey, cluster } diff --git a/paimon-web-ui/src/locales/zh/modules/system.ts b/paimon-web-ui/src/locales/zh/modules/system.ts index 298f1c9a..0193c5da 100644 --- a/paimon-web-ui/src/locales/zh/modules/system.ts +++ b/paimon-web-ui/src/locales/zh/modules/system.ts @@ -102,11 +102,16 @@ const roleKey = { const cluster = { cluster_name: '集群名称', cluster_type: '集群类型', + cluster_status: '集群状态', + deployment_type: '部署模式', cluster_host: '集群地址', cluster_port: '集群端口', + test_cluster: '测试集群连通性', enabled: '是否启用', create: '新增集群', update: '更新集群', + cluster_status_check_success: '集群状态检查成功', + cluster_status_check_error: '检查集群状态时出错:', } export default { user, role, roleKey, cluster } diff --git a/paimon-web-ui/src/views/system/cluster/components/cluster-form/index.tsx b/paimon-web-ui/src/views/system/cluster/components/cluster-form/index.tsx index 7484d7e9..fd1eb0a1 100644 --- a/paimon-web-ui/src/views/system/cluster/components/cluster-form/index.tsx +++ b/paimon-web-ui/src/views/system/cluster/components/cluster-form/index.tsx @@ -39,21 +39,54 @@ const props = { port: 0, enabled: true, type: '', + deploymentMode: '', }), }, 'onUpdate:formValue': [Function, Object] as PropType<((value: ClusterDTO) => void) | undefined>, 'onConfirm': Function, + 'onCheckClusterStatus': Function as PropType<(cluster: ClusterDTO) => void>, } export default defineComponent({ - name: 'UserForm', + name: 'ClusterForm', props, setup(props) { + interface DeploymentOption { + label: string + value: string + } + const typeOptions = [ { label: 'Flink', value: 'Flink' }, { label: 'Spark', value: 'Spark' }, ] + const deploymentModeFlink = [ + { label: 'Yarn Session', value: 'yarn-session' }, + { label: 'Flink SQL Gateway', value: 'flink-sql-gateway' }, + ] + + const deploymentModeSpark: DeploymentOption[] = [] + + const deploymentModeOptions = computed(() => { + if (props.formValue.type === 'Flink') { + return deploymentModeFlink + } + else if (props.formValue.type === 'Spark') { + return deploymentModeSpark + } + return [] + }) + + watch(() => props.formValue.type, (newType) => { + if (newType === 'Spark' && deploymentModeSpark.length === 0) { + props['onUpdate:formValue']?.({ ...props.formValue, deploymentMode: '' }) + } + else if (newType === 'Flink') { + props['onUpdate:formValue']?.({ ...props.formValue, deploymentMode: deploymentModeFlink[0].value }) + } + }) + const rules = { clusterName: { required: true, @@ -76,6 +109,11 @@ export default defineComponent({ trigger: ['blur', 'input'], message: 'type required', }, + deploymentMode: { + required: true, + trigger: ['blur', 'input'], + message: 'deploymentMode required', + }, } const { t } = useLocaleHooks() @@ -101,12 +139,14 @@ export default defineComponent({ port: 0, enabled: true, type: '', + deploymentMode: '', }) } return { ...toRefs(props), typeOptions, + deploymentModeOptions, formRef, rules, handleCloseModal, @@ -146,6 +186,22 @@ export default defineComponent({ options={this.typeOptions} /> </n-form-item> + <n-form-item label={this.t('system.cluster.deployment_type')} path="deploymentMode"> + <n-select + v-model:value={this.formValue.deploymentMode} + options={this.deploymentModeOptions} + /> + </n-form-item> + <n-link + onClick={() => { + if (this.onCheckClusterStatus) { + this.onCheckClusterStatus(this.formValue) + } + }} + style="color: #007BFF; cursor: pointer; text-decoration: underline;" + > + {this.t('system.cluster.test_cluster')} + </n-link> </n-form> ), action: () => ( diff --git a/paimon-web-ui/src/views/system/cluster/index.tsx b/paimon-web-ui/src/views/system/cluster/index.tsx index d07a0862..ff2862d4 100644 --- a/paimon-web-ui/src/views/system/cluster/index.tsx +++ b/paimon-web-ui/src/views/system/cluster/index.tsx @@ -17,14 +17,15 @@ under the License. */ import type { TableColumns } from 'naive-ui/es/data-table/src/interface' import dayjs from 'dayjs' -import { EditOutlined } from '@vicons/antd' +import { EditOutlined, HeartTwotone } from '@vicons/antd' +import { Add } from '@vicons/ionicons5' import ClusterForm from './components/cluster-form' import ClusterDelete from './components/cluster-delete' import styles from './index.module.scss' -import { createCluster, getClusterList, updateCluster } from '@/api/models/cluster' +import { checkClusterStatus, createCluster, getClusterList, updateCluster } from '@/api/models/cluster' import type { ClusterDTO } from '@/api/models/cluster/types' @@ -39,29 +40,59 @@ export default defineComponent({ { title: () => t('system.cluster.cluster_name'), key: 'clusterName', + width: 160, }, { title: () => t('system.cluster.cluster_host'), key: 'host', + width: 150, }, { title: () => t('system.cluster.cluster_port'), key: 'port', + width: 120, }, { title: () => t('system.cluster.cluster_type'), key: 'type', + width: 120, + }, + { + title: () => t('system.cluster.deployment_type'), + key: 'deploymentMode', + width: 160, + render: (row: ClusterDTO) => { + switch (row.deploymentMode) { + case 'flink-sql-gateway': + return 'Flink SQL Gateway' + case 'yarn-session': + return 'Yarn Session' + default: + return row.deploymentMode + } + }, }, { title: () => t('system.cluster.enabled'), key: 'enabled', + width: 100, render: (row: ClusterDTO) => { return row.enabled ? t('common.yes') : t('common.no') }, }, + { + title: () => t('system.cluster.cluster_status'), + key: 'heartbeatStatus', + width: 120, + render: (row) => { + const status = String(row.heartbeatStatus).toLowerCase() + return status.charAt(0).toUpperCase() + status.slice(1) + }, + }, { title: () => t('common.create_time'), key: 'createTime', + width: 160, render: (row: ClusterDTO) => { return row?.createTime ? dayjs(row?.createTime).format('YYYY-MM-DD HH:mm') : '-' }, @@ -69,6 +100,7 @@ export default defineComponent({ { title: () => t('common.update_time'), key: 'updateTime', + width: 160, render: (row: ClusterDTO) => { return row?.updateTime ? dayjs(row?.updateTime).format('YYYY-MM-DD HH:mm') : '-' }, @@ -76,7 +108,9 @@ export default defineComponent({ { title: () => t('common.action'), key: 'actions', + fixed: 'right', resizable: true, + width: 150, render: (row: ClusterDTO) => { return ( <n-space> @@ -85,6 +119,11 @@ export default defineComponent({ icon: () => <n-icon component={EditOutlined} />, }} </n-button> + <n-button onClick={() => handleCheckClusterStatus(row)} strong secondary circle type="success"> + {{ + icon: () => <n-icon component={HeartTwotone} />, + }} + </n-button> <ClusterDelete clusterId={row?.id} onDelete={getTableData} /> </n-space> ) @@ -104,6 +143,7 @@ export default defineComponent({ host: '', port: 0, type: '', + deploymentMode: '', enabled: true, }) @@ -124,6 +164,22 @@ export default defineComponent({ formVisible.value = true } + async function handleCheckClusterStatus(cluster: ClusterDTO) { + const [, checkStatus] = checkClusterStatus() + + try { + await checkStatus({ + params: cluster, + }) + message.success(t('system.cluster.cluster_status_check_success')) + getTableData() + } + catch (error) { + const errorMessage = (error as Error).message + message.error(t('system.cluster.cluster_status_check_error') + errorMessage) + } + } + const tableVariables = reactive({ searchForm: { clusterName: '', @@ -180,6 +236,7 @@ export default defineComponent({ formValue, handleCreateModal, onConfirm, + handleCheckClusterStatus, } }, render() { @@ -188,7 +245,12 @@ export default defineComponent({ <n-card> <n-space vertical> <n-space justify="space-between"> - <n-button onClick={this.handleCreateModal} type="primary">{this.t('system.user.add')}</n-button> + <n-button onClick={this.handleCreateModal} type="primary"> + {{ + icon: () => <n-icon component={Add} />, + default: () => this.t('system.cluster.create'), + }} + </n-button> </n-space> <n-data-table columns={this.columns} @@ -200,7 +262,14 @@ export default defineComponent({ /> </n-space> </n-card> - <ClusterForm modelLoading={this.modelLoading} formType={this.formType} v-model:visible={this.formVisible} v-model:formValue={this.formValue} onConfirm={this.onConfirm} /> + <ClusterForm + modelLoading={this.modelLoading} + formType={this.formType} + v-model:visible={this.formVisible} + v-model:formValue={this.formValue} + onConfirm={this.onConfirm} + onCheckClusterStatus={this.handleCheckClusterStatus} + /> </n-space> ) },
