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