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%'}`}>
{