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