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 565b82b5 [Improvement] Refactor frontend of job query page (#442)
565b82b5 is described below

commit 565b82b551873c9ca75c85af470108ae1b3e5863
Author: s7monk <[email protected]>
AuthorDate: Mon Jun 24 18:59:06 2024 +0800

    [Improvement] Refactor frontend of job query page (#442)
---
 paimon-web-ui/{src/store/job/type.ts => .env}      |   2 +-
 paimon-web-ui/licenses/LICENSE-@stomp-stompjs      |  13 ++
 paimon-web-ui/package.json                         |   1 +
 paimon-web-ui/src/api/models/job/types/job.ts      |   2 +
 paimon-web-ui/src/composables/useWebSocket.ts      |  93 ++++++++
 paimon-web-ui/src/store/job/index.ts               | 148 ++++++++----
 paimon-web-ui/src/store/job/type.ts                |  13 ++
 .../console/components/controls/index.tsx          |  43 +++-
 .../components/console/components/log/index.tsx    |   4 -
 .../components/console/components/table/index.tsx  |  16 +-
 .../components/query/components/console/index.tsx  |  32 ++-
 .../components/query/components/debugger/index.tsx |  30 ++-
 .../query/components/tabs/index.module.scss        |  57 ++++-
 .../components/query/components/tabs/index.tsx     | 113 +++++++++
 .../playground/components/query/index.module.scss  |  35 +--
 .../views/playground/components/query/index.tsx    | 253 ++++++---------------
 16 files changed, 534 insertions(+), 321 deletions(-)

diff --git a/paimon-web-ui/src/store/job/type.ts b/paimon-web-ui/.env
similarity index 93%
copy from paimon-web-ui/src/store/job/type.ts
copy to paimon-web-ui/.env
index 13ea0f54..1d7281fe 100644
--- a/paimon-web-ui/src/store/job/type.ts
+++ b/paimon-web-ui/.env
@@ -15,4 +15,4 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-export type ExecutionMode = 'Streaming' | 'Batch'
+VITE_WS_URL=ws://127.0.0.1:10088/ws
\ No newline at end of file
diff --git a/paimon-web-ui/licenses/LICENSE-@stomp-stompjs 
b/paimon-web-ui/licenses/LICENSE-@stomp-stompjs
new file mode 100644
index 00000000..de46877c
--- /dev/null
+++ b/paimon-web-ui/licenses/LICENSE-@stomp-stompjs
@@ -0,0 +1,13 @@
+Copyright 2018-2020 Deepak Kumar <[email protected]>
+
+Licensed 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.
\ No newline at end of file
diff --git a/paimon-web-ui/package.json b/paimon-web-ui/package.json
index fc115b3d..71e91ea2 100644
--- a/paimon-web-ui/package.json
+++ b/paimon-web-ui/package.json
@@ -25,6 +25,7 @@
     "@antv/x6": "^2.15.3",
     "@antv/x6-plugin-dnd": "^2.1.1",
     "@antv/x6-vue-shape": "^2.1.1",
+    "@stomp/stompjs": "^7.0.0",
     "dart-sass": "^1.25.0",
     "dayjs": "^1.11.11",
     "highlight.js": "^11.9.0",
diff --git a/paimon-web-ui/src/api/models/job/types/job.ts 
b/paimon-web-ui/src/api/models/job/types/job.ts
index c327a771..1a1ec2e0 100644
--- a/paimon-web-ui/src/api/models/job/types/job.ts
+++ b/paimon-web-ui/src/api/models/job/types/job.ts
@@ -19,6 +19,7 @@ export interface Job {
   submitId: string
   jobId: string
   jobName: string
+  fileName: string
   type: string
   executeMode: string
   clusterId: string
@@ -34,6 +35,7 @@ export interface Job {
 
 export interface JobSubmitDTO {
   jobName: string
+  fileName: string
   taskType: string
   clusterId: string
   config?: {
diff --git a/paimon-web-ui/src/composables/useWebSocket.ts 
b/paimon-web-ui/src/composables/useWebSocket.ts
new file mode 100644
index 00000000..8899054d
--- /dev/null
+++ b/paimon-web-ui/src/composables/useWebSocket.ts
@@ -0,0 +1,93 @@
+/* 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 { Stomp } from '@stomp/stompjs'
+import { onUnmounted, ref } from 'vue'
+import type { Ref } from 'vue'
+import type { Client, Frame, IMessage } from '@stomp/stompjs'
+
+interface WebSocketOptions {
+  onOpen?: (frame: Frame) => void
+  onMessage?: (message: IMessage, topic: string) => void
+  onError?: (event: Event) => void
+  onClose?: (event: CloseEvent) => void
+}
+
+export function useWebSocket(url: string, options?: WebSocketOptions) {
+  const client: Ref<Client | null> = ref(null)
+  const subscriptions: Ref<Map<string, { unsubscribe: () => void }>> = ref(new 
Map())
+
+  const connect = (): void => {
+    const stompClient = Stomp.client(url)
+    stompClient.reconnect_delay = 1000
+    stompClient.heartbeatIncoming = 30000
+    stompClient.heartbeatOutgoing = 30000
+
+    stompClient.onConnect = (frame: Frame) => {
+      options?.onOpen?.(frame)
+    }
+
+    stompClient.onStompError = (frame: Frame) => {
+      console.error('Broker reported error:', frame.headers.message)
+      console.error('Additional details:', frame.body)
+    }
+
+    stompClient.activate()
+    client.value = stompClient
+  }
+
+  const subscribe = (topic: string): void => {
+    if (client.value && client.value.connected) {
+      const subscription = client.value.subscribe(topic, (message: IMessage) 
=> {
+        options?.onMessage?.(message, topic)
+      })
+      subscriptions.value.set(topic, subscription)
+    }
+    else {
+      console.error('STOMP client is not connected.')
+    }
+  }
+
+  const unsubscribe = (topic: string): void => {
+    const subscription = subscriptions.value.get(topic)
+    if (subscription) {
+      subscription.unsubscribe()
+      subscriptions.value.delete(topic)
+    }
+  }
+
+  const sendMessage = (destination: string, body: string, headers = {}): void 
=> {
+    if (client.value && client.value.connected) {
+      client.value.publish({ destination, headers, body })
+    }
+    else {
+      console.error('STOMP client is not connected.')
+    }
+  }
+
+  const closeConnection = (): void => {
+    subscriptions.value.forEach(sub => sub.unsubscribe())
+    subscriptions.value.clear()
+    client.value?.deactivate()
+  }
+
+  onUnmounted(() => {
+    closeConnection()
+  })
+
+  return { connect, subscribe, unsubscribe, sendMessage, closeConnection }
+}
diff --git a/paimon-web-ui/src/store/job/index.ts 
b/paimon-web-ui/src/store/job/index.ts
index 3a313abb..6dbeb8d6 100644
--- a/paimon-web-ui/src/store/job/index.ts
+++ b/paimon-web-ui/src/store/job/index.ts
@@ -15,89 +15,137 @@ 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 { ExecutionMode, JobDetails } 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
+  jobs: Record<string, JobDetails>
   jobLog?: string
 }
 
 export const useJobStore = defineStore({
   id: 'job',
   state: (): JobState => ({
-    executionMode: 'Streaming',
-    currentJob: null,
-    jobResultData: null,
-    jobStatus: '',
-    executionTime: '0m:0s',
+    jobs: {},
     jobLog: '',
   }),
   persist: false,
   getters: {
-    getExecutionMode(): ExecutionMode {
-      return this.executionMode
+    getJobDetails(state): (key: string) => JobDetails | undefined {
+      return key => state.jobs[key]
     },
-    getCurrentJob(): Job | null {
-      return this.currentJob
+    getCurrentJob(state): (key: string) => Job | null {
+      return key => state.jobs[key]?.job
     },
-    getJobResultData(): JobResultData | null {
-      return this.jobResultData
+    getExecutionMode(state): (key: string) => string | undefined {
+      return key => state.jobs[key]?.executionMode
     },
-    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
+    getJobStatus(state): (key: string) => string {
+      return (key) => {
+        if (!state.jobs[key]) {
+          return 'UNKNOWN'
+        }
+        return state.jobs[key].jobStatus
+      }
     },
-    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
+    getJobResultData(state): (key: string) => JobResultData | null {
+      return key => state.jobs[key]?.jobResultData
     },
-    getJobStatus(): string {
-      return this.jobStatus
+    getExecutionTime(state): (key: string) => number {
+      return (key) => {
+        if (!state.jobs[key]) {
+          return 0
+        }
+        return state.jobs[key].executionTime
+      }
+    },
+    getColumns(state): (key: string) => number {
+      return (key) => {
+        const initResultData = state.jobs[key]?.job?.resultData
+        const refreshedResultData = state.jobs[key]?.jobResultData?.resultData
+        if (initResultData && initResultData.length > 0) {
+          return Object.keys(initResultData[0]).length
+        }
+        if (refreshedResultData && refreshedResultData.length > 0) {
+          return Object.keys(refreshedResultData[0]).length
+        }
+        return 0
+      }
     },
-    getExecutionTime(): string {
-      return this.executionTime
+    getRows(state): (key: string) => number {
+      return (key) => {
+        const initResultData = state.jobs[key]?.job?.resultData
+        const refreshedResultData = state.jobs[key]?.jobResultData?.resultData
+        if (initResultData) {
+          return initResultData.length
+        }
+        if (refreshedResultData) {
+          return refreshedResultData.length
+        }
+        return 0
+      }
     },
     getJobLog(): string | undefined {
       return this.jobLog
     },
   },
   actions: {
-    setExecutionMode(executionMode: ExecutionMode) {
-      this.executionMode = executionMode
+    addJob(key: string, jobDetails: JobDetails) {
+      this.jobs[key] = jobDetails
     },
-    setCurrentJob(currentJob: Job) {
-      this.currentJob = currentJob
+    updateJobStatus(key: string, jobStatus: string) {
+      if (this.jobs[key]) {
+        this.jobs[key].jobStatus = jobStatus
+      }
     },
-    setJobResultData(jobResultData: JobResultData) {
-      this.jobResultData = jobResultData
+    updateExecutionMode(key: string, executionMode: ExecutionMode) {
+      if (this.jobs[key]) {
+        this.jobs[key].executionMode = executionMode
+      }
     },
-    setJobStatus(jobStatus: string) {
-      this.jobStatus = jobStatus
+    updateJob(key: string, currentJob: Job) {
+      if (this.jobs[key]) {
+        this.jobs[key].job = currentJob
+      }
     },
-    setExecutionTime(executionTime: string) {
-      this.executionTime = executionTime
+    updateJobResultData(key: string, jobResultData: JobResultData) {
+      if (this.jobs[key]) {
+        this.jobs[key].jobResultData = jobResultData
+      }
     },
     setJobLog(jobLog: string) {
       this.jobLog = jobLog
     },
-    resetCurrentResult() {
-      this.currentJob = null
-      this.jobResultData = null
-      this.jobStatus = ''
-      this.executionTime = '0m:0s'
+    startJobTimer(key: string) {
+      if (this.jobs[key]) {
+        this.jobs[key].timerId = window.setInterval(() => {
+          this.jobs[key].executionTime = Math.floor((Date.now() - 
this.jobs[key].startTime) / 1000)
+        }, 3000)
+      }
+    },
+    stopJobTimer(key: string) {
+      if (this.jobs[key] && this.jobs[key].timerId) {
+        clearInterval(this.jobs[key].timerId)
+        this.jobs[key].executionTime = Math.floor((Date.now() - 
this.jobs[key].startTime) / 1000)
+      }
+    },
+    resetJob(key: string) {
+      if (this.jobs[key]) {
+        this.jobs[key] = {
+          executionMode: 'Streaming',
+          job: null,
+          jobResultData: null,
+          jobStatus: '',
+          executionTime: 0,
+          startTime: 0,
+          displayResult: false,
+        }
+      }
+    },
+    removeJob(key: string) {
+      if (this.jobs[key]) {
+        delete this.jobs[key]
+      }
     },
   },
 })
diff --git a/paimon-web-ui/src/store/job/type.ts 
b/paimon-web-ui/src/store/job/type.ts
index 13ea0f54..923867ac 100644
--- a/paimon-web-ui/src/store/job/type.ts
+++ b/paimon-web-ui/src/store/job/type.ts
@@ -15,4 +15,17 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
+import type { Job, JobResultData } from '@/api/models/job/types/job'
+
 export type ExecutionMode = 'Streaming' | 'Batch'
+
+export interface JobDetails {
+  executionMode: ExecutionMode
+  job: Job | null
+  jobResultData: JobResultData | null
+  jobStatus: string
+  executionTime: number
+  timerId?: number
+  startTime: number
+  displayResult: boolean
+}
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 a1ed454a..32289358 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
@@ -27,16 +27,25 @@ import { useJobStore } from '@/store/job'
 
 export default defineComponent({
   name: 'TableActionBar',
-  setup() {
+  props: {
+    tabData: {
+      type: Object as PropType<any>,
+      default: () => ({}),
+    },
+  },
+  setup(props) {
     const { t } = useLocaleHooks()
     const message = useMessage()
     const jobStore = useJobStore()
-
     const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
-
-    const currentJob = computed(() => jobStore.getCurrentJob)
-    const jobStatus = computed(() => jobStore.getJobStatus)
-    const executionTime = computed(() => jobStore.getExecutionTime)
+    const tabData = toRef(props.tabData)
+    const currentKey = computed(() => {
+      const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
+      return currentTab ? currentTab.key : null
+    })
+    const currentJob = computed(() => jobStore.getCurrentJob(currentKey.value))
+    const jobStatus = computed(() => jobStore.getJobStatus(currentKey.value))
+    const executionTime = computed(() => 
formatTime(jobStore.getExecutionTime(currentKey.value)))
     const selectedInterval = ref('Disabled')
     const refreshIntervalId = ref<number | null>(null)
     const activeButton = ref('table')
@@ -59,7 +68,7 @@ export default defineComponent({
               token,
             }
             const response = await fetchResult(resultFetchDTO)
-            jobStore.setJobResultData(response.data)
+            jobStore.updateJobResultData(currentKey.value, response.data)
           }
           catch (error) {
             console.error('Error fetching result:', error)
@@ -86,10 +95,12 @@ export default defineComponent({
         }
         try {
           const response = await stopJob(stopJobDTO)
-          if (response.code === 200)
+          if (response.code === 200) {
             message.success(t('playground.job_stopping_successfully'))
-          else
+          }
+          else {
             message.warning(t('playground.job_stopping_failed'))
+          }
         }
         catch (error) {
           message.warning(t('playground.job_stopping_failed'))
@@ -104,7 +115,7 @@ export default defineComponent({
     })
 
     const isScheduleButtonDisabled = computed(() => {
-      return jobStore.getExecutionMode === 'Batch'
+      return jobStore.getExecutionMode(currentKey.value) === 'Batch'
     })
 
     const jobStatusColor = computed(() => {
@@ -180,8 +191,16 @@ export default defineComponent({
       }
     }
 
-    const rowCount = computed(() => jobStore.getRows)
-    const columnCount = computed(() => jobStore.getColumns)
+    function 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 rowCount = computed(() => jobStore.getRows(currentKey.value))
+    const columnCount = computed(() => jobStore.getColumns(currentKey.value))
 
     return {
       t,
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.tsx
 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.tsx
index 40278c2e..2cf779ac 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.tsx
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.tsx
@@ -58,10 +58,6 @@ export default defineComponent({
       })
     })
 
-    onUnmounted(() => {
-      mittBus.off('resizeLog')
-    })
-
     return {
       logContent,
       maxLogHeight,
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 bad61638..2b4c157c 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
@@ -27,11 +27,21 @@ export default defineComponent({
       type: Number as PropType<number>,
       default: 150,
     },
+    tabData: {
+      type: Object as PropType<any>,
+      default: () => ({}),
+    },
   },
   setup(props) {
     const { t } = useLocaleHooks()
     const message = useMessage()
+    const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
     const jobStore = useJobStore()
+    const tabData = toRef(props.tabData)
+    const currentKey = computed(() => {
+      const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
+      return currentTab ? currentTab.key : null
+    })
     const scrollX = ref('100%')
     const tableContainer = ref<HTMLElement | null>(null)
     const tableRef = ref<DataTableInst | null>(null)
@@ -46,8 +56,8 @@ export default defineComponent({
       render?: (row: any, index: number) => string | number | JSX.Element
     }
 
-    const initialData = computed(() => jobStore.getCurrentJob?.resultData || 
[])
-    const refreshedData = computed(() => jobStore.getJobResultData?.resultData 
|| [])
+    const initialData = computed(() => 
jobStore.getCurrentJob(currentKey.value)?.resultData || [])
+    const refreshedData = computed(() => 
jobStore.getJobResultData(currentKey.value)?.resultData || [])
     const data = computed(() => refreshedData.value.length > 0 ? 
refreshedData.value : initialData.value)
 
     const columns = computed(() => {
@@ -57,8 +67,6 @@ export default defineComponent({
       return []
     })
 
-    const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
-
     function generateColumns(sampleObject: any) {
       const indexColumn: TableColumn = {
         title: '#',
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/index.tsx
 
b/paimon-web-ui/src/views/playground/components/query/components/console/index.tsx
index c3ad0284..93334680 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/index.tsx
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/index.tsx
@@ -21,16 +21,29 @@ import TableActionBar from './components/controls'
 import TableResult from './components/table'
 import LogConsole from './components/log'
 import styles from './index.module.scss'
+import { useJobStore } from '@/store/job'
 
 export default defineComponent({
   name: 'EditorConsole',
   emits: ['ConsoleUp', 'ConsoleDown', 'ConsoleClose'],
+  props: {
+    tabData: {
+      type: Object as PropType<any>,
+      default: () => ({}),
+    },
+  },
   setup(props, { emit }) {
     const { t } = useLocaleHooks()
-    const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
     const editorConsoleRef = ref<HTMLElement | null>(null)
     const adjustedHeight = ref(0)
-    const displayResult = ref(false)
+    const tabData = toRef(props.tabData)
+    const currentKey = computed(() => {
+      const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
+      return currentTab ? currentTab.key : null
+    })
+    const jobStore = useJobStore()
+    const displayResult = computed(() => 
jobStore.getJobDetails(currentKey.value)?.displayResult)
+    const jobStatus = computed(() => jobStore.getJobStatus(currentKey.value))
 
     const handleUp = () => {
       emit('ConsoleUp', 'up')
@@ -44,8 +57,16 @@ export default defineComponent({
       emit('ConsoleClose', 'close')
     }
 
-    mittBus.on('displayResult', () => displayResult.value = true)
+    watch(jobStatus, (newStatus, oldStatus) => {
+      if (newStatus === 'RUNNING' && oldStatus !== 'RUNNING') {
+        jobStore.startJobTimer(currentKey.value)
+      }
+      else if (newStatus !== 'RUNNING' && oldStatus === 'RUNNING') {
+        jobStore.stopJobTimer(currentKey.value)
+      }
+    })
 
+    // handle resize
     const handleResize = throttle((entries) => {
       for (const entry of entries) {
         const { height } = entry.contentRect
@@ -74,6 +95,7 @@ export default defineComponent({
       editorConsoleRef,
       adjustedHeight,
       displayResult,
+      tabData,
     }
   },
   render() {
@@ -93,8 +115,8 @@ export default defineComponent({
             {
               this.displayResult
               && [
-                <TableActionBar />,
-                <TableResult maxHeight={this.adjustedHeight} />,
+                <TableActionBar tabData={this.tabData} />,
+                <TableResult maxHeight={this.adjustedHeight} 
tabData={this.tabData} />,
               ]
             }
           </n-tab-pane>
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 b20a9f96..d9ec49c4 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
@@ -26,7 +26,6 @@ import type { JobSubmitDTO } from '@/api/models/job/types/job'
 import { createRecord, stopJob, submitJob } from '@/api/models/job'
 import { useJobStore } from '@/store/job'
 
-import type { ExecutionMode } from '@/store/job/type'
 import type { RecordDTO } from '@/api/models/job/types/record'
 
 export default defineComponent({
@@ -47,8 +46,18 @@ export default defineComponent({
     const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
     const statementName = ref<string>('')
     const tabData = toRef(props.tabData)
-    const currentJob = computed(() => jobStore.getCurrentJob)
-    const jobStatus = computed(() => jobStore.getJobStatus)
+    const currentKey = computed(() => {
+      const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
+      return currentTab ? currentTab.key : null
+    })
+    const jobStatus = ref('')
+    const currentJob = computed(() => jobStore.getCurrentJob(currentKey.value))
+    watchEffect(() => {
+      const key = currentKey.value
+      if (key !== null) {
+        jobStatus.value = jobStore.getJobStatus(key)
+      }
+    })
 
     const debuggerVariables = reactive<{
       operatingConditionOptions: { label: string, key: string }[]
@@ -90,7 +99,6 @@ export default defineComponent({
 
     async function handleSave() {
       const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
-
       if (!currentTab)
         return
 
@@ -182,10 +190,12 @@ export default defineComponent({
         }
         try {
           const response = await stopJob(stopJobDTO)
-          if (response.code === 200)
+          if (response.code === 200) {
             message.success(t('playground.job_stopping_successfully'))
-          else
+          }
+          else {
             message.warning(t('playground.job_stopping_failed'))
+          }
         }
         catch (error) {
           message.warning(t('playground.job_stopping_failed'))
@@ -203,8 +213,8 @@ export default defineComponent({
         handleStopJob()
       }
       else {
-        jobStore.setExecutionMode(debuggerVariables.conditionValue3 as 
ExecutionMode)
-        jobStore.resetCurrentResult()
+        if (jobStore.getJobDetails(currentKey.value))
+          jobStore.resetJob(currentKey.value)
 
         const currentSQL = currentTab.content
         if (!currentSQL)
@@ -212,6 +222,7 @@ export default defineComponent({
 
         const jobDataDTO: JobSubmitDTO = {
           jobName: currentTab.tableName,
+          fileName: currentTab.key,
           taskType: debuggerVariables.conditionValue,
           clusterId: debuggerVariables.conditionValue2,
           statements: currentSQL,
@@ -222,10 +233,7 @@ export default defineComponent({
           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)
-            mittBus.emit('getStatus')
-            mittBus.emit('displayResult')
           }
           else {
             message.error(`${t('playground.job_submission_failed')}`)
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/tabs/index.module.scss
 
b/paimon-web-ui/src/views/playground/components/query/components/tabs/index.module.scss
index 9b67bd79..f05e5766 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/tabs/index.module.scss
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/tabs/index.module.scss
@@ -15,21 +15,54 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-
-.tabs {
+.container {
   display: flex;
-  align-items: center;
+  flex-direction: column;
+  height: 100%;
+  width: 100%;
+  .tabs {
+    display: flex;
+    align-items: center;
+    .dot {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: #33994A;
+      margin-right: 8px;
+    }
+    .asterisk {
+      color: #C82E2E;
+      padding-left: 10px;
+    }
+  }
 
-  .dot {
-    width: 8px;
-    height: 8px;
-    border-radius: 50%;
-    background-color: #33994A;
-    margin-right: 8px;
+  .debugger {
+    display: flex;
+    height: 60px;
   }
 
-  .asterisk {
-    color: #C82E2E;
-    padding-left: 10px;
+  .editor {
+    height: 100%;
+    flex: 1;
+  }
+  .console {
+    height: 100%;
+    flex: 1;
+  }
+  .console-splitter {
+    position: relative;
+    height: 0;
+    background: transparent;
+    cursor: ns-resize;
+    &::after {
+      content: '';
+      position: absolute;
+      top: -5px;
+      right: 0;
+      bottom: -5px;
+      left: 0;
+      background: transparent;
+      z-index: 1;
+    }
   }
 }
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 0d480bb0..f0b18911 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
@@ -15,14 +15,24 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
+import type * as monaco from 'monaco-editor'
+import { format } from 'sql-formatter'
 import dayjs from 'dayjs'
+import EditorDebugger from '../debugger'
 import styles from './index.module.scss'
 import ContextMenu from '@/components/context-menu'
+import { useJobStore } from '@/store/job'
+import MonacoEditor from '@/components/monaco-editor'
+import EditorConsole from 
'@/views/playground/components/query/components/console'
 
 export default defineComponent({
   name: 'EditorTabs',
   setup() {
+    const message = useMessage()
+    const jobStore = useJobStore()
     const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
+    const editorSize = ref(0.6)
+    const menuTreeRef = ref()
 
     const tabVariables = reactive({
       chooseTab: '',
@@ -30,6 +40,32 @@ export default defineComponent({
       row: {} as any,
     })
 
+    const editorVariables = reactive({
+      editor: {} as any,
+      language: 'sql',
+    })
+
+    const editorMounted = (editor: monaco.editor.IStandaloneCodeEditor) => {
+      editorVariables.editor = editor
+    }
+
+    const handleFormat = () => {
+      
toRaw(editorVariables.editor).setValue(format(toRaw(editorVariables.editor).getValue()))
+    }
+
+    const editorSave = () => {
+      message.success('Save success')
+      tabVariables.panelsList.find((item: any) => item.key === 
tabVariables.chooseTab).content = toRaw(editorVariables.editor).getValue()
+      handleFormat()
+      tabVariables.panelsList.find((item: any) => item.key === 
tabVariables.chooseTab).isSaved = true
+      menuTreeRef.value && menuTreeRef.value?.onLoadRecordData()
+    }
+
+    const handleContentChange = (value: string) => {
+      tabVariables.panelsList.find((item: any) => item.key === 
tabVariables.chooseTab).content = value
+      tabVariables.panelsList.find((item: any) => item.key === 
tabVariables.chooseTab).isSaved = false
+    }
+
     const handleAdd = () => {
       const timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss')
       tabVariables.panelsList.push({
@@ -49,6 +85,7 @@ export default defineComponent({
           tabVariables.chooseTab = tabVariables.panelsList[index - 1].key
         else
           tabVariables.chooseTab = tabVariables.panelsList[index]?.key || ''
+        jobStore.removeJob(key)
       }
     }
 
@@ -100,14 +137,48 @@ export default defineComponent({
       mittBus.emit('initTabData', tabVariables)
     })
 
+    const handleConsoleUp = () => {
+      editorSize.value = 0
+      mittBus.emit('editorResized')
+    }
+    const handleConsoleDown = () => {
+      editorSize.value = 0.6
+      mittBus.emit('editorResized')
+    }
+    const showConsole = ref(true)
+    const handleConsoleClose = () => {
+      editorSize.value = 0.98
+      showConsole.value = false
+    }
+    const handleDragEnd = () => {
+      mittBus.emit('editorResized')
+      mittBus.emit('resizeLog')
+    }
+    mittBus.on('reloadLayout', () => {
+      editorSize.value = 0.6
+      showConsole.value = true
+    })
+
     return {
       ...toRefs(tabVariables),
+      ...toRefs(editorVariables),
       handleAdd,
       handleClose,
       changeTreeChoose,
       openContextMenu,
       ...toRefs(contextMenuVariables),
       handleContextMenuSelect,
+      editorSize,
+      tabVariables,
+      handleDragEnd,
+      editorMounted,
+      editorSave,
+      showConsole,
+      handleContentChange,
+      handleConsoleUp,
+      handleConsoleDown,
+      handleConsoleClose,
+      handleFormat,
     }
   },
   render() {
@@ -118,7 +189,9 @@ export default defineComponent({
           type="card"
           addable
           closable
+          style={{ height: '100%', width: '100%' }}
           tab-style="min-width: 160px;"
+          pane-style="padding-top: 0; height: 100%; width: 100%"
           on-close={this.handleClose}
           on-add={this.handleAdd}
           on-update:value={this.changeTreeChoose}
@@ -139,6 +212,46 @@ export default defineComponent({
                       {!item.isSaved && <div class={styles.asterisk}>*</div>}
                     </div>
                   ),
+                  default: () => [
+                    <div class={styles.debugger}>
+                      <EditorDebugger tabData={this.tabVariables} 
onHandleFormat={this.handleFormat} onHandleSave={this.editorSave} />
+                    </div>,
+                    <div style={{ display: 'flex', flex: 1, flexDirection: 
'column', maxHeight: 'calc(100vh - 177px)', height: 'calc(100vh - 177px)' }}>
+                      <n-split direction="vertical" max={0.6} min={0.00} 
resize-trigger-size={0} v-model:size={this.editorSize} 
on-drag-end={this.handleDragEnd}>
+                        {{
+                          '1': () => (
+                            <div class={styles.editor}>
+                              <n-card content-style="height: 100%;padding: 0;">
+                                <MonacoEditor
+                                  
v-model={this.tabVariables.panelsList.find((item: any) => item.key === 
this.tabVariables.chooseTab).content}
+                                  language={this.language}
+                                  onEditorMounted={this.editorMounted}
+                                  onEditorSave={this.editorSave}
+                                  onChange={this.handleContentChange}
+                                />
+                              </n-card>
+                            </div>
+                          ),
+                          '2': () => (this.showConsole && (
+                            <div class={styles.console}>
+                              <n-card content-style="height: 100%;padding: 0;">
+                                <EditorConsole
+                                  onConsoleDown={this.handleConsoleDown}
+                                  onConsoleUp={this.handleConsoleUp}
+                                  onConsoleClose={this.handleConsoleClose}
+                                  tabData={this.tabVariables}
+                                />
+                              </n-card>
+                            </div>
+                          )
+                          ),
+                          'resize-trigger': () => (
+                            <div class={styles['console-splitter']} />
+                          ),
+                        }}
+                      </n-split>
+                    </div>,
+                  ],
                 }}
               >
               </n-tab-pane>
diff --git 
a/paimon-web-ui/src/views/playground/components/query/index.module.scss 
b/paimon-web-ui/src/views/playground/components/query/index.module.scss
index cbda480c..9af75d8d 100644
--- a/paimon-web-ui/src/views/playground/components/query/index.module.scss
+++ b/paimon-web-ui/src/views/playground/components/query/index.module.scss
@@ -59,41 +59,8 @@ under the License. */
     }
 
     .tabs {
-      height: 41px;
-      width: 100%;
-    }
-  
-    .debugger {
-      display: flex;
-      height: 64px;
-    }
-  
-    .editor {
-      height: 100%;
-      flex: 1;
-    }
-
-    .console {
       height: 100%;
-      flex: 1;
-    }
-
-    .console-splitter {
-      position: relative;
-      height: 0;
-      background: transparent;
-      cursor: ns-resize;
-
-      &::after {
-        content: '';
-        position: absolute;
-        top: -5px;
-        right: 0;
-        bottom: -5px;
-        left: 0;
-        background: transparent;
-        z-index: 1;
-      }
+      width: 100%;
     }
   }
 }
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 5b9a15a2..5a8351e1 100644
--- a/paimon-web-ui/src/views/playground/components/query/index.tsx
+++ b/paimon-web-ui/src/views/playground/components/query/index.tsx
@@ -15,84 +15,30 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-import type * as monaco from 'monaco-editor'
-import { format } from 'sql-formatter'
-import { useMessage } from 'naive-ui'
 import { onMounted } from 'vue'
 import styles from './index.module.scss'
 import MenuTree from './components/menu-tree'
 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, getLogs, refreshJobStatus } from '@/api/models/job'
+import { getLogs, refreshJobStatus } from '@/api/models/job'
 import { createSession } from '@/api/models/session'
+import type { ExecutionMode, JobDetails } from '@/store/job/type'
 
 export default defineComponent({
   name: 'QueryPage',
   setup() {
-    const message = useMessage()
     const jobStore = useJobStore()
     const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
-    const menuTreeRef = ref()
 
-    const tabData = ref({}) as any
-    const startTime = ref(0)
-    const elapsedTime = ref(0)
-    const currentJob = computed(() => jobStore.getCurrentJob)
-    const jobStatus = computed(() => jobStore.getJobStatus)
-    const editorSize = ref(0.6)
-
-    const formattedTime = computed(() => formatTime(elapsedTime.value))
-
-    const editorVariables = reactive({
-      editor: {} as any,
-      language: 'sql',
+    const tabData = ref({
+      panelsList: [],
+      chooseTab: null,
+    }) as any
+    const currentKey = computed(() => {
+      const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
+      return currentTab ? currentTab.key : null
     })
-
-    const editorMounted = (editor: monaco.editor.IStandaloneCodeEditor) => {
-      editorVariables.editor = editor
-    }
-
-    const handleFormat = () => {
-      
toRaw(editorVariables.editor).setValue(format(toRaw(editorVariables.editor).getValue()))
-    }
-
-    const editorSave = () => {
-      message.success('Save success')
-      tabData.value.panelsList.find((item: any) => item.key === 
tabData.value.chooseTab).content = toRaw(editorVariables.editor).getValue()
-      handleFormat()
-      tabData.value.panelsList.find((item: any) => item.key === 
tabData.value.chooseTab).isSaved = true
-
-      menuTreeRef.value && menuTreeRef.value?.onLoadRecordData()
-    }
-
-    const handleContentChange = (value: string) => {
-      tabData.value.panelsList.find((item: any) => item.key === 
tabData.value.chooseTab).content = value
-      tabData.value.panelsList.find((item: any) => item.key === 
tabData.value.chooseTab).isSaved = false
-    }
-
-    const handleConsoleUp = () => {
-      editorSize.value = 0
-      mittBus.emit('editorResized')
-    }
-
-    const handleConsoleDown = () => {
-      editorSize.value = 0.6
-      mittBus.emit('editorResized')
-    }
-
-    const showConsole = ref(true)
-    const handleConsoleClose = () => {
-      editorSize.value = 0.98
-      showConsole.value = false
-    }
-
-    const handleDragEnd = () => {
-      mittBus.emit('editorResized')
-      mittBus.emit('resizeLog')
-    }
+    const currentJob = computed(() => jobStore.getCurrentJob(currentKey.value))
 
     // mitt - handle tab choose
     mittBus.on('initTabData', (data: any) => {
@@ -107,8 +53,6 @@ export default defineComponent({
       }, 1000)
     }
 
-    onMounted(getJobLog)
-
     let createSessionIntervalId: number | undefined
     watch(currentJob, (newJob) => {
       if (newJob && createSessionIntervalId === undefined) {
@@ -137,74 +81,67 @@ export default defineComponent({
       }
     })
 
-    const getJobStatusIntervalId = ref<number | undefined>()
-
-    const stopGetJobStatus = () => {
-      if (getJobStatusIntervalId.value)
-        clearInterval(getJobStatusIntervalId.value)
+    // get job status
+    const wsUrl = import.meta.env.VITE_WS_URL
+    function setupGetJobStatusWebSocket() {
+      const { connect, subscribe } = useWebSocket(wsUrl, {
+        onMessage: (message) => {
+          const data = JSON.parse(message.body)
+          if (data && data.jobId && data.status && data.fileName) {
+            jobStore.updateJobStatus(data.fileName, data.status)
+          }
+        },
+        onOpen: () => {
+          subscribe('/topic/jobStatus')
+        },
+        onError: (event) => {
+          console.error('WebSocket encountered an error:', event)
+        },
+        onClose: () => {
+        },
+      })
+
+      connect()
     }
 
-    const startGetJobStatus = () => {
-      stopGetJobStatus()
-      getJobStatusIntervalId.value = setInterval(async () => {
-        if (currentJob.value && currentJob.value.jobId) {
-          const response = await getJobStatus(currentJob.value.jobId)
-          if (response.data)
-            jobStore.setJobStatus(response.data.status)
-        }
-      }, 1000)
+    function setupSubmitJobWebSocket() {
+      const { connect, subscribe } = useWebSocket(wsUrl, {
+        onMessage: (message) => {
+          const data = JSON.parse(message.body)
+          if (data && data.jobId && data.fileName) {
+            const jobDetail: JobDetails = {
+              executionMode: data.executeMode as ExecutionMode,
+              job: data,
+              jobResultData: null,
+              jobStatus: '',
+              executionTime: 0,
+              startTime: Date.now(),
+              displayResult: true,
+            }
+            jobStore.addJob(data.fileName, jobDetail)
+          }
+        },
+        onOpen: () => {
+          subscribe('/topic/job')
+        },
+        onError: (event) => {
+          console.error('WebSocket encountered an error:', event)
+        },
+        onClose: () => {
+        },
+      })
+
+      connect()
     }
 
-    mittBus.on('getStatus', () => startGetJobStatus())
-    mittBus.on('reloadLayout', () => {
-      editorSize.value = 0.6
-      showConsole.value = true
-    })
-
-    watch(jobStatus, (jobStatus) => {
-      if (jobStatus === 'FINISHED' || jobStatus === 'CANCELED' || jobStatus 
=== 'FAILED')
-        stopGetJobStatus()
+    onMounted(() => {
+      setupGetJobStatusWebSocket()
+      setupSubmitJobWebSocket()
+      getJobLog()
     })
 
-    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)
-      }
-    })
-
-    function 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`
-    }
-
-    watch(formattedTime, formattedTime => 
jobStore.setExecutionTime(formattedTime))
-
     onUnmounted(() => {
-      jobStore.resetCurrentResult()
+      jobStore.resetJob(currentKey.value)
       if (refreshJobStatusIntervalId !== undefined) {
         clearInterval(refreshJobStatusIntervalId)
         refreshJobStatusIntervalId = undefined
@@ -220,19 +157,7 @@ export default defineComponent({
     })
 
     return {
-      ...toRefs(editorVariables),
-      menuTreeRef,
-      editorMounted,
-      editorSave,
-      handleContentChange,
-      handleFormat,
       tabData,
-      handleConsoleUp,
-      handleConsoleDown,
-      handleConsoleClose,
-      showConsole,
-      editorSize,
-      handleDragEnd,
     }
   },
   render() {
@@ -247,58 +172,10 @@ export default defineComponent({
             ),
             '2': () => (
               <div class={styles['editor-area']}>
-                <n-card class={styles.card} content-style="padding: 5px 
18px;display: flex;flex-direction: column;">
+                <n-card class={styles.card} content-style="padding: 5px 
18px;display: flex;flex-direction: column; height:100%;">
                   <div class={styles.tabs}>
                     <EditorTabs />
                   </div>
-                  <div class={styles.debugger}>
-                    {
-                      this.tabData.panelsList?.length > 0
-                      && (
-                        <EditorDebugger tabData={this.tabData} 
onHandleFormat={this.handleFormat} onHandleSave={this.editorSave} />
-                      )
-                    }
-                  </div>
-                  <div style={{ display: 'flex', flex: 1, flexDirection: 
'column', maxHeight: 'calc(100vh - 181px)' }}>
-                    <n-split direction="vertical" max={0.6} min={0.00} 
resize-trigger-size={0} v-model:size={this.editorSize} 
on-drag-end={this.handleDragEnd}>
-                      {{
-                        '1': () => (
-                          <div class={styles.editor}>
-                            {
-                              this.tabData.panelsList?.length > 0
-                              && (
-                                <n-card content-style="height: 100%;padding: 
0;">
-                                  <MonacoEditor
-                                    
v-model={this.tabData.panelsList.find((item: any) => item.key === 
this.tabData.chooseTab).content}
-                                    language={this.language}
-                                    onEditorMounted={this.editorMounted}
-                                    onEditorSave={this.editorSave}
-                                    onChange={this.handleContentChange}
-                                  />
-                                </n-card>
-                              )
-                            }
-                          </div>
-                        ),
-                        '2': () => (this.showConsole && (
-                          <div class={styles.console}>
-                            {
-                                this.tabData.panelsList?.length > 0
-                                && (
-                                  <n-card content-style="height: 100%;padding: 
0;">
-                                    <EditorConsole 
onConsoleDown={this.handleConsoleDown} onConsoleUp={this.handleConsoleUp} 
onConsoleClose={this.handleConsoleClose} />
-                                  </n-card>
-                                )
-                              }
-                          </div>
-                        )
-                        ),
-                        'resize-trigger': () => (
-                          <div class={styles['console-splitter']} />
-                        ),
-                      }}
-                    </n-split>
-                  </div>
                 </n-card>
               </div>
             ),

Reply via email to