This is an automated email from the ASF dual-hosted git repository.

nicholasjiang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-webui.git


The following commit(s) were added to refs/heads/main by this push:
     new 475f7328 [Feature] Enhance job submission frontend (#266)
475f7328 is described below

commit 475f7328e160cc264a2d6d414504c7a9d2f33fa1
Author: s7monk <[email protected]>
AuthorDate: Fri May 31 19:49:31 2024 +0800

    [Feature] Enhance job submission frontend (#266)
---
 paimon-web-ui/package.json                         |   2 +-
 paimon-web-ui/src/api/models/cluster/index.ts      |  17 +-
 paimon-web-ui/src/api/models/job/index.ts          |  55 ++++++
 paimon-web-ui/src/api/models/job/types/job.ts      |  75 +++++++
 paimon-web-ui/src/locales/en/modules/playground.ts |   6 +
 paimon-web-ui/src/locales/zh/modules/playground.ts |   6 +
 paimon-web-ui/src/store/job/index.ts               |  97 +++++++++
 .../index.module.scss => store/job/type.ts}        |  33 +---
 .../console/components/controls/index.module.scss  |   4 +
 .../console/components/controls/index.tsx          | 220 ++++++++++++++++++---
 .../components/console/components/table/index.tsx  | 112 +++++++----
 .../components/query/components/debugger/index.tsx | 125 +++++++++---
 .../components/query/components/tabs/index.tsx     |   8 +-
 .../views/playground/components/query/index.tsx    |  60 ++++++
 14 files changed, 688 insertions(+), 132 deletions(-)

diff --git a/paimon-web-ui/package.json b/paimon-web-ui/package.json
index 67e65d03..5e132081 100644
--- a/paimon-web-ui/package.json
+++ b/paimon-web-ui/package.json
@@ -19,7 +19,7 @@
     "@antv/x6-plugin-dnd": "^2.1.1",
     "@antv/x6-vue-shape": "^2.1.1",
     "dart-sass": "^1.25.0",
-    "dayjs": "^1.11.10",
+    "dayjs": "^1.11.11",
     "lodash": "^4.17.21",
     "mitt": "^3.0.1",
     "monaco-editor": "^0.43.0",
diff --git a/paimon-web-ui/src/api/models/cluster/index.ts 
b/paimon-web-ui/src/api/models/cluster/index.ts
index 92f88092..ec937e3c 100644
--- a/paimon-web-ui/src/api/models/cluster/index.ts
+++ b/paimon-web-ui/src/api/models/cluster/index.ts
@@ -16,7 +16,7 @@ specific language governing permissions and limitations
 under the License. */
 
 import httpRequest from '../../request'
-import type { Cluster, ClusterDTO, ClusterNameParams } from './types'
+import type {Cluster, ClusterDTO, ClusterNameParams} from './types'
 import type { ResponseOptions } from '@/api/types'
 
 /**
@@ -29,6 +29,17 @@ export function getClusterList() {
   })
 }
 
+/**
+ * # List Cluster by ClusterType
+ */
+export function getClusterListByType(type: string, pageNum: number, pageSize: 
number) {
+  return httpRequest.get('/cluster/list', {
+    type,
+    pageNum,
+    pageSize
+  });
+}
+
 /**
  * # Create Cluster
  */
@@ -40,7 +51,7 @@ export function createCluster() {
 }
 
 /**
- * # Update user
+ * # Update Cluster
  */
 export function updateCluster() {
   return httpRequest.createHooks!<unknown, ClusterDTO>({
@@ -50,7 +61,7 @@ export function updateCluster() {
 }
 
 /**
- * # delete a Cluster
+ * # Delete a Cluster
  */
 export function deleteCluster(userId: number) {
   return httpRequest.delete!<unknown, ClusterDTO>(`/cluster/${userId}`)
diff --git a/paimon-web-ui/src/api/models/job/index.ts 
b/paimon-web-ui/src/api/models/job/index.ts
new file mode 100644
index 00000000..e83fdfb4
--- /dev/null
+++ b/paimon-web-ui/src/api/models/job/index.ts
@@ -0,0 +1,55 @@
+/* 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. */
+
+import httpRequest from '../../request'
+import type {JobSubmitDTO, Job, ResultFetchDTO, JobResultData, JobStatus, 
StopJobDTO} from "@/api/models/job/types/job"
+import type {ResponseOptions} from "@/api/types"
+
+/**
+ * # Submit a job
+ */
+export function submitJob(jobSubmitDTO: JobSubmitDTO) {
+  return httpRequest.post<JobSubmitDTO, ResponseOptions<Job>>('/job/submit', 
jobSubmitDTO)
+}
+
+/**
+ * # Fetch the result of a submitted job
+ */
+export function fetchResult(resultFetchDTO: ResultFetchDTO) {
+  return httpRequest.post<ResultFetchDTO, 
ResponseOptions<JobResultData>>('/job/fetch', resultFetchDTO)
+}
+
+/**
+ * # Refresh the status of jobs
+ */
+export function refreshJobStatus() {
+  return httpRequest.post('/job/refresh')
+}
+
+/**
+ * # Fetch the status of a specific job by its ID
+ */
+export function getJobStatus(jobId: string) {
+  return httpRequest.get<string, 
ResponseOptions<JobStatus>>(`/job/status/get/${jobId}`)
+}
+
+/**
+ * # Stop a job
+ */
+export function stopJob(stopJobDTO: StopJobDTO) {
+  return httpRequest.post<StopJobDTO, ResponseOptions<void>>('/job/stop', 
stopJobDTO)
+}
\ No newline at end of file
diff --git a/paimon-web-ui/src/api/models/job/types/job.ts 
b/paimon-web-ui/src/api/models/job/types/job.ts
new file mode 100644
index 00000000..c327a771
--- /dev/null
+++ b/paimon-web-ui/src/api/models/job/types/job.ts
@@ -0,0 +1,75 @@
+/* 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. */
+
+export interface Job {
+  submitId: string
+  jobId: string
+  jobName: string
+  type: string
+  executeMode: string
+  clusterId: string
+  sessionId: string
+  uid: number
+  status?: string
+  shouldFetchResult: boolean
+  resultData: ResultDataItem[]
+  token: number
+  startTime: string
+  endTime: string
+}
+
+export interface JobSubmitDTO {
+  jobName: string
+  taskType: string
+  clusterId: string
+  config?: {
+    [key: string]: string
+  }
+  statements: string
+  streaming: boolean
+}
+
+export interface JobResultData {
+  resultData: ResultDataItem[]
+  columns: number
+  rows: number
+  token: number
+}
+
+export interface ResultDataItem {
+  [key: string]: any
+}
+
+export interface ResultFetchDTO {
+  submitId: string
+  clusterId: string
+  sessionId: string
+  taskType: string
+  token: number
+}
+
+export interface JobStatus {
+  jobId: string
+  status: string
+}
+
+export interface StopJobDTO {
+  clusterId: string
+  jobId: string
+  taskType: string
+  withSavepoint: boolean
+}
diff --git a/paimon-web-ui/src/locales/en/modules/playground.ts 
b/paimon-web-ui/src/locales/en/modules/playground.ts
index b5126316..103a9c8b 100644
--- a/paimon-web-ui/src/locales/en/modules/playground.ts
+++ b/paimon-web-ui/src/locales/en/modules/playground.ts
@@ -49,4 +49,10 @@ export default {
   schedule_refresh: 'Schedule Refresh',
   download: 'Export Data',
   copy: 'Copy Data',
+  job_submission_successfully: 'Job Submitted Successfully',
+  job_submission_failed: 'Job Submitted Failed',
+  no_data: 'No Data',
+  job_stopping_successfully: 'Job Stopped Successfully',
+  job_stopping_failed: 'Job Stopped Failed',
+  data_copied_successfully: 'Data Copied to Clipboard',
 }
diff --git a/paimon-web-ui/src/locales/zh/modules/playground.ts 
b/paimon-web-ui/src/locales/zh/modules/playground.ts
index 73d59e9e..c37e079c 100644
--- a/paimon-web-ui/src/locales/zh/modules/playground.ts
+++ b/paimon-web-ui/src/locales/zh/modules/playground.ts
@@ -49,4 +49,10 @@ export default {
   schedule_refresh: '定时刷新',
   download: '导出数据',
   copy: '复制数据',
+  job_submission_successfully: 'SQL 任务提交成功',
+  job_submission_failed: '任务提交失败',
+  no_data: '暂无数据,请重新执行SQL',
+  job_stopping_successfully: '停止任务成功',
+  job_stopping_failed: '停止任务失败',
+  data_copied_successfully: '数据已复制到剪贴板',
 }
diff --git a/paimon-web-ui/src/store/job/index.ts 
b/paimon-web-ui/src/store/job/index.ts
new file mode 100644
index 00000000..ad2d264b
--- /dev/null
+++ b/paimon-web-ui/src/store/job/index.ts
@@ -0,0 +1,97 @@
+/* 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. */
+
+import type { ExecutionMode } from './type'
+import type { Job, JobResultData } from '@/api/models/job/types/job'
+
+export interface JobState {
+  executionMode: ExecutionMode
+  currentJob: Job | null
+  jobResultData: JobResultData | null
+  jobStatus: string,
+  executionTime: string
+}
+
+export const useJobStore = defineStore({
+  id: 'job',
+  state: (): JobState => ({
+    executionMode: 'Streaming',
+    currentJob: null,
+    jobResultData: null,
+    jobStatus: '',
+    executionTime: '0m:0s'
+  }),
+  persist: false,
+  getters: {
+    getExecutionMode(): ExecutionMode {
+      return this.executionMode
+    },
+    getCurrentJob(): Job | null {
+      return this.currentJob
+    },
+    getJobResultData(): JobResultData | null {
+      return this.jobResultData
+    },
+    getColumns(): number {
+      if (this.currentJob && this.currentJob.resultData && 
this.currentJob.resultData.length > 0) {
+        return Object.keys(this.currentJob.resultData[0]).length
+      }else if (this.jobResultData && this.jobResultData.resultData && 
this.jobResultData.resultData.length > 0) {
+        return Object.keys(this.jobResultData.resultData[0]).length
+      } else {
+        return 0
+      }
+    },
+    getRows(): number {
+      if (this.currentJob && this.currentJob.resultData && 
this.currentJob.resultData.length > 0) {
+        return this.currentJob.resultData.length
+      }else if (this.jobResultData && this.jobResultData.resultData && 
this.jobResultData.resultData.length > 0) {
+        return this.jobResultData.resultData.length
+      } else {
+        return 0
+      }
+    },
+    getJobStatus(): string {
+      return this.jobStatus
+    },
+    getExecutionTime(): string {
+      return this.executionTime
+    }
+  },
+  actions: {
+    setExecutionMode(executionMode: ExecutionMode) {
+      this.executionMode = executionMode
+    },
+    setCurrentJob(currentJob: Job) {
+      this.currentJob = currentJob
+    },
+    setJobResultData(jobResultData: JobResultData) {
+      this.jobResultData = jobResultData
+    },
+    setJobStatus(jobStatus: string) {
+      this.jobStatus = jobStatus
+    },
+    setExecutionTime(executionTime: string) {
+      this.executionTime = executionTime
+    },
+    resetCurrentResult() {
+      this.currentJob = null
+      this.jobResultData = null
+      this.jobStatus = ''
+      this.executionTime = '0m:0s'
+    },
+  }
+})
\ No newline at end of file
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.module.scss
 b/paimon-web-ui/src/store/job/type.ts
similarity index 63%
copy from 
paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.module.scss
copy to paimon-web-ui/src/store/job/type.ts
index d36d5490..3eeeb003 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.module.scss
+++ b/paimon-web-ui/src/store/job/type.ts
@@ -15,35 +15,4 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
-  $color-gray: #7D818E;
-
-  display: flex;
-  align-items: center;
-  width: 100%;
-  height: 42px;
-
-  .left {
-    display: flex;
-    padding: 10px 0 10px 20px;
-  }
-
-  .right {
-    display: flex;
-    flex: 1;
-    justify-content: flex-end;
-    padding: 10px 20px 10px 0;
-  }
-
-  .active-button {
-    color: #1a6efb;
-  }
-
-  .table-action-bar-button {
-    color: $color-gray;
-  }
-
-  .table-action-bar-text {
-    color: $color-gray;
-  }
-}
\ No newline at end of file
+export type ExecutionMode = 'Streaming' | 'Batch'
\ No newline at end of file
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.module.scss
 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.module.scss
index d36d5490..842942c8 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.module.scss
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.module.scss
@@ -43,6 +43,10 @@ under the License. */
     color: $color-gray;
   }
 
+  .stop-button-running {
+    color: #D94F4F
+  }
+
   .table-action-bar-text {
     color: $color-gray;
   }
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.tsx
 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.tsx
index dce63f46..be3e5058 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.tsx
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/controls/index.tsx
@@ -16,29 +16,190 @@ specific language governing permissions and limitations
 under the License. */
 
 import { Copy, DataTable, Renew } from '@vicons/carbon'
-import { StopOutline } from '@vicons/ionicons5'
+import { StopOutline, Stop } from '@vicons/ionicons5'
 import { ClockCircleOutlined, DownloadOutlined, LineChartOutlined } from 
'@vicons/antd'
 import styles from './index.module.scss'
-import { useConfigStore } from '@/store/config'
+import {fetchResult, stopJob} from "@/api/models/job"
+import {useMessage} from "naive-ui"
+import dayjs from 'dayjs'
+import duration from 'dayjs/plugin/duration'
+import { useJobStore } from '@/store/job'
 
 export default defineComponent({
   name: 'TableActionBar',
-  setup() {
-    const { t } = useLocaleHooks()
+  setup: function () {
+    const {t} = useLocaleHooks()
+    const message = useMessage()
+    const jobStore = useJobStore()
 
-    const configStore = useConfigStore()
-    const isDarkMode = computed(() => configStore.theme === 'dark')
+    const {mittBus} = getCurrentInstance()!.appContext.config.globalProperties
 
+    const currentJob = computed(() => jobStore.getCurrentJob)
+    const jobStatus = computed(() => jobStore.getJobStatus)
+    const executionTime = computed(() => jobStore.getExecutionTime)
+    const selectedInterval = ref('Disabled')
+    const refreshIntervalId = ref<number | null>(null)
     const activeButton = ref('table')
+
     const setActiveButton = (button: any) => {
       activeButton.value = button
     }
 
+    const handleRefreshData = async () => {
+      if (currentJob.value) {
+        if (currentJob.value.shouldFetchResult) {
+          try {
+            const job = toRaw(currentJob.value)
+            const { submitId, clusterId, sessionId, type: taskType, token } = 
job
+            const resultFetchDTO = {
+              submitId,
+              clusterId,
+              sessionId,
+              taskType,
+              token
+            }
+            const response  = await fetchResult(resultFetchDTO)
+            jobStore.setJobResultData(response.data)
+          } catch (error) {
+            console.error('Error fetching result:', error)
+          }
+        } else {
+          message.warning(t('playground.no_data'))
+        }
+      } else {
+        message.warning(t('playground.no_data'))
+      }
+    }
+
+    const handleStopJob = async () => {
+      if (currentJob.value) {
+        const job = toRaw(currentJob.value);
+        const { clusterId, jobId, type: taskType} = job
+        const stopJobDTO = {
+          clusterId,
+          jobId,
+          taskType,
+          withSavepoint: false
+        }
+        try {
+          const response = await stopJob(stopJobDTO);
+          if (response.code === 200) {
+            message.success(t('playground.job_stopping_successfully'))
+          } else {
+            message.warning(t('playground.job_stopping_failed'))
+          }
+        } catch (error) {
+          message.warning(t('playground.job_stopping_failed'))
+        }
+      }
+    }
+
+    const currentStopIcon = computed(() => jobStatus.value === 'RUNNING' ? 
StopOutline : Stop);
+
+    const isButtonDisabled = computed(() => {
+      return jobStatus.value !== 'RUNNING'
+    })
+
+    const isScheduleButtonDisabled = computed(() => {
+      return jobStore.getExecutionMode === 'Batch'
+    })
+
+    const jobStatusColor = computed(() => {
+      switch (jobStatus.value.toUpperCase()) {
+        case 'RUNNING':
+          return '#33994A'
+        case 'CANCELED':
+          return '#f6b658'
+        case 'FINISHED':
+          return '#f5c1bd'
+        case 'FAILED':
+          return '#f9827c'
+        default:
+          return '#7ce998';
+      }
+    })
+
+    const formattedJobStatus = computed(() => {
+      return jobStatus.value.charAt(0).toUpperCase() + 
jobStatus.value.slice(1).toLowerCase()
+    })
+
+    const dropdownOptions = [
+      { label: 'Disabled', key: 'Disabled' },
+      { label: '5s', key: '5s' },
+      { label: '10s', key: '10s' },
+      { label: '30s', key: '30s' },
+      { label: '1m', key: '1m' },
+      { label: '5m', key: '5m' }
+    ];
+
+    const clearRefreshInterval = () => {
+      if (refreshIntervalId.value) {
+        clearInterval(refreshIntervalId.value)
+        refreshIntervalId.value = null
+      }
+    }
+
+    const setRefreshInterval = (milliseconds: number) => {
+      clearRefreshInterval()
+      refreshIntervalId.value = setInterval(handleRefreshData, milliseconds)
+    }
+
+    watch(jobStatus, () => {
+      if (jobStatus.value !== 'RUNNING') {
+        if (refreshIntervalId.value) {
+          clearRefreshInterval()
+        }
+      }
+    })
+
+    dayjs.extend(duration);
+    const handleSelect = (key: any) => {
+      selectedInterval.value = key
+      switch (key) {
+        case '5s':
+          setRefreshInterval(dayjs.duration(5, 'seconds').asMilliseconds())
+          break
+        case '10s':
+          setRefreshInterval(dayjs.duration(10, 'seconds').asMilliseconds())
+          break
+        case '30s':
+          setRefreshInterval(dayjs.duration(30, 'seconds').asMilliseconds())
+          break
+        case '1m':
+          setRefreshInterval(dayjs.duration(1, 'minute').asMilliseconds())
+          break
+        case '5m':
+          setRefreshInterval(dayjs.duration(5, 'minutes').asMilliseconds())
+          break
+        case 'Disabled':
+        default:
+          clearRefreshInterval()
+          break
+      }
+    }
+
+    const rowCount = computed(() => jobStore.getRows)
+    const columnCount = computed(() => jobStore.getColumns)
+
     return {
       t,
+      mittBus,
       activeButton,
       setActiveButton,
-      isDarkMode,
+      handleRefreshData,
+      jobStatus,
+      currentStopIcon,
+      isButtonDisabled,
+      isScheduleButtonDisabled,
+      handleStopJob,
+      formattedJobStatus,
+      jobStatusColor,
+      dropdownOptions,
+      selectedInterval,
+      handleSelect,
+      columnCount,
+      rowCount,
+      executionTime
     }
   },
   render() {
@@ -95,6 +256,7 @@ export default defineComponent({
               trigger: () => (
                 <n-button
                   text
+                  onClick={this.handleRefreshData}
                   class={styles['table-action-bar-button']}
                   v-slots={{
                     icon: () => <n-icon component={Renew} size="20"></n-icon>,
@@ -114,10 +276,11 @@ export default defineComponent({
               trigger: () => (
                 <n-button
                   text
-                  class={styles['table-action-bar-button']}
-                  style="color:  #D94F4F"
+                  onClick={this.handleStopJob}
+                  disabled={this.isButtonDisabled}
+                  class={this.jobStatus === 'RUNNING' ? 
styles['stop-button-running'] : styles['table-action-bar-button']}
                   v-slots={{
-                    icon: () => <n-icon component={StopOutline} 
size="20"></n-icon>,
+                    icon: () => <n-icon component={this.currentStopIcon} 
size="20"></n-icon>,
                   }}
                 >
                 </n-button>
@@ -126,36 +289,41 @@ export default defineComponent({
           >
             <span>{this.t('playground.stop_job')}</span>
           </n-popover>
-          <n-popover
-            trigger="hover"
+          <n-dropdown
+            trigger="click"
+            size="small"
             placement="bottom-start"
-            show-arrow={false}
+            options={this.dropdownOptions}
+            disabled={this.isScheduleButtonDisabled}
+            v-model:value={this.selectedInterval}
+            on-select={this.handleSelect}
             v-slots={{
               trigger: () => (
                 <n-button
                   text
-                  class={styles['table-action-bar-button']}
-                  v-slots={{
-                    icon: () => <n-icon component={ClockCircleOutlined} 
size="18.5"></n-icon>,
-                  }}
-                >
+                  disabled={this.isScheduleButtonDisabled}
+                  class={styles['table-action-bar-button']}>
                 </n-button>
               ),
+              default: () => (
+                <n-icon
+                  size="20"
+                  class={styles['table-action-bar-button']}
+                  component={ClockCircleOutlined}/>
+              )
             }}
-          >
-            <span>{this.t('playground.schedule_refresh')}</span>
-          </n-popover>
+          />
           <n-divider vertical style="height: 20px; margin-left: 0px; 
margin-right: 0px;" />
-          <span class={styles['table-action-bar-text']}>4 Columns</span>
+          <span class={styles['table-action-bar-text']}>{this.columnCount} 
Columns</span>
         </n-space>
         <div class={styles.right}>
           <n-space item-style="display: flex; align-items: center;">
             <div class={styles['table-action-bar-text']}>
               Job:
-              <span style="color: #33994A"> Running</span>
+              <span style={{color: this.jobStatusColor}}> 
{this.formattedJobStatus}</span>
             </div>
-            <span class={styles['table-action-bar-text']}>Rows: 3</span>
-            <span class={styles['table-action-bar-text']}>1m:06s</span>
+            <span class={styles['table-action-bar-text']}>Rows: 
{this.rowCount}</span>
+            <span class={styles['table-action-bar-text']}>{ this.executionTime 
}</span>
             <n-popover
               trigger="hover"
               placement="bottom-start"
@@ -164,6 +332,7 @@ export default defineComponent({
                 trigger: () => (
                   <n-button
                     text
+                    onClick={() => this.mittBus.emit('triggerDownloadCsv')}
                     class={styles['table-action-bar-button']}
                     v-slots={{
                       icon: () => <n-icon component={DownloadOutlined} 
size="20"></n-icon>,
@@ -183,6 +352,7 @@ export default defineComponent({
                 trigger: () => (
                   <n-button
                     text
+                    onClick={() => this.mittBus.emit('triggerCopyData')}
                     class={styles['table-action-bar-button']}
                     v-slots={{
                       icon: () => <n-icon component={Copy} size="20"></n-icon>,
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/table/index.tsx
 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/table/index.tsx
index 14e69a3d..ed195933 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/table/index.tsx
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/table/index.tsx
@@ -16,67 +16,97 @@ specific language governing permissions and limitations
 under the License. */
 
 import styles from './index.module.scss'
+import type { DataTableInst } from 'naive-ui'
+import { useMessage } from "naive-ui"
+import { useJobStore } from '@/store/job'
 
 export default defineComponent({
   name: 'TableResult',
-  setup() {
-    const columns = [
-      {
+  setup: function (props, {emit}) {
+    const {t} = useLocaleHooks()
+    const message = useMessage()
+    const jobStore = useJobStore()
+
+    const tableRef = ref<DataTableInst | null>(null)
+
+    interface TableColumn {
+      title: string
+      key: string
+      fixed?: string
+      width?: number
+      render?: (row: any, index: number) => string | number | JSX.Element
+    }
+
+    const initialData = computed(() => jobStore.getCurrentJob?.resultData || 
[]);
+    const refreshedData = computed(() => jobStore.getJobResultData?.resultData 
|| []);
+    const data = computed(() => refreshedData.value.length > 0 ? 
refreshedData.value : initialData.value);
+
+    const columns = computed(() => {
+      if (data.value.length > 0) {
+        return generateColumns(data.value[0]);
+      }
+      return [];
+    });
+
+    const {mittBus} = getCurrentInstance()!.appContext.config.globalProperties
+
+    const generateColumns = (sampleObject: any) => {
+      const indexColumn: TableColumn = {
         title: '#',
-        key: 'key',
-        render: (_: any, index: number) => {
-          return `${index + 1}`
-        },
-      },
-      {
-        title: 'id',
-        key: 'id',
-        resizable: true,
-      },
-      {
-        title: 'name',
-        key: 'name',
-        resizable: true,
-      },
-      {
-        title: 'age',
-        key: 'age',
-        resizable: true,
-      },
-      {
-        title: 'address',
-        key: 'address',
+        key: 'index',
+        fixed: 'left',
+        width: 50,
+        render: (row, index) => `${index + 1}`
+      }
+
+      const dynamicColumns = Object.keys(sampleObject).map(key => ({
+        title: key,
+        key: key,
         resizable: true,
-      },
-    ]
-
-    interface User {
-      id: number
-      name: string
-      age: number
-      address: string
+        sortable: true
+      }))
+
+      return [indexColumn, ...dynamicColumns];
     }
 
-    const data: User[] = [
-      { id: 1, name: 'jack', age: 36, address: 'beijing' },
-      { id: 2, name: 'li hua', age: 38, address: 'shanghai' },
-      { id: 3, name: 'zhao ming', age: 27, address: 'hangzhou' },
-      { id: 3, name: 'zhao ming', age: 27, address: 'hangzhou' },
-    ]
+    mittBus.on('triggerDownloadCsv', () => {
+      if (tableRef.value) {
+        tableRef.value.downloadCsv({fileName: 'data-table'})
+      }
+    })
+
+    mittBus.on('triggerCopyData', () => {
+      if (data.value && data.value.length > 0) {
+        const jsonData = JSON.stringify(data.value, null, 2)
+        navigator.clipboard.writeText(jsonData)
+          .then(() => 
message.success(t('playground.data_copied_successfully')))
+          .catch(err => console.error('Failed to copy data: ', err))
+      }
+    })
+
+    onUnmounted(() => {
+      mittBus.off('triggerDownloadCsv')
+      mittBus.off('triggerCopyData')
+    });
 
     return {
       columns,
       data,
+      tableRef,
     }
   },
   render() {
     return (
       <div>
         <n-data-table
+          ref={(el: any) => { this.tableRef = el }}
           class={styles.table}
           columns={this.columns}
           data={this.data}
-          max-height={138}
+          max-height={90}
+          v-slots={{
+            empty: () => '',
+          }}
         />
       </div>
     )
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/debugger/index.tsx
 
b/paimon-web-ui/src/views/playground/components/query/components/debugger/index.tsx
index d647cb58..1386c6b1 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/debugger/index.tsx
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/debugger/index.tsx
@@ -16,46 +16,49 @@ specific language governing permissions and limitations
 under the License. */
 
 import { ChevronDown, Play, ReaderOutline, Save } from '@vicons/ionicons5'
+import { getClusterListByType } from '@/api/models/cluster'
 import styles from './index.module.scss'
+import type { Cluster } from "@/api/models/cluster/types"
+import type {JobSubmitDTO} from "@/api/models/job/types/job"
+import { submitJob } from "@/api/models/job"
+import { useMessage } from "naive-ui"
+import { useJobStore } from '@/store/job'
+import type {ExecutionMode} from "@/store/job/type"
 
 export default defineComponent({
   name: 'EditorDebugger',
   emits: ['handleFormat', 'handleSave'],
   setup(props, { emit }) {
     const { t } = useLocaleHooks()
+    const message = useMessage()
+    const jobStore = useJobStore()
 
-    const debuggerVariables = reactive({
+    const tabData = ref({}) as any
+
+    const debuggerVariables = reactive<{
+      operatingConditionOptions: { label: string; key: string }[]
+      conditionValue: string
+      bigDataOptions: { label: string; value: string }[]
+      conditionValue2: string
+      clusterOptions: { label: string; value: string }[]
+      conditionValue3: string
+      executionModeOptions: { label: string; value: string }[]
+    }>({
       operatingConditionOptions: [
-        {
-          label: 'Limit 100 items',
-          key: '100',
-        },
-        {
-          label: 'Limit 1000 items',
-          key: '1000',
-        },
+        { label: 'Limit 100 items', key: '100' },
+        { label: 'Limit 1000 items', key: '1000' },
       ],
       conditionValue: 'Flink',
       bigDataOptions: [
-        {
-          label: 'Flink',
-          value: 'Flink',
-        },
-        {
-          label: 'Spark',
-          value: 'Spark',
-        },
+        { label: 'Flink', value: 'Flink' },
+        { label: 'Spark', value: 'Spark' },
       ],
-      conditionValue2: 'test1',
-      clusterOptions: [
-        {
-          label: 'test1',
-          value: 'test1',
-        },
-        {
-          label: 'test2',
-          value: 'test2',
-        },
+      conditionValue2: '',
+      clusterOptions: [],
+      conditionValue3: 'Streaming',
+      executionModeOptions: [
+        { label: 'Streaming', value: 'Streaming' },
+        { label: 'Batch', value: 'Batch' },
       ],
     })
 
@@ -71,12 +74,78 @@ export default defineComponent({
       emit('handleSave')
     }
 
+    function getClusterData() {
+      getClusterListByType(debuggerVariables.conditionValue, 1, 
Number.MAX_SAFE_INTEGER).then(response => {
+        if (response && response.data) {
+          const clusterList = response.data as Cluster[]
+          debuggerVariables.clusterOptions = clusterList.map(cluster => ({
+            label: cluster.clusterName,
+            value: cluster.id.toString()
+          }))
+          if (debuggerVariables.clusterOptions.length > 0) {
+            debuggerVariables.conditionValue2 = 
debuggerVariables.clusterOptions[0].value
+          }
+        }
+      }).catch(error => {
+        console.error('Failed to fetch clusters:', error)
+      })
+    }
+
+    watch(() => debuggerVariables.conditionValue, (newValue) => {
+      getClusterData()
+    })
+
+    onMounted(() => {getClusterData()})
+
+    const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
+    mittBus.on('initTabData', (data: any) => {
+      tabData.value = data
+    });
+
+    const handleSubmit = async () => {
+      const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
+
+      if (!currentTab) {
+        return
+      }
+
+      jobStore.setExecutionMode(debuggerVariables.conditionValue3 as 
ExecutionMode)
+      jobStore.resetCurrentResult()
+
+      const currentSQL = currentTab.content
+      if (!currentSQL) {
+        return
+      }
+
+      const jobDataDTO: JobSubmitDTO = {
+        jobName: currentTab.tableName,
+        taskType: debuggerVariables.conditionValue,
+        clusterId: debuggerVariables.conditionValue2,
+        statements: currentSQL,
+        streaming: debuggerVariables.conditionValue3 === 'Streaming'
+      };
+
+      try {
+        const response = await submitJob(jobDataDTO);
+        if (response.code === 200) {
+          message.success(t('playground.job_submission_successfully'))
+          jobStore.setCurrentJob(response.data)
+          mittBus.emit('jobResult', response.data);
+        } else {
+          message.error(`${t('playground.job_submission_failed')}`)
+        }
+      } catch (error) {
+        console.error('Failed to submit job:', error)
+      }
+    }
+
     return {
       t,
       ...toRefs(debuggerVariables),
       handleSelect,
       handleFormat,
       handleSave,
+      handleSubmit
     }
   },
   render() {
@@ -85,6 +154,7 @@ export default defineComponent({
         <n-space>
           <n-button
             type="primary"
+            onClick={this.handleSubmit}
             v-slots={{
               icon: () => <n-icon component={Play} />,
               default: () => {
@@ -103,6 +173,7 @@ export default defineComponent({
           </n-button>
           <n-select style="width:160px;" v-model:value={this.conditionValue} 
options={this.bigDataOptions} />
           <n-select style="width:160px;" v-model:value={this.conditionValue2} 
options={this.clusterOptions} />
+          <n-select style="width:160px;" v-model:value={this.conditionValue3} 
options={this.executionModeOptions} />
         </n-space>
         <div class={styles.operations}>
           <n-space>
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/tabs/index.tsx 
b/paimon-web-ui/src/views/playground/components/query/components/tabs/index.tsx
index 052483f3..62ea32a4 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/tabs/index.tsx
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/tabs/index.tsx
@@ -17,6 +17,7 @@ under the License. */
 
 import styles from './index.module.scss'
 import ContextMenu from '@/components/context-menu'
+import dayjs from 'dayjs'
 
 export default defineComponent({
   name: 'EditorTabs',
@@ -30,13 +31,14 @@ export default defineComponent({
     })
 
     const handleAdd = () => {
+      const timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss')
       tabVariables.panelsList.push({
-        tableName: `test${tabVariables.panelsList.length + 1}`,
-        key: `test${tabVariables.panelsList.length + 1}`,
+        tableName: timestamp,
+        key: timestamp,
         isSaved: false,
         content: '',
       })
-      tabVariables.chooseTab = `test${tabVariables.panelsList.length}`
+      tabVariables.chooseTab = timestamp
     }
 
     const handleClose = (key: any) => {
diff --git a/paimon-web-ui/src/views/playground/components/query/index.tsx 
b/paimon-web-ui/src/views/playground/components/query/index.tsx
index 3125eca7..8c7ab808 100644
--- a/paimon-web-ui/src/views/playground/components/query/index.tsx
+++ b/paimon-web-ui/src/views/playground/components/query/index.tsx
@@ -24,11 +24,19 @@ import EditorTabs from './components/tabs'
 import EditorDebugger from './components/debugger'
 import EditorConsole from './components/console'
 import MonacoEditor from '@/components/monaco-editor'
+import { useJobStore } from '@/store/job'
+import {getJobStatus} from "@/api/models/job"
 
 export default defineComponent({
   name: 'QueryPage',
   setup() {
     const message = useMessage()
+    const jobStore = useJobStore()
+
+    const startTime = ref(0)
+    const elapsedTime = ref(0)
+    const currentJob = computed(() => jobStore.getCurrentJob)
+    const jobStatus = computed(() => jobStore.getJobStatus)
 
     const editorVariables = reactive({
       editor: {} as any,
@@ -85,6 +93,58 @@ export default defineComponent({
       tabData.value = data
     })
 
+    let getJobStatusIntervalId: number
+    onMounted(() => {
+      getJobStatusIntervalId = setInterval(async () => {
+        if (currentJob.value && currentJob.value.jobId) {
+          const response = await getJobStatus(currentJob.value.jobId)
+          if (response.data) {
+            jobStore.setJobStatus(response.data.status)
+          }
+        }
+      }, 1000)
+    })
+
+    let computeExecutionTimeIntervalId: number
+    const startTimer = () => {
+      if (computeExecutionTimeIntervalId) {
+        clearInterval(computeExecutionTimeIntervalId)
+      }
+      elapsedTime.value = 0
+      startTime.value = Date.now()
+      computeExecutionTimeIntervalId = setInterval(() => {
+        elapsedTime.value = Math.floor((Date.now() - startTime.value) / 1000)
+      }, 3000)
+    }
+
+    const stopTimer = ()=> {
+      if (computeExecutionTimeIntervalId) {
+        clearInterval(computeExecutionTimeIntervalId)
+      }
+    }
+
+    watch(jobStatus, (newStatus, oldStatus) => {
+      if (newStatus === 'RUNNING' && oldStatus !== 'RUNNING') {
+        startTimer();
+      } else if (newStatus !== 'RUNNING' && oldStatus === 'RUNNING') {
+        stopTimer();
+        elapsedTime.value = Math.floor((Date.now() - startTime.value) / 1000)
+      }
+    })
+
+    const formatTime =  (seconds: number): string  => {
+      const days = Math.floor(seconds / 86400)
+      const hours = Math.floor((seconds % 86400) / 3600)
+      const mins = Math.floor((seconds % 3600) / 60)
+      const secs = seconds % 60
+      return `${days > 0 ? `${days}d:` : ''}${hours > 0 || days > 0 ? 
`${hours}h:` : ''}${mins}m:${secs}s`
+    }
+
+    const formattedTime = computed(() => formatTime(elapsedTime.value))
+    watch(formattedTime, (formattedTime) => 
jobStore.setExecutionTime(formattedTime))
+
+    onUnmounted(() => jobStore.resetCurrentResult())
+
     return {
       ...toRefs(editorVariables),
       editorMounted,


Reply via email to