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 42a4ebff [Feature] Introduce job log console (#417)
42a4ebff is described below

commit 42a4ebff84e7c6e1bec47f4030b192446dd78b42
Author: s7monk <[email protected]>
AuthorDate: Tue Jun 18 19:06:44 2024 +0800

    [Feature] Introduce job log console (#417)
---
 paimon-web-ui/package.json                         |  1 +
 paimon-web-ui/src/App.tsx                          | 20 +++++
 paimon-web-ui/src/api/models/job/index.ts          |  7 ++
 paimon-web-ui/src/store/job/index.ts               |  8 ++
 .../console/{ => components/log}/index.module.scss | 17 ++---
 .../components/console/components/log/index.tsx    | 87 ++++++++++++++++++++++
 .../query/components/console/index.module.scss     |  2 +-
 .../components/query/components/console/index.tsx  |  5 +-
 .../views/playground/components/query/index.tsx    | 84 +++++++++++++--------
 9 files changed, 186 insertions(+), 45 deletions(-)

diff --git a/paimon-web-ui/package.json b/paimon-web-ui/package.json
index a279baba..fc115b3d 100644
--- a/paimon-web-ui/package.json
+++ b/paimon-web-ui/package.json
@@ -27,6 +27,7 @@
     "@antv/x6-vue-shape": "^2.1.1",
     "dart-sass": "^1.25.0",
     "dayjs": "^1.11.11",
+    "highlight.js": "^11.9.0",
     "lodash": "^4.17.21",
     "mitt": "^3.0.1",
     "monaco-editor": "^0.43.0",
diff --git a/paimon-web-ui/src/App.tsx b/paimon-web-ui/src/App.tsx
index 39f05866..85cbbc12 100644
--- a/paimon-web-ui/src/App.tsx
+++ b/paimon-web-ui/src/App.tsx
@@ -22,9 +22,27 @@ import {
   enUS,
   zhCN,
 } from 'naive-ui'
+import hljs from 'highlight.js/lib/core'
+import csharp from 'highlight.js/lib/languages/csharp'
 import { useConfigStore } from '@/store/config'
 import themes from '@/themes'
 
+hljs.registerLanguage('csharp', csharp)
+
+const log = function (hljs: any) {
+  return {
+    contains: [
+      ...hljs.getLanguage('csharp').contains,
+      {
+        begin: /Job ID: [^\]]+/,
+        relevance: 0,
+      },
+    ],
+  }
+}
+
+hljs.registerLanguage('log', log)
+
 export default defineComponent({
   name: 'App',
   setup() {
@@ -37,6 +55,7 @@ export default defineComponent({
       theme,
       themeOverrides,
       locale,
+      hljs,
     }
   },
   render() {
@@ -47,6 +66,7 @@ export default defineComponent({
         locale={this.locale === 'en' ? enUS : zhCN}
         date-locale={this.locale === 'en' ? dateEnUS : dateZhCN}
         style={{ width: '100%', height: '100vh' }}
+        hljs={this.hljs}
       >
         <n-message-provider>
           <n-dialog-provider>
diff --git a/paimon-web-ui/src/api/models/job/index.ts 
b/paimon-web-ui/src/api/models/job/index.ts
index 1a6f5da7..1bc7f7d9 100644
--- a/paimon-web-ui/src/api/models/job/index.ts
+++ b/paimon-web-ui/src/api/models/job/index.ts
@@ -76,3 +76,10 @@ export function getRecordList(params: RecordNameParams) {
 export function createRecord(params: RecordDTO) {
   return httpRequest.post<RecordDTO, ResponseOptions<unknown>>('/statement', 
params)
 }
+
+/**
+ * # Get job logs
+ */
+export function getLogs() {
+  return httpRequest.get<unknown, ResponseOptions<string>>('/job/logs/get')
+}
diff --git a/paimon-web-ui/src/store/job/index.ts 
b/paimon-web-ui/src/store/job/index.ts
index 098e940e..3a313abb 100644
--- a/paimon-web-ui/src/store/job/index.ts
+++ b/paimon-web-ui/src/store/job/index.ts
@@ -24,6 +24,7 @@ export interface JobState {
   jobResultData: JobResultData | null
   jobStatus: string
   executionTime: string
+  jobLog?: string
 }
 
 export const useJobStore = defineStore({
@@ -34,6 +35,7 @@ export const useJobStore = defineStore({
     jobResultData: null,
     jobStatus: '',
     executionTime: '0m:0s',
+    jobLog: '',
   }),
   persist: false,
   getters: {
@@ -68,6 +70,9 @@ export const useJobStore = defineStore({
     getExecutionTime(): string {
       return this.executionTime
     },
+    getJobLog(): string | undefined {
+      return this.jobLog
+    },
   },
   actions: {
     setExecutionMode(executionMode: ExecutionMode) {
@@ -85,6 +90,9 @@ export const useJobStore = defineStore({
     setExecutionTime(executionTime: string) {
       this.executionTime = executionTime
     },
+    setJobLog(jobLog: string) {
+      this.jobLog = jobLog
+    },
     resetCurrentResult() {
       this.currentJob = null
       this.jobResultData = null
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/index.module.scss
 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.module.scss
similarity index 85%
copy from 
paimon-web-ui/src/views/playground/components/query/components/console/index.module.scss
copy to 
paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.module.scss
index 626d6f8b..e8ae6749 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/index.module.scss
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.module.scss
@@ -15,15 +15,14 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-
-.editor-console {
-  width: 100%;
+.container {
+  display: flex;
+  align-items: center;
   height: 100%;
-  position: relative;
 
-  .operations {
-    position: absolute;
-    top: 17px;
-    right: 20px;
+  .log{
+    width: 100%;
+    height: 100%;
+    padding: 14px 0 12px 14px;
   }
-}
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..40278c2e
--- /dev/null
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/components/log/index.tsx
@@ -0,0 +1,87 @@
+/* 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 styles from './index.module.scss'
+import { useJobStore } from '@/store/job'
+
+export default defineComponent({
+  name: 'LogConsole',
+  props: {
+    maxHeight: {
+      type: Number as PropType<number>,
+      default: 150,
+    },
+  },
+  setup(props) {
+    const jobStore = useJobStore()
+    const { mittBus } = 
getCurrentInstance()!.appContext.config.globalProperties
+    const logContent = computed(() => jobStore.getJobLog)
+    const maxLogHeight = ref(0)
+    const logRef = ref<any>(null)
+    const containerRef = ref<HTMLElement | null>(null)
+
+    function updateLogHeight() {
+      const viewportHeight = window.innerHeight
+      const calculatedHeight = (viewportHeight - 181) * 0.4 - 54
+      maxLogHeight.value = Math.max(calculatedHeight, props.maxHeight + 52)
+    }
+
+    mittBus.on('resizeLog', () => {
+      updateLogHeight()
+    })
+
+    watchEffect(() => {
+      if (logContent.value) {
+        nextTick(() => {
+          logRef.value?.scrollTo({ position: 'bottom', silent: true })
+        })
+      }
+    })
+
+    onMounted(() => {
+      nextTick(() => {
+        updateLogHeight()
+      })
+    })
+
+    onUnmounted(() => {
+      mittBus.off('resizeLog')
+    })
+
+    return {
+      logContent,
+      maxLogHeight,
+      logRef,
+      containerRef,
+    }
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <n-log
+          ref="logRef"
+          class={styles.log}
+          style={{ maxHeight: `${this.maxLogHeight}px` }}
+          line-height={1.8}
+          log={this.logContent}
+          language="log"
+          rows={40}
+        />
+      </div>
+    )
+  },
+})
diff --git 
a/paimon-web-ui/src/views/playground/components/query/components/console/index.module.scss
 
b/paimon-web-ui/src/views/playground/components/query/components/console/index.module.scss
index 626d6f8b..5c2faf2b 100644
--- 
a/paimon-web-ui/src/views/playground/components/query/components/console/index.module.scss
+++ 
b/paimon-web-ui/src/views/playground/components/query/components/console/index.module.scss
@@ -16,7 +16,7 @@ specific language governing permissions and limitations
 under the License. */
 
 
-.editor-console {
+.container {
   width: 100%;
   height: 100%;
   position: relative;
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 df165af2..c3ad0284 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
@@ -19,6 +19,7 @@ import { CloseSharp, KeyboardDoubleArrowDownSharp, 
KeyboardDoubleArrowUpSharp }
 import { throttle } from 'lodash'
 import TableActionBar from './components/controls'
 import TableResult from './components/table'
+import LogConsole from './components/log'
 import styles from './index.module.scss'
 
 export default defineComponent({
@@ -77,7 +78,7 @@ export default defineComponent({
   },
   render() {
     return (
-      <div class={styles['editor-console']} ref="editorConsoleRef">
+      <div class={styles.container} ref="editorConsoleRef">
         <n-tabs
           type="line"
           size="large"
@@ -86,7 +87,7 @@ export default defineComponent({
           pane-style="padding: 0px;box-sizing: border-box;"
         >
           <n-tab-pane name="logs" tab={this.t('playground.logs')}>
-            {/* {this.t('playground.logs')} */}
+            <LogConsole maxHeight={this.adjustedHeight} />
           </n-tab-pane>
           <n-tab-pane name="result" tab={this.t('playground.result')}>
             {
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 151253cc..5b9a15a2 100644
--- a/paimon-web-ui/src/views/playground/components/query/index.tsx
+++ b/paimon-web-ui/src/views/playground/components/query/index.tsx
@@ -18,6 +18,7 @@ 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'
@@ -25,7 +26,7 @@ import EditorDebugger from './components/debugger'
 import EditorConsole from './components/console'
 import MonacoEditor from '@/components/monaco-editor'
 import { useJobStore } from '@/store/job'
-import { getJobStatus, refreshJobStatus } from '@/api/models/job'
+import { getJobStatus, getLogs, refreshJobStatus } from '@/api/models/job'
 import { createSession } from '@/api/models/session'
 
 export default defineComponent({
@@ -90,6 +91,7 @@ export default defineComponent({
 
     const handleDragEnd = () => {
       mittBus.emit('editorResized')
+      mittBus.emit('resizeLog')
     }
 
     // mitt - handle tab choose
@@ -97,6 +99,16 @@ export default defineComponent({
       tabData.value = data
     })
 
+    let getJobLogsIntervalId: number | undefined
+    const getJobLog = () => {
+      getJobLogsIntervalId = setInterval(async () => {
+        const response = await getLogs()
+        jobStore.setJobLog(response.data)
+      }, 1000)
+    }
+
+    onMounted(getJobLog)
+
     let createSessionIntervalId: number | undefined
     watch(currentJob, (newJob) => {
       if (newJob && createSessionIntervalId === undefined) {
@@ -201,6 +213,10 @@ export default defineComponent({
         clearInterval(createSessionIntervalId)
         createSessionIntervalId = undefined
       }
+      if (getJobLogsIntervalId !== undefined) {
+        clearInterval(getJobLogsIntervalId)
+        getJobLogsIntervalId = undefined
+      }
     })
 
     return {
@@ -243,44 +259,46 @@ export default defineComponent({
                       )
                     }
                   </div>
-                  <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}>
-                          {
+                  <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;">
-                                  <EditorConsole 
onConsoleDown={this.handleConsoleDown} onConsoleUp={this.handleConsoleUp} 
onConsoleClose={this.handleConsoleClose} />
+                                  <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>
-                      )
-                      ),
-                      'resize-trigger': () => (
-                        <div class={styles['console-splitter']} />
-                      ),
-                    }}
-                  </n-split>
+                          </div>
+                        ),
+                        '2': () => (this.showConsole && (
+                          <div class={styles.console}>
+                            {
+                                this.tabData.panelsList?.length > 0
+                                && (
+                                  <n-card content-style="height: 100%;padding: 
0;">
+                                    <EditorConsole 
onConsoleDown={this.handleConsoleDown} onConsoleUp={this.handleConsoleUp} 
onConsoleClose={this.handleConsoleClose} />
+                                  </n-card>
+                                )
+                              }
+                          </div>
+                        )
+                        ),
+                        'resize-trigger': () => (
+                          <div class={styles['console-splitter']} />
+                        ),
+                      }}
+                    </n-split>
+                  </div>
                 </n-card>
               </div>
             ),

Reply via email to