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 9045d0cf [Feature] Support playground query saving record (#286)
9045d0cf is described below

commit 9045d0cf4aacfc8df702f6916d047f55f694d41c
Author: xiaomo <[email protected]>
AuthorDate: Mon Jun 3 10:55:00 2024 +0800

    [Feature] Support playground query saving record (#286)
---
 paimon-web-ui/src/App.tsx                          |  4 +-
 paimon-web-ui/src/api/models/job/index.ts          | 17 ++++-
 paimon-web-ui/src/api/models/job/types/record.ts   | 42 +++++++++++
 .../components/query/components/debugger/index.tsx | 76 ++++++++++++++++++--
 .../query/components/menu-tree/index.tsx           | 83 ++++++++++++++--------
 .../views/playground/components/query/index.tsx    | 15 ++--
 6 files changed, 194 insertions(+), 43 deletions(-)

diff --git a/paimon-web-ui/src/App.tsx b/paimon-web-ui/src/App.tsx
index 55bef678..39f05866 100644
--- a/paimon-web-ui/src/App.tsx
+++ b/paimon-web-ui/src/App.tsx
@@ -49,7 +49,9 @@ export default defineComponent({
         style={{ width: '100%', height: '100vh' }}
       >
         <n-message-provider>
-          <router-view />
+          <n-dialog-provider>
+            <router-view />
+          </n-dialog-provider>
         </n-message-provider>
       </n-config-provider>
     )
diff --git a/paimon-web-ui/src/api/models/job/index.ts 
b/paimon-web-ui/src/api/models/job/index.ts
index 8887cc8e..1a6f5da7 100644
--- a/paimon-web-ui/src/api/models/job/index.ts
+++ b/paimon-web-ui/src/api/models/job/index.ts
@@ -20,6 +20,7 @@ import httpRequest from '../../request'
 import type { ResponseOptions } from '@/api/types'
 import type { Job, JobResultData, JobStatus, JobSubmitDTO, ResultFetchDTO, 
StopJobDTO } from '@/api/models/job/types/job'
 import type { History, HistoryNameParams } from 
'@/api/models/job/types/history'
+import type { Record, RecordDTO, RecordNameParams } from 
'@/api/models/job/types/record'
 /**
  * # Submit a job
  */
@@ -59,5 +60,19 @@ export function stopJob(stopJobDTO: StopJobDTO) {
  * # List job histories
  */
 export function getJobHistoryList(params: HistoryNameParams) {
-  return httpRequest.get<HistoryNameParams, 
ResponseOptions<History[]>>('/history/list', { ...params })
+  return httpRequest.get<HistoryNameParams, 
ResponseOptions<History[]>>('/history/list', params)
+}
+
+/**
+ * # List job record
+ */
+export function getRecordList(params: RecordNameParams) {
+  return httpRequest.get<RecordNameParams, 
ResponseOptions<Record[]>>('/statement/list', params)
+}
+
+/**
+ * # create job record
+ */
+export function createRecord(params: RecordDTO) {
+  return httpRequest.post<RecordDTO, ResponseOptions<unknown>>('/statement', 
params)
 }
diff --git a/paimon-web-ui/src/api/models/job/types/record.ts 
b/paimon-web-ui/src/api/models/job/types/record.ts
new file mode 100644
index 00000000..60179c50
--- /dev/null
+++ b/paimon-web-ui/src/api/models/job/types/record.ts
@@ -0,0 +1,42 @@
+/* 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 Record {
+  id: number
+  name: string
+  taskType: string
+  isStreaming: boolean
+  uid: number
+  clusterId: number
+  statements: string
+  statementName?: string
+}
+
+export interface RecordNameParams {
+  statementName?: string
+  pageNum: number
+  pageSize: number
+}
+
+export interface RecordDTO {
+  statementName: string
+  taskType: string
+  isStreaming: boolean
+  uid?: number
+  clusterId?: number
+  statements: string
+}
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 9008fb47..3d664aa7 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,24 +16,36 @@ specific language governing permissions and limitations
 under the License. */
 
 import { ChevronDown, Play, ReaderOutline, Save } from '@vicons/ionicons5'
-import { useMessage } from 'naive-ui'
+import { NInput, useMessage } from 'naive-ui'
+
 import styles from './index.module.scss'
 import { getClusterListByType } from '@/api/models/cluster'
 import type { Cluster } from '@/api/models/cluster/types'
 import type { JobSubmitDTO } from '@/api/models/job/types/job'
-import { submitJob } from '@/api/models/job'
+import { createRecord, 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({
   name: 'EditorDebugger',
   emits: ['handleFormat', 'handleSave'],
+  props: {
+    tabData: {
+      type: Object as PropType<any>,
+      default: () => ({}),
+    },
+  },
   setup(props, { emit }) {
-    const { t } = useLocaleHooks()
     const message = useMessage()
+    const dialog = useDialog()
+
+    const { t } = useLocaleHooks()
     const jobStore = useJobStore()
 
-    const tabData = ref({}) as any
+    const statementName = ref<string>('')
+    const tabData = toRef(props.tabData)
 
     const debuggerVariables = reactive<{
       operatingConditionOptions: { label: string, key: string }[]
@@ -69,8 +81,60 @@ export default defineComponent({
       emit('handleFormat')
     }
 
-    const handleSave = () => {
-      emit('handleSave')
+    async function handleSave() {
+      const currentTab = tabData.value.panelsList.find((item: any) => item.key 
=== tabData.value.chooseTab)
+
+      if (!currentTab)
+        return
+
+      const currentSQL = currentTab.content
+      if (!currentSQL) {
+        message.warning(`Can't submit Empty content`)
+        return
+      }
+
+      const _dialogInst = dialog.create({
+        title: 'Create Record',
+        content: () => h(
+          NInput,
+          {
+            placeholder: 'Input you statement name',
+            modelValue: statementName.value,
+            onInput: (e: string) => {
+              statementName.value = e
+            },
+          },
+        ),
+        positiveText: t('playground.save'),
+        onPositiveClick: async () => {
+          if (!statementName.value || !statementName.value.trim())
+            return message.error('statement name is required')
+
+          const recordDataDTO: RecordDTO = {
+            statementName: statementName.value,
+            taskType: debuggerVariables.conditionValue,
+            clusterId: Number(debuggerVariables.conditionValue2),
+            statements: currentSQL,
+            isStreaming: debuggerVariables.conditionValue3 === 'Streaming',
+          }
+
+          _dialogInst.loading = true
+          try {
+            const response = await createRecord(recordDataDTO)
+            if (response.code === 200)
+              emit('handleSave')
+
+            else
+              message.error(`${t('playground.job_submission_failed')}`)
+          }
+          catch (error) {
+            console.error('Failed to submit job:', error)
+          }
+          finally {
+            _dialogInst.loading = false
+          }
+        },
+      })
     }
 
     function getClusterData() {
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/menu-tree/index.tsx
 
b/paimon-web-ui/src/views/playground/components/query/components/menu-tree/index.tsx
index d4136846..258c67e9 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/menu-tree/index.tsx
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/menu-tree/index.tsx
@@ -15,7 +15,7 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-import { CloseSharp, CodeSlash, FileTrayFullOutline, Search, ServerOutline } 
from '@vicons/ionicons5'
+import { CloseSharp, FileTrayFullOutline, Search, ServerOutline } from 
'@vicons/ionicons5'
 import { NIcon, type TreeOption } from 'naive-ui'
 import { DatabaseOutlined } from '@vicons/antd'
 import { HistoryToggleOffOutlined } from '@vicons/material'
@@ -23,7 +23,7 @@ import { HistoryToggleOffOutlined } from '@vicons/material'
 import styles from './index.module.scss'
 import { useCatalogStore } from '@/store/catalog'
 import { getColumns } from '@/api/models/catalog'
-import { getJobHistoryList } from '@/api/models/job'
+import { getJobHistoryList, getRecordList } from '@/api/models/job'
 
 import type { DataTypeDTO } from '@/api/models/catalog'
 
@@ -36,7 +36,7 @@ interface RecordItem {
 
 export default defineComponent({
   name: 'MenuTree',
-  setup() {
+  setup(_, { expose }) {
     const { t } = useLocaleHooks()
     const message = useMessage()
 
@@ -46,6 +46,7 @@ export default defineComponent({
 
     const tabData = ref({}) as any
     const recordList = ref<RecordItem[]>([])
+    const historyList = ref<RecordItem[]>([])
     const isDetailVisible = ref(true)
     const filterValue = ref('')
     const selectedKeys = ref([])
@@ -94,6 +95,7 @@ export default defineComponent({
             tabData.value.chooseTab = option.key
             return
           }
+
           tabData.value.panelsList.push({
             tableName: option.label,
             key: option.key,
@@ -132,27 +134,6 @@ export default defineComponent({
       tabData.value = data
     })
 
-    const savedQueryList = ref([
-      {
-        key: 1,
-        label: 'test1',
-        prefix: () =>
-          h(NIcon, { color: '#0066FF' }, {
-            default: () => h(CodeSlash),
-          }),
-        content: '',
-      },
-      {
-        key: 2,
-        label: 'test2',
-        prefix: () =>
-          h(NIcon, { color: '#0066FF' }, {
-            default: () => h(CodeSlash),
-          }),
-        content: '',
-      },
-    ]) as any
-
     function handleClose() {
       isDetailVisible.value = !isDetailVisible.value
     }
@@ -165,11 +146,11 @@ export default defineComponent({
       }
     }
 
-    async function loadHistoryData() {
+    async function onLoadHistoryData() {
       const params = { name: filterValue.value, pageNum: 1, pageSize: 
Number.MAX_SAFE_INTEGER }
       try {
         const response = await getJobHistoryList(params)
-        recordList.value = response.data.map(item => ({
+        historyList.value = response.data.map(item => ({
           key: item.id,
           label: item.name,
           prefix: () => (<n-icon color="#0066FF" 
size={20}><HistoryToggleOffOutlined /></n-icon>),
@@ -181,11 +162,29 @@ export default defineComponent({
       }
     }
 
+    async function onLoadRecordData() {
+      const params = { statementName: filterValue.value, pageNum: 1, pageSize: 
Number.MAX_SAFE_INTEGER }
+      try {
+        const response = await getRecordList(params)
+
+        recordList.value = response.data.map(item => ({
+          key: item.id,
+          label: item.statementName || '',
+          prefix: () => (<n-icon color="#0066FF" 
size={20}><HistoryToggleOffOutlined /></n-icon>),
+          content: item.statements,
+        }))
+      }
+      catch (error) {
+        message.error(JSON.stringify(error))
+      }
+    }
+
     watch(() => catalogStore.currentTable, onFetchData)
 
     onMounted(() => {
       onFetchData()
-      loadHistoryData()
+      onLoadHistoryData()
+      onLoadRecordData()
       catalogStore.getAllCatalogs(true)
     })
 
@@ -200,6 +199,27 @@ export default defineComponent({
         return { prefix: 'Aa' }
     }
 
+    function onChangeTab(value: string) {
+      switch (value) {
+        case 'data':
+          onFetchData()
+          break
+        case 'saved_query':
+          onLoadRecordData()
+          break
+        case 'query_record':
+          onLoadHistoryData()
+          break
+
+        default:
+          break
+      }
+    }
+
+    expose({
+      onLoadRecordData,
+    })
+
     return {
       t,
       filterValue,
@@ -210,20 +230,21 @@ export default defineComponent({
       handleTreeSelect,
       renderPrefix,
       handleClose,
-      savedQueryList,
       recordList,
+      historyList,
       currentTable: catalogStoreRef.currentTable,
       columns,
       isDetailVisible,
       selectedKeys,
       getTypePrefix,
+      onChangeTab,
     }
   },
   render() {
     return (
       <div class={styles.container}>
         <n-card class={styles.card} content-style="padding:7px 18px;">
-          <n-tabs default-value="data" justify-content="space-between" 
type="line" style="height: 100%">
+          <n-tabs default-value="data" justify-content="space-between" 
type="line" style="height: 100%" onUpdateValue={this.onChangeTab}>
             <n-tab-pane name="data" tab={this.t('playground.data')} 
style="height: 100%">
               <div class={styles.vertical}>
                 <n-input
@@ -310,7 +331,7 @@ export default defineComponent({
                   expand-on-click
                   selected-keys={this.selectedKeys}
                   on-update:selected-keys={this.handleTreeSelect}
-                  data={this.savedQueryList}
+                  data={this.recordList}
                   pattern={this.filterValue}
                   node-props={this.nodeProps}
                 />
@@ -332,7 +353,7 @@ export default defineComponent({
                   expand-on-click
                   selected-keys={this.selectedKeys}
                   on-update:selected-keys={this.handleTreeSelect}
-                  data={this.recordList}
+                  data={this.historyList}
                   pattern={this.filterValue}
                   node-props={this.nodeProps}
                 />
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 b6eeb395..34a20196 100644
--- a/paimon-web-ui/src/views/playground/components/query/index.tsx
+++ b/paimon-web-ui/src/views/playground/components/query/index.tsx
@@ -33,12 +33,16 @@ export default defineComponent({
     const message = useMessage()
     const jobStore = useJobStore()
 
+    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 formattedTime = computed(() => formatTime(elapsedTime.value))
+
     const editorVariables = reactive({
       editor: {} as any,
       language: 'sql',
@@ -57,6 +61,8 @@ export default defineComponent({
       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) => {
@@ -94,6 +100,7 @@ export default defineComponent({
     })
 
     const getJobStatusIntervalId = ref<number | undefined>()
+
     onMounted(() => {
       getJobStatusIntervalId.value = setInterval(async () => {
         if (currentJob.value && currentJob.value.jobId) {
@@ -131,7 +138,7 @@ export default defineComponent({
       }
     })
 
-    const formatTime = (seconds: number): string => {
+    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)
@@ -139,13 +146,13 @@ export default defineComponent({
       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),
+      menuTreeRef,
       editorMounted,
       editorSave,
       handleContentChange,
@@ -162,7 +169,7 @@ export default defineComponent({
     return (
       <div class={styles.query}>
         <div class={styles['menu-tree']}>
-          <MenuTree />
+          <MenuTree ref="menuTreeRef" />
         </div>
         <div class={styles['editor-area']}>
           <n-card class={styles.card} content-style="padding: 5px 
18px;display: flex;flex-direction: column;">
@@ -170,7 +177,7 @@ export default defineComponent({
               <EditorTabs />
             </div>
             <div class={styles.debugger}>
-              <EditorDebugger onHandleFormat={this.handleFormat} 
onHandleSave={this.editorSave} />
+              <EditorDebugger tabData={this.tabData} 
onHandleFormat={this.handleFormat} onHandleSave={this.editorSave} />
             </div>
             <div class={styles.editor} style={`height: 
${this.consoleHeightType === 'up' ? '20%' : '60%'}`}>
               {

Reply via email to