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>
     )
   },

Reply via email to