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/incubator-paimon-webui.git


The following commit(s) were added to refs/heads/main by this push:
     new fb38935  [Feature] Introduce playground module (#46)
fb38935 is described below

commit fb389351c9b41267b530a0ea607c362d39c172f1
Author: labbomb <[email protected]>
AuthorDate: Fri Oct 6 12:09:17 2023 +0800

    [Feature] Introduce playground module (#46)
---
 paimon-web-ui-new/package.json                     |   2 +
 paimon-web-ui-new/pnpm-lock.yaml                   |  60 ++++-
 paimon-web-ui-new/src/App.tsx                      |   4 +-
 .../src/components/monaco-editor/index.tsx         |  38 +++-
 .../src/components/monaco-editor/type.ts           |   4 +-
 .../src/locales/en/modules/playground.ts           |  20 +-
 .../src/locales/zh/modules/playground.ts           |  20 +-
 paimon-web-ui-new/src/main.ts                      |   2 +
 paimon-web-ui-new/src/router/modules/playground.ts |  17 +-
 .../views/playground/components/catalog/index.tsx  |  88 --------
 .../components/console}/index.module.scss          |  11 +-
 .../components/query/components/console/index.tsx  | 109 +++++++++
 .../components/debugger}/index.module.scss         |  14 +-
 .../components/query/components/debugger/index.tsx | 139 ++++++++++++
 .../components/menu-tree}/index.module.scss        |  13 +-
 .../query/components/menu-tree/index.tsx           | 247 +++++++++++++++++++++
 .../components/tabs}/index.module.scss             |  19 +-
 .../components/query/components/tabs/index.tsx     | 109 +++++++++
 .../{ => components/query}/index.module.scss       |  47 ++--
 .../views/playground/components/query/index.tsx    | 138 ++++++++++++
 .../{ => components/slider}/index.module.scss      |  46 ++--
 .../views/playground/components/slider/index.tsx   | 143 ++++++++++++
 .../components/console}/index.module.scss          |  11 +-
 .../workbench/components/console/index.tsx         | 109 +++++++++
 .../components/debugger}/index.module.scss         |  14 +-
 .../workbench/components/debugger/index.tsx        | 139 ++++++++++++
 .../components/menu-tree}/index.module.scss        |  13 +-
 .../workbench/components/menu-tree/index.tsx       | 159 +++++++++++++
 .../components/tabs}/index.module.scss             |  19 +-
 .../components/workbench/components/tabs/index.tsx | 109 +++++++++
 .../{ => components/workbench}/index.module.scss   |  47 ++--
 .../playground/components/workbench/index.tsx      | 138 ++++++++++++
 .../src/views/playground/index.module.scss         |  24 +-
 paimon-web-ui-new/src/views/playground/index.tsx   |  79 +------
 34 files changed, 1882 insertions(+), 269 deletions(-)

diff --git a/paimon-web-ui-new/package.json b/paimon-web-ui-new/package.json
index e3443bb..6fb7ba9 100644
--- a/paimon-web-ui-new/package.json
+++ b/paimon-web-ui-new/package.json
@@ -13,11 +13,13 @@
   },
   "dependencies": {
     "dart-sass": "^1.25.0",
+    "mitt": "^3.0.1",
     "monaco-editor": "^0.43.0",
     "pinia": "^2.1.6",
     "pinia-plugin-persistedstate": "^3.2.0",
     "sass": "^1.66.1",
     "sass-loader": "^13.3.2",
+    "sql-formatter": "^13.0.0",
     "vue": "^3.3.4",
     "vue-i18n": "^9.4.0",
     "vue-router": "^4.2.4"
diff --git a/paimon-web-ui-new/pnpm-lock.yaml b/paimon-web-ui-new/pnpm-lock.yaml
index 17b654a..eb3e50d 100644
--- a/paimon-web-ui-new/pnpm-lock.yaml
+++ b/paimon-web-ui-new/pnpm-lock.yaml
@@ -23,6 +23,9 @@ dependencies:
   dart-sass:
     specifier: ^1.25.0
     version: 1.25.0
+  mitt:
+    specifier: ^3.0.1
+    version: 3.0.1
   monaco-editor:
     specifier: ^0.43.0
     version: 0.43.0
@@ -38,6 +41,9 @@ dependencies:
   sass-loader:
     specifier: ^13.3.2
     version: 13.3.2([email protected])([email protected])
+  sql-formatter:
+    specifier: ^13.0.0
+    version: 13.0.0
   vue:
     specifier: ^3.3.4
     version: 3.3.4
@@ -1393,7 +1399,6 @@ packages:
 
   /[email protected]:
     resolution: {integrity: 
sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
-    dev: true
 
   /[email protected]:
     resolution: {integrity: 
sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
@@ -1756,6 +1761,10 @@ packages:
       path-type: 4.0.0
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==}
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
     engines: {node: '>=6.0.0'}
@@ -2224,6 +2233,11 @@ packages:
       has-symbols: 1.0.3
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==}
+    engines: {node: '>=10'}
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
     engines: {node: '>=10'}
@@ -2770,6 +2784,10 @@ packages:
       brace-expansion: 2.0.1
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==}
     dependencies:
@@ -2783,6 +2801,10 @@ packages:
     resolution: {integrity: 
sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==}
     dev: false
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
     dev: true
@@ -2830,6 +2852,16 @@ packages:
     resolution: {integrity: 
sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==}
+    hasBin: true
+    dependencies:
+      commander: 2.20.3
+      moo: 0.5.2
+      railroad-diagrams: 1.0.0
+      randexp: 0.4.6
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
     dev: false
@@ -3139,6 +3171,18 @@ packages:
     resolution: {integrity: 
sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==}
+    dev: false
+
+  /[email protected]:
+    resolution: {integrity: 
sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==}
+    engines: {node: '>=0.12'}
+    dependencies:
+      discontinuous-range: 1.0.0
+      ret: 0.1.15
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
     dependencies:
@@ -3187,6 +3231,11 @@ packages:
       supports-preserve-symlinks-flag: 1.0.0
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==}
+    engines: {node: '>=0.12'}
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -3407,6 +3456,15 @@ packages:
     resolution: {integrity: 
sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==}
     dev: true
 
+  /[email protected]:
+    resolution: {integrity: 
sha512-V21cVvge4rhn9Fa7K/fTKcmPM+x1yee6Vhq8ZwgaWh3VPBqApgsaoFB5kLAhiqRo5AmSaRyLU7LIdgnNwH01/w==}
+    hasBin: true
+    dependencies:
+      argparse: 2.0.1
+      get-stdin: 8.0.0
+      nearley: 2.20.1
+    dev: false
+
   /[email protected]:
     resolution: {integrity: 
sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==}
     engines: {node: '>= 0.4'}
diff --git a/paimon-web-ui-new/src/App.tsx b/paimon-web-ui-new/src/App.tsx
index 96768c7..90b23e0 100644
--- a/paimon-web-ui-new/src/App.tsx
+++ b/paimon-web-ui-new/src/App.tsx
@@ -47,7 +47,9 @@ export default defineComponent({
       date-locale={this.locale === 'en' ? dateEnUS : dateZhCN}
       style={{ width: '100%', height: '100vh' }}
     >
-      <router-view />
+      <n-message-provider>
+        <router-view />
+      </n-message-provider>
     </n-config-provider>
   }
 })
diff --git a/paimon-web-ui-new/src/components/monaco-editor/index.tsx 
b/paimon-web-ui-new/src/components/monaco-editor/index.tsx
index 5bf54af..55a917d 100644
--- a/paimon-web-ui-new/src/components/monaco-editor/index.tsx
+++ b/paimon-web-ui-new/src/components/monaco-editor/index.tsx
@@ -23,6 +23,7 @@ import tsWorker from 
'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
 import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
 import { editorProps } from './type'
 import { useConfigStore } from '@/store/config'
+import { format } from 'sql-formatter'
 
 // @ts-ignore: worker
 self.MonacoEnvironment = {
@@ -46,7 +47,7 @@ self.MonacoEnvironment = {
 export default defineComponent({
   name: 'MonacoEditor',
   props: editorProps,
-  emits: ['update:modelValue', 'change', 'EditorMounted'],
+  emits: ['update:modelValue', 'change', 'EditorMounted', 'EditorSave'],
   setup(props, { emit }) {
     const configStore = useConfigStore()
     const monacoEditorThemeRef = ref(configStore.getCurrentTheme === 'dark' ? 
'vs-dark' : 'vs')
@@ -61,12 +62,45 @@ export default defineComponent({
         target: monaco.languages.typescript.ScriptTarget.ES2020,
         allowNonTsExtensions: true
       })
+      monaco.languages.registerCompletionItemProvider('sql', {
+        provideCompletionItems: function(model, position) {
+          const word = model.getWordUntilPosition(position);
+          const range = {
+            startLineNumber: position.lineNumber,
+            endLineNumber: position.lineNumber,
+            startColumn: word.startColumn,
+            endColumn: word.endColumn
+          };
+          const suggestions = [];
+          const sqlStr = ['select','from','where','and','or','limit','order 
by','group by'];
+            for(const i in sqlStr){
+              suggestions.push({
+                label: sqlStr[i],
+                kind: monaco.languages.CompletionItemKind['Function'],
+                insertText: sqlStr[i],
+                detail: '',
+                range:range
+              });
+            }
+          return {
+            suggestions: suggestions
+          };
+        },
+      });
+
       editor = monaco.editor.create(codeEditBox.value, {
         value: props.modelValue,
         language: props.language,
         theme: monacoEditorThemeRef.value,
         ...props.options
       })
+
+      editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function 
() {
+        emit('EditorSave')
+      })
+
+      editor.setValue(format(toRaw(editor).getValue()))
+      
       editor.onDidChangeModelContent(() => {
         const value = editor.getValue()
         emit('update:modelValue', value)
@@ -81,6 +115,7 @@ export default defineComponent({
           const value = editor.getValue()
           if (newValue !== value) {
             editor.setValue(newValue)
+            editor.setValue(format(toRaw(editor).getValue()))
           }
         }
       }
@@ -106,6 +141,7 @@ export default defineComponent({
         init()
       }
     )
+    
     onBeforeUnmount(() => {
       editor.dispose()
     })
diff --git a/paimon-web-ui-new/src/components/monaco-editor/type.ts 
b/paimon-web-ui-new/src/components/monaco-editor/type.ts
index a611096..abfd5c1 100644
--- a/paimon-web-ui-new/src/components/monaco-editor/type.ts
+++ b/paimon-web-ui-new/src/components/monaco-editor/type.ts
@@ -66,12 +66,12 @@ export const editorProps = {
         renderLineHighlight: 'line',
         selectOnLineNumbers: true,
         minimap: {
-          enabled: true
+          enabled: false
         },
         readOnly: false,
         contextmenu: true,
         fontSize: 16,
-        scrollBeyondLastLine: false,
+        scrollBeyondLastLine: true,
         overviewRulerBorder: false
       }
     }
diff --git a/paimon-web-ui-new/src/locales/en/modules/playground.ts 
b/paimon-web-ui-new/src/locales/en/modules/playground.ts
index feedda3..9babf17 100644
--- a/paimon-web-ui-new/src/locales/en/modules/playground.ts
+++ b/paimon-web-ui-new/src/locales/en/modules/playground.ts
@@ -16,6 +16,22 @@ specific language governing permissions and limitations
 under the License. */
 
 export default {
-  select_catalog: 'Select Catalog',
-  search: 'Search'
+  query: 'Query',
+  workbench: 'Workbench',
+  task_operation_and_maintenance: 'Task Operation and Maintenance',
+  settings: 'Settings',
+  terminal: 'Terminal',
+  git_branch: 'Git Branch',
+  data: 'Data',
+  saved_query: 'Saved Query',
+  query_record: 'Query Record',
+  search: 'Search',
+  run: 'Run',
+  format: 'Format',
+  save: 'Save',
+  clear: 'Clear',
+  unfold: 'Unfold',
+  collapse: 'Collapse',
+  logs: 'Logs',
+  result: 'Result',
 }
diff --git a/paimon-web-ui-new/src/locales/zh/modules/playground.ts 
b/paimon-web-ui-new/src/locales/zh/modules/playground.ts
index e9350cb..2787b5b 100644
--- a/paimon-web-ui-new/src/locales/zh/modules/playground.ts
+++ b/paimon-web-ui-new/src/locales/zh/modules/playground.ts
@@ -16,6 +16,22 @@ specific language governing permissions and limitations
 under the License. */
 
 export default {
-  select_catalog: '选择 Catalog',
-  search: '搜索'
+  query: '查询',
+  workbench: '工作台',
+  task_operation_and_maintenance: '任务运维',
+  settings: '设置',
+  terminal: '终端',
+  git_branch: 'Git 分支',
+  data: '数据',
+  saved_query: '已保存查询',
+  query_record: '查询记录',
+  search: '搜索',
+  run: '运行',
+  format: '格式化',
+  save: '保存',
+  clear: '清空',
+  unfold: '展开',
+  collapse: '折叠',
+  logs: '日志',
+  result: '结果',
 }
diff --git a/paimon-web-ui-new/src/main.ts b/paimon-web-ui-new/src/main.ts
index 82a46d9..303ab83 100644
--- a/paimon-web-ui-new/src/main.ts
+++ b/paimon-web-ui-new/src/main.ts
@@ -24,6 +24,7 @@ import naive from 'naive-ui'
 import i18n from './locales'
 import { Setting } from './config'
 import '@/assets/styles/main.scss'
+import mitt from 'mitt'
 
 const app = createApp(App)
 const pinia = createPinia()
@@ -33,6 +34,7 @@ app.use(pinia)
 pinia.use(piniaPluginPersistedstate)
 app.use(router)
 app.use(naive)
+app.config.globalProperties.mittBus = mitt()
 
 app.mount('#app')
 
diff --git a/paimon-web-ui-new/src/router/modules/playground.ts 
b/paimon-web-ui-new/src/router/modules/playground.ts
index b8333c1..07863af 100644
--- a/paimon-web-ui-new/src/router/modules/playground.ts
+++ b/paimon-web-ui-new/src/router/modules/playground.ts
@@ -27,7 +27,22 @@ export default [
         path: '/playground',
         name: 'playground',
         meta: { title: '查询控制台' },
-        component: () => import('@/views/playground')
+        redirect: { name: 'playground-query' },
+        component: () => import('@/views/playground'),
+        children: [
+          {
+            path: '/playground/query',
+            name: 'playground-query',
+            meta: { title: '查询' },
+            component: () => import('@/views/playground/components/query')
+          },
+          {
+            path: '/playground/workbench',
+            name: 'playground-workbench',
+            meta: { title: '工作台' },
+            component: () => import('@/views/playground/components/workbench')
+          },
+        ]
       },
     ]
   }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.tsx 
b/paimon-web-ui-new/src/views/playground/components/catalog/index.tsx
deleted file mode 100644
index ea44e8c..0000000
--- a/paimon-web-ui-new/src/views/playground/components/catalog/index.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-/* 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 i18n from "@/locales"
-import type { TreeOption } from "naive-ui";
-import styles from './index.module.scss'
-
-export default defineComponent({
-  name: 'CataLogPage',
-  setup() {
-    const selectedValue= ref(null)
-
-    const options= [
-      {
-        label: 'catalog1',
-        value: 'catalog1'
-      },
-      {
-        label: 'catalog2',
-        value: 'catalog2'
-      }
-    ]
-
-    const searchVal = ref('')
-
-    const treeData: TreeOption[] = [
-      {
-        label: 'paimon',
-        key: 'paimon',
-        children: [
-          {
-            label: 'paimon-table-01',
-            key: 'paimon-table-01',
-            children: [
-              { label: 'id', key: 'id' },
-              { label: 'name', key: 'name' }
-            ]
-          },
-          {
-            label: 'paimon-table-02',
-            key: 'paimon-table-02',
-            children: [
-              { label: 'Osaka', key: 'Osaka' },
-            ]
-          }
-        ]
-      }
-    ]
-
-    return { selectedValue, options, searchVal, treeData }
-  },
-  render() {
-    return (
-      <div class={styles.container}>
-        <div class={styles['select-catalog']}>
-          <n-select
-            v-model:value={this.selectedValue}
-            filterable
-            placeholder={i18n.global.t('playground.select_catalog')}
-            options={this.options}
-          /> 
-        </div>
-        <n-space vertical size={12}>
-          <n-input v-model:value={this.searchVal} 
placeholder={i18n.global.t('playground.search')} />
-          <n-tree
-            pattern={this.searchVal}
-            data={this.treeData}
-            block-line
-          />
-        </n-space>
-      </div>
-    );
-  },
-});
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/query/components/console/index.module.scss
similarity index 85%
copy from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/query/components/console/index.module.scss
index e32b957..626d6f8 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/console/index.module.scss
@@ -15,10 +15,15 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
+
+.editor-console {
   width: 100%;
+  height: 100%;
+  position: relative;
 
-  .select-catalog {
-    padding-bottom: 10px;
+  .operations {
+    position: absolute;
+    top: 17px;
+    right: 20px;
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/query/components/console/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/query/components/console/index.tsx
new file mode 100644
index 0000000..4ecf92a
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/console/index.tsx
@@ -0,0 +1,109 @@
+/* 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 { ChevronDown, ChevronUp, TrashOutline } from '@vicons/ionicons5'
+import styles from './index.module.scss'
+
+export default defineComponent({
+  name: 'EditorConsole',
+  emits: ['ConsoleUp', 'ConsoleDown'],
+  setup(props, { emit }) {
+    const { t } = useLocaleHooks()
+
+    const handleUp = () => {
+      emit('ConsoleUp', 'up')
+    }
+
+    const handleDown = () => {
+      emit('ConsoleDown', 'down')
+    }
+
+    return {
+      t,
+      handleUp,
+      handleDown
+    }
+  },
+  render() {
+    return (
+      <div class={styles['editor-console']}>
+        <n-tabs
+          type="line"
+          size="large"
+          default-value="logs"
+          tabs-padding={20}
+          pane-style="padding: 20px;"
+        >
+          <n-tab-pane name="logs" tab={this.t('playground.logs')}>
+            {this.t('playground.logs')}
+          </n-tab-pane>
+          <n-tab-pane name="result" tab={this.t('playground.result')}>
+            {this.t('playground.result')}
+          </n-tab-pane>
+        </n-tabs>
+        <div class={styles.operations}>
+          <n-space>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    text
+                    v-slots={{
+                      icon: () => <n-icon component={TrashOutline}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.clear')}</span>
+            </n-popover>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    text
+                    onClick={this.handleUp}
+                    v-slots={{
+                      icon: () => <n-icon component={ChevronUp}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.unfold')}</span>
+            </n-popover>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    text
+                    onClick={this.handleDown}
+                    v-slots={{
+                      icon: () => <n-icon component={ChevronDown}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.collapse')}</span>
+            </n-popover>
+          </n-space>
+        </div>
+      </div>   
+    )
+  }
+})
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/query/components/debugger/index.module.scss
similarity index 82%
copy from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/query/components/debugger/index.module.scss
index e32b957..0ab8842 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/debugger/index.module.scss
@@ -15,10 +15,20 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
+
 .container {
+  display: flex;
+  align-items: center;
   width: 100%;
+  height: 100%;
+
+  .run {
+    display: flex;
+  }
 
-  .select-catalog {
-    padding-bottom: 10px;
+  .operations {
+    display: flex;
+    flex: 1;
+    justify-content: flex-end;
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/query/components/debugger/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/query/components/debugger/index.tsx
new file mode 100644
index 0000000..0443172
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/debugger/index.tsx
@@ -0,0 +1,139 @@
+/* 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 { Play, ChevronDown, ReaderOutline, Save } from '@vicons/ionicons5';
+import styles from './index.module.scss'
+
+export default defineComponent({
+  name: 'EditorDebugger',
+  emits: ['handleFormat', 'handleSave'],
+  setup(props, { emit }) {
+    const { t } = useLocaleHooks()
+
+    const debuggerVariables = reactive({
+      operatingConditionOptions: [
+        {
+          label: 'Limit 100 items',
+          key: "100"
+        },
+        {
+          label: 'Limit 1000 items',
+          key: "1000"
+        },
+      ],
+      conditionValue: 'Flink',
+      bigDataOptions: [
+        {
+          label: 'Flink',
+          value: "Flink"
+        },
+        {
+          label: 'Spark',
+          value: "Spark"
+        },
+      ],
+      conditionValue2: 'test1',
+      clusterOptions: [
+        {
+          label: 'test1',
+          value: "test1"
+        },
+        {
+          label: 'test2',
+          value: "test2"
+        },
+      ]
+    })
+
+    const handleSelect = (key: string) => {
+      console.log(key)
+    }
+
+    const handleFormat = () => {
+      emit('handleFormat')
+    }
+
+    const handleSave = () => {
+      emit('handleSave')
+    }
+
+    return {
+      t,
+      ...toRefs(debuggerVariables),
+      handleSelect,
+      handleFormat,
+      handleSave
+    }
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <n-space>
+          <n-button
+            type="primary"
+            v-slots={{
+              icon: () => <n-icon component={Play} />,
+              default: () => {
+               return <div class={styles.run}>
+                  {this.t('playground.run')}
+                  <n-divider vertical />
+                  <n-dropdown trigger="hover" show-arrow 
options={this.operatingConditionOptions} on-select={this.handleSelect}>
+                    <n-icon component={ChevronDown} />
+                  </n-dropdown>
+               </div>
+              }
+            }}
+          ></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-space>
+        <div class={styles.operations}>
+          <n-space>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    onClick={this.handleFormat}
+                    v-slots={{
+                      icon: () => <n-icon component={ReaderOutline}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.format')}</span>
+            </n-popover>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    onClick={this.handleSave}
+                    v-slots={{
+                      icon: () => <n-icon component={Save}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.save')}</span>
+            </n-popover>
+          </n-space>
+        </div>
+      </div>
+    );
+  }
+});
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/query/components/menu-tree/index.module.scss
similarity index 86%
copy from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/query/components/menu-tree/index.module.scss
index e32b957..536333a 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/menu-tree/index.module.scss
@@ -17,8 +17,17 @@ under the License. */
 
 .container {
   width: 100%;
+  height: 100%;
 
-  .select-catalog {
-    padding-bottom: 10px;
+  .card {
+    height: 100%;
+
+    .search {
+      display: flex;
+    }
+
+    .icon {
+      display: flex;
+    }
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/query/components/menu-tree/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/query/components/menu-tree/index.tsx
new file mode 100644
index 0000000..552a78b
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/menu-tree/index.tsx
@@ -0,0 +1,247 @@
+/* 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 { CodeSlash, FileTrayFullOutline, Search, ServerOutline } from 
'@vicons/ionicons5';
+import styles from './index.module.scss'
+import { NIcon, type TreeOption } from 'naive-ui';
+
+export default defineComponent({
+  name: 'MenuTree',
+  setup() {
+    const { t } = useLocaleHooks()
+
+    const treeVariables = reactive({
+      treeData: [
+        {
+          key: 'paimon',
+          label: 'paimon',
+          prefix: () =>
+            h(NIcon, null, {
+              default: () => h(ServerOutline)
+            }),
+          children: [
+            {
+              key: 'user',
+              label: 'user',
+              prefix: () =>
+                h(NIcon, null, {
+                  default: () => h(ServerOutline)
+                }),
+              children: [
+                {
+                  label: 'user_table',
+                  key: '1',
+                  content: 'select * from abc where abc.a="abc";select * from 
cba where cba.a="cba";',
+                  prefix: () =>
+                    h(NIcon, null, {
+                      default: () => h(FileTrayFullOutline)
+                    })
+                },
+                {
+                  label: 'people_table',
+                  key: '2',
+                  content: 'select * from abc where abc.a="abc";',
+                  prefix: () =>
+                    h(NIcon, null, {
+                      default: () => h(FileTrayFullOutline)
+                    })
+                }
+              ]
+            },
+            {
+              key: 'role',
+              label: 'role',
+              prefix: () =>
+                h(NIcon, null, {
+                  default: () => h(ServerOutline)
+                }),
+              children: [
+                {
+                  label: 'user_table',
+                  key: '3',
+                  content: 'select * from kkk;',
+                  prefix: () =>
+                    h(NIcon, null, {
+                      default: () => h(FileTrayFullOutline)
+                    })
+                },
+              ]
+            }
+          ]
+        }
+      ],
+      filterValue: '',
+      selectedKeys: []
+    })
+
+    const nodeProps = ({ option }: { option: TreeOption }) => {
+      return {
+        onClick () {
+          if (option.children) return
+          if (tabData.value.panelsList?.some((item: any) => item.key === 
option.key)) {
+            tabData.value.chooseTab = option.key
+            return
+          }
+          tabData.value.panelsList.push({
+            tableName: option.label,
+            key: option.key,
+            isSaved: false,
+            content: option.content
+          })
+          tabData.value.chooseTab = option.key
+        },
+      }
+    }
+
+    const handleTreeSelect = (value: never[], option: { children: any; }[]) => 
{
+      if (option[0]?.children) return
+      treeVariables.selectedKeys = value
+    }
+
+    // mitt - handle tab choose
+    const tabData = ref({}) as any
+    const { mittBus }  = 
getCurrentInstance()!.appContext.config.globalProperties
+    mittBus.on('initTabData', (data: any) => {
+      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
+
+    const recordList = ref([
+      {
+        key: 3,
+        label: 'test3',
+        prefix: () =>
+          h(NIcon, {color: '#0066FF'}, {
+            default: () => h(CodeSlash)
+          }),
+        content: ''
+      },
+      {
+        key: 4,
+        label: 'test4',
+        prefix: () =>
+          h(NIcon, {color: '#0066FF'}, {
+            default: () => h(CodeSlash)
+          }),
+        content: ''
+      }
+    ]) as any
+
+    onMounted(() => {
+      mittBus.emit('initTreeData', treeVariables)
+    })
+
+    return {
+      t,
+      ...toRefs(treeVariables),
+      nodeProps,
+      handleTreeSelect,
+      savedQueryList,
+      recordList
+    }
+  },
+  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">
+            <n-tab-pane name="data" tab={this.t('playground.data')}>
+              <n-space vertical>
+                <n-input placeholder={this.t('playground.search')} 
style="width: 100%;"
+                  v-model:value={this.filterValue}
+                  v-slots={{
+                    prefix: () => <n-icon component={Search} />
+                  }}
+                >
+                </n-input>
+                <n-tree
+                  block-line
+                  expand-on-click
+                  selected-keys={this.selectedKeys}
+                  on-update:selected-keys={this.handleTreeSelect}
+                  data={this.treeData}
+                  pattern={this.filterValue}
+                  node-props={this.nodeProps}
+                />
+              </n-space>
+            </n-tab-pane>
+            <n-tab-pane name="saved_query" 
tab={this.t('playground.saved_query')}>
+              <n-space vertical>
+                <n-input placeholder={this.t('playground.search')} 
style="width: 100%;"
+                  v-model:value={this.filterValue}
+                  v-slots={{
+                    prefix: () => <n-icon component={Search} />
+                  }}
+                >
+                </n-input>
+                <n-tree
+                  block-line
+                  expand-on-click
+                  selected-keys={this.selectedKeys}
+                  on-update:selected-keys={this.handleTreeSelect}
+                  data={this.savedQueryList}
+                  pattern={this.filterValue}
+                  node-props={this.nodeProps}
+                />
+              </n-space>
+            </n-tab-pane>
+            <n-tab-pane name="query_record" 
tab={this.t('playground.query_record')}>
+            <n-space vertical>
+                <n-input placeholder={this.t('playground.search')} 
style="width: 100%;"
+                  v-model:value={this.filterValue}
+                  v-slots={{
+                    prefix: () => <n-icon component={Search} />
+                  }}
+                >
+                </n-input>
+                <n-tree
+                  block-line
+                  expand-on-click
+                  selected-keys={this.selectedKeys}
+                  on-update:selected-keys={this.handleTreeSelect}
+                  data={this.recordList}
+                  pattern={this.filterValue}
+                  node-props={this.nodeProps}
+                />
+              </n-space>
+            </n-tab-pane>
+          </n-tabs>
+        </n-card>
+      </div>
+    );
+  }
+});
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/query/components/tabs/index.module.scss
similarity index 76%
copy from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/query/components/tabs/index.module.scss
index e32b957..9b67bd7 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/tabs/index.module.scss
@@ -15,10 +15,21 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
-  width: 100%;
 
-  .select-catalog {
-    padding-bottom: 10px;
+.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;
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/query/components/tabs/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/query/components/tabs/index.tsx
new file mode 100644
index 0000000..706953c
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/query/components/tabs/index.tsx
@@ -0,0 +1,109 @@
+/* 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'
+
+export default defineComponent({
+  name: 'EditorTabs',
+  setup() {
+    const { mittBus }  = 
getCurrentInstance()!.appContext.config.globalProperties
+
+    const tabVariables = reactive({
+      chooseTab: '',
+      panelsList: [] as any,
+    })
+
+    const handleAdd = () => {
+      tabVariables.panelsList.push({
+        tableName: 'test' + (tabVariables.panelsList.length + 1),
+        key: 'test' + (tabVariables.panelsList.length + 1),
+        isSaved: false,
+        content: ''
+      })
+      tabVariables.chooseTab = 'test' + tabVariables.panelsList.length
+    }
+
+    const handleClose = (key: any) => {
+      const index = tabVariables.panelsList.findIndex((item: any) => item.key 
=== key)
+      tabVariables.panelsList.splice(index, 1)
+      if (key === tabVariables.chooseTab) {
+        if (tabVariables.panelsList[index - 1]) {
+          tabVariables.chooseTab = tabVariables.panelsList[index - 1].key
+        } else {
+          tabVariables.chooseTab = tabVariables.panelsList[index]?.key || ''
+        }
+      }
+    }
+
+    // mitt - handle tree choose
+    const treeData = ref({}) as any
+    const changeTreeChoose = (value: string) => {
+      treeData.value.selectedKeys = [value]
+      tabVariables.chooseTab = value
+    }
+    mittBus.on('initTreeData', (data: any) => {
+      treeData.value = data
+    })
+
+    onMounted(() => {
+      mittBus.emit('initTabData', tabVariables)
+    })
+
+    return {
+      ...toRefs(tabVariables),
+      handleAdd,
+      handleClose,
+      changeTreeChoose
+    }
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <n-tabs
+          v-model:value={this.chooseTab}
+          type="card"
+          addable
+          closable
+          tab-style="min-width: 160px;"
+          on-close={this.handleClose}
+          on-add={this.handleAdd}
+          on-update:value={this.changeTreeChoose}
+          v-slots={{
+            prefix: () => '',
+            suffix: () => ''
+          }}
+        >
+          {
+            this.panelsList.map((item: any) => (
+              <n-tab-pane name={item.key}
+                v-slots={{
+                  tab: () => (
+                    <div class={styles.tabs}>
+                      <div class={styles.dot}></div>
+                      <div>{item.tableName}</div>
+                      {!item.isSaved && <div class={styles.asterisk}>*</div>}
+                    </div>
+                  )
+                }}
+              ></n-tab-pane>
+            ))
+          }
+        </n-tabs>
+      </div>
+    );
+  }
+});
diff --git a/paimon-web-ui-new/src/views/playground/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/query/index.module.scss
similarity index 67%
copy from paimon-web-ui-new/src/views/playground/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/query/index.module.scss
index 839bb21..369f097 100644
--- a/paimon-web-ui-new/src/views/playground/index.module.scss
+++ b/paimon-web-ui-new/src/views/playground/components/query/index.module.scss
@@ -15,32 +15,45 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
-  height: 100%;
+.query {
+  display: flex;
   width: 100%;
+  height: 100%;
 
-  :global {
-    .n-tabs {
-      height: 100%;
-    }
+  .menu-tree {
+    width: 20%;
+    height: 100%;
   }
 
-  .content {
-    display: flex;
+  .editor-area {
+    width: 80%;
     height: 100%;
 
-    .catalog {
-      width: 350px;
-      height: 100%;
-      padding: 10px;
-      box-sizing: border-box;
+    :global {
+      .n-card {
+        height: 100%;
+      }
     }
 
+    .tabs {
+      display: flex;
+      height: 41px;
+      width: 100%;
+    }
+  
+    .debugger {
+      display: flex;
+      height: 64px;
+    }
+  
     .editor {
-      height: 100%;
-      flex: 1;
-      padding: 10px;
-      box-sizing: border-box;
+      height: 60%;
+      overflow-y: scroll;
+    }
+
+    .console {
+      height: 40%;
+      overflow-y: scroll;
     }
   }
 }
diff --git a/paimon-web-ui-new/src/views/playground/components/query/index.tsx 
b/paimon-web-ui-new/src/views/playground/components/query/index.tsx
new file mode 100644
index 0000000..2a2ee2d
--- /dev/null
+++ b/paimon-web-ui-new/src/views/playground/components/query/index.tsx
@@ -0,0 +1,138 @@
+/* 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 MenuTree from './components/menu-tree';
+import EditorTabs from './components/tabs';
+import EditorDebugger from './components/debugger';
+import * as monaco from 'monaco-editor'
+import MonacoEditor from '@/components/monaco-editor';
+import EditorConsole from './components/console';
+import { format } from 'sql-formatter';
+import { useMessage } from 'naive-ui'
+
+export default defineComponent({
+  name: 'QueryPage',
+  setup() {
+    const message = useMessage()
+
+    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')
+      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
+    }
+
+    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 consoleHeightType = ref('down')
+
+    const handleConsoleUp = (type: string) => {
+      consoleHeightType.value = type
+    }
+
+    const handleConsoleDown = (type: string) => {
+      consoleHeightType.value = type
+    }
+
+
+    watch(
+      () => consoleHeightType.value,
+      () => {
+        if (tabData.value.panelsList?.length > 0) {
+          editorVariables.editor?.layout()
+        }
+      }
+    )
+
+    // mitt - handle tab choose
+    const tabData = ref({}) as any
+    const { mittBus }  = 
getCurrentInstance()!.appContext.config.globalProperties
+    mittBus.on('initTabData', (data: any) => {
+      tabData.value = data
+    })
+
+    return {
+      ...toRefs(editorVariables),
+      editorMounted,
+      editorSave,
+      handleContentChange,
+      handleFormat,
+      tabData,
+      handleConsoleUp,
+      handleConsoleDown,
+      consoleHeightType
+    }
+  },
+  render() {
+    return (
+      <div class={styles.query}>
+        <div class={styles['menu-tree']}>
+          <MenuTree />
+        </div>
+        <div class={styles['editor-area']}>
+          <n-card class={styles.card} content-style={'padding: 5px 
18px;display: flex;flex-direction: column;'}>
+            <div class={styles.tabs}>
+              <EditorTabs />
+            </div>
+            <div class={styles.debugger}>
+              <EditorDebugger onHandleFormat={this.handleFormat} 
onHandleSave={this.editorSave} />
+            </div>
+            <div class={styles.editor} style={`height: 
${this.consoleHeightType === 'up' ? '20%' : '60%'}`}>
+              {
+                this.tabData.panelsList?.length > 0 &&
+                <n-card content-style={'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>
+            <div class={styles.console} style={`height: 
${this.consoleHeightType === 'up' ? '80%' : '40%'}`}>
+              {
+                this.tabData.panelsList?.length > 0 &&
+                <n-card content-style={'padding: 0;'}>
+                  <EditorConsole onConsoleDown={this.handleConsoleDown} 
onConsoleUp={this.handleConsoleUp} />
+                </n-card>
+              }
+            </div>
+          </n-card>
+        </div>
+      </div>
+    );
+  }
+});
diff --git a/paimon-web-ui-new/src/views/playground/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/slider/index.module.scss
similarity index 64%
copy from paimon-web-ui-new/src/views/playground/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/slider/index.module.scss
index 839bb21..2fdba5b 100644
--- a/paimon-web-ui-new/src/views/playground/index.module.scss
+++ b/paimon-web-ui-new/src/views/playground/components/slider/index.module.scss
@@ -15,32 +15,36 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
+.slider {
+  display: flex;
+  flex-wrap: wrap;
+  flex-direction: column;
+  width: 60px;
   height: 100%;
-  width: 100%;
+  padding: 20px 0;
+  box-sizing: border-box;
+  background-color: var(--n-color);
 
-  :global {
-    .n-tabs {
-      height: 100%;
-    }
+  .workspace {
+    display: flex;
+    flex: 1;
+    justify-content: center;
+    width: 100%;
   }
 
-  .content {
+  .functional-domain {
     display: flex;
-    height: 100%;
+    justify-content: center;
+    align-items: flex-end;
+    width: 100%;
+    height: 200px;
+  }
+}
 
-    .catalog {
-      width: 350px;
-      height: 100%;
-      padding: 10px;
-      box-sizing: border-box;
-    }
+.light {
+  background-color: #fff;
+}
 
-    .editor {
-      height: 100%;
-      flex: 1;
-      padding: 10px;
-      box-sizing: border-box;
-    }
-  }
+.dark {
+  background-color: #18181c;
 }
diff --git a/paimon-web-ui-new/src/views/playground/components/slider/index.tsx 
b/paimon-web-ui-new/src/views/playground/components/slider/index.tsx
new file mode 100644
index 0000000..8194769
--- /dev/null
+++ b/paimon-web-ui-new/src/views/playground/components/slider/index.tsx
@@ -0,0 +1,143 @@
+/* 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 { Layers, CodeSlashSharp, Settings, Terminal, GitBranch } from 
'@vicons/ionicons5';
+import { useConfigStore } from '@/store/config'
+import { NIcon } from 'naive-ui';
+
+export default defineComponent({
+  name: 'SliderPage',
+  setup() {
+    const configStore = useConfigStore()
+    const { t } = useLocaleHooks()
+    const router = useRouter()
+
+    const renderIcon = (icon: any) => {
+      return () => h(NIcon, { size: 24 }, { default: () => h(icon) })
+    }
+
+    const sliderVariables = reactive({
+      workspaceList: [
+        {
+          icon: renderIcon(Layers),
+          title: 'Layers',
+          description: computed(() => (t('playground.query'))),
+          isClick: true,
+          path: '/playground/query'
+        },
+        {
+          icon: renderIcon(CodeSlashSharp),
+          title: 'Code',
+          description: computed(() => (t('playground.workbench'))),
+          isClick: false,
+          path: '/playground/workbench'
+        },
+      ],
+      domainList: [
+        {
+          icon: renderIcon(Settings),
+          title: 'Settings',
+          description: computed(() => (t('playground.settings')))
+        },
+        {
+          icon: renderIcon(Terminal),
+          title: 'Terminal',
+          description: computed(() => (t('playground.terminal')))
+        },
+        {
+          icon: renderIcon(GitBranch),
+          title: 'GitBranch',
+          description: computed(() => (t('playground.git_branch'))),
+        }
+      ],
+    })
+
+    const handleClick = (index: number, type: string) => {
+      if (type === 'workspace') {
+        for (const i in sliderVariables.workspaceList) {
+          sliderVariables.workspaceList[i].isClick = false
+        }
+        sliderVariables.workspaceList[index].isClick = true
+        router.push(sliderVariables.workspaceList[index].path)
+      }
+    }
+
+    return {
+      configStore,
+      handleClick,
+      ...toRefs(sliderVariables)
+    }
+  },
+  render() {
+    return (
+      <div class={[this.configStore.getCurrentTheme === 'light' ? styles.light 
: styles.dark, styles.slider]}>
+        <div class={styles.workspace}>
+          <n-space vertical size={20}>
+            { 
+              this.workspaceList.map((item: any, index: number) => {
+                return (
+                  <n-popover trigger="hover" placement="right"
+                    v-slots={{
+                      trigger: () => (
+                        <n-button
+                          type={item.isClick ? 'primary' : 'default'}
+                          text
+                          onClick={() => this.handleClick(index, 'workspace')}
+                          v-slots={{
+                            icon: () => item.icon()
+                          }}
+                        >
+                        </n-button>
+                      )
+                    }}>
+                    <span>{item.description}</span>
+                  </n-popover>
+                )
+              })
+            }
+          </n-space>
+        </div>
+        <div class={styles['functional-domain']}>
+          <n-space vertical size={20}>
+            { 
+              this.domainList.map((item: any, index: number) => {
+                return (
+                  <n-popover trigger="hover" placement="right"
+                    v-slots={{
+                      trigger: () => (
+                        <n-button
+                          text
+                          onClick={() => this.handleClick(index, 'domain')}
+                          v-slots={{
+                            icon: () => item.icon()
+                          }}
+                        >
+                        </n-button>
+                      )
+                    }}>
+                    <span>{item.description}</span>
+                  </n-popover>
+                )
+              })
+            }
+          </n-space>
+        </div>
+      </div>
+    );
+  },
+});
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/console/index.module.scss
similarity index 85%
copy from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/workbench/components/console/index.module.scss
index e32b957..626d6f8 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/console/index.module.scss
@@ -15,10 +15,15 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
+
+.editor-console {
   width: 100%;
+  height: 100%;
+  position: relative;
 
-  .select-catalog {
-    padding-bottom: 10px;
+  .operations {
+    position: absolute;
+    top: 17px;
+    right: 20px;
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/workbench/components/console/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/console/index.tsx
new file mode 100644
index 0000000..4ecf92a
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/console/index.tsx
@@ -0,0 +1,109 @@
+/* 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 { ChevronDown, ChevronUp, TrashOutline } from '@vicons/ionicons5'
+import styles from './index.module.scss'
+
+export default defineComponent({
+  name: 'EditorConsole',
+  emits: ['ConsoleUp', 'ConsoleDown'],
+  setup(props, { emit }) {
+    const { t } = useLocaleHooks()
+
+    const handleUp = () => {
+      emit('ConsoleUp', 'up')
+    }
+
+    const handleDown = () => {
+      emit('ConsoleDown', 'down')
+    }
+
+    return {
+      t,
+      handleUp,
+      handleDown
+    }
+  },
+  render() {
+    return (
+      <div class={styles['editor-console']}>
+        <n-tabs
+          type="line"
+          size="large"
+          default-value="logs"
+          tabs-padding={20}
+          pane-style="padding: 20px;"
+        >
+          <n-tab-pane name="logs" tab={this.t('playground.logs')}>
+            {this.t('playground.logs')}
+          </n-tab-pane>
+          <n-tab-pane name="result" tab={this.t('playground.result')}>
+            {this.t('playground.result')}
+          </n-tab-pane>
+        </n-tabs>
+        <div class={styles.operations}>
+          <n-space>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    text
+                    v-slots={{
+                      icon: () => <n-icon component={TrashOutline}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.clear')}</span>
+            </n-popover>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    text
+                    onClick={this.handleUp}
+                    v-slots={{
+                      icon: () => <n-icon component={ChevronUp}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.unfold')}</span>
+            </n-popover>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    text
+                    onClick={this.handleDown}
+                    v-slots={{
+                      icon: () => <n-icon component={ChevronDown}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.collapse')}</span>
+            </n-popover>
+          </n-space>
+        </div>
+      </div>   
+    )
+  }
+})
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/debugger/index.module.scss
similarity index 82%
copy from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/workbench/components/debugger/index.module.scss
index e32b957..0ab8842 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/debugger/index.module.scss
@@ -15,10 +15,20 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
+
 .container {
+  display: flex;
+  align-items: center;
   width: 100%;
+  height: 100%;
+
+  .run {
+    display: flex;
+  }
 
-  .select-catalog {
-    padding-bottom: 10px;
+  .operations {
+    display: flex;
+    flex: 1;
+    justify-content: flex-end;
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/workbench/components/debugger/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/debugger/index.tsx
new file mode 100644
index 0000000..0443172
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/debugger/index.tsx
@@ -0,0 +1,139 @@
+/* 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 { Play, ChevronDown, ReaderOutline, Save } from '@vicons/ionicons5';
+import styles from './index.module.scss'
+
+export default defineComponent({
+  name: 'EditorDebugger',
+  emits: ['handleFormat', 'handleSave'],
+  setup(props, { emit }) {
+    const { t } = useLocaleHooks()
+
+    const debuggerVariables = reactive({
+      operatingConditionOptions: [
+        {
+          label: 'Limit 100 items',
+          key: "100"
+        },
+        {
+          label: 'Limit 1000 items',
+          key: "1000"
+        },
+      ],
+      conditionValue: 'Flink',
+      bigDataOptions: [
+        {
+          label: 'Flink',
+          value: "Flink"
+        },
+        {
+          label: 'Spark',
+          value: "Spark"
+        },
+      ],
+      conditionValue2: 'test1',
+      clusterOptions: [
+        {
+          label: 'test1',
+          value: "test1"
+        },
+        {
+          label: 'test2',
+          value: "test2"
+        },
+      ]
+    })
+
+    const handleSelect = (key: string) => {
+      console.log(key)
+    }
+
+    const handleFormat = () => {
+      emit('handleFormat')
+    }
+
+    const handleSave = () => {
+      emit('handleSave')
+    }
+
+    return {
+      t,
+      ...toRefs(debuggerVariables),
+      handleSelect,
+      handleFormat,
+      handleSave
+    }
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <n-space>
+          <n-button
+            type="primary"
+            v-slots={{
+              icon: () => <n-icon component={Play} />,
+              default: () => {
+               return <div class={styles.run}>
+                  {this.t('playground.run')}
+                  <n-divider vertical />
+                  <n-dropdown trigger="hover" show-arrow 
options={this.operatingConditionOptions} on-select={this.handleSelect}>
+                    <n-icon component={ChevronDown} />
+                  </n-dropdown>
+               </div>
+              }
+            }}
+          ></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-space>
+        <div class={styles.operations}>
+          <n-space>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    onClick={this.handleFormat}
+                    v-slots={{
+                      icon: () => <n-icon component={ReaderOutline}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.format')}</span>
+            </n-popover>
+            <n-popover trigger="hover" placement="bottom"
+              v-slots={{
+                trigger: () => (
+                  <n-button
+                    onClick={this.handleSave}
+                    v-slots={{
+                      icon: () => <n-icon component={Save}></n-icon>
+                    }}
+                  >
+                  </n-button>
+                )
+              }}>
+              <span>{this.t('playground.save')}</span>
+            </n-popover>
+          </n-space>
+        </div>
+      </div>
+    );
+  }
+});
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/menu-tree/index.module.scss
similarity index 86%
copy from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/workbench/components/menu-tree/index.module.scss
index e32b957..536333a 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/menu-tree/index.module.scss
@@ -17,8 +17,17 @@ under the License. */
 
 .container {
   width: 100%;
+  height: 100%;
 
-  .select-catalog {
-    padding-bottom: 10px;
+  .card {
+    height: 100%;
+
+    .search {
+      display: flex;
+    }
+
+    .icon {
+      display: flex;
+    }
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/workbench/components/menu-tree/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/menu-tree/index.tsx
new file mode 100644
index 0000000..0cbc368
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/menu-tree/index.tsx
@@ -0,0 +1,159 @@
+/* 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 { CodeSlash, FileTrayFullOutline, Search, ServerOutline } from 
'@vicons/ionicons5';
+import styles from './index.module.scss'
+import { NIcon, type TreeOption } from 'naive-ui';
+
+export default defineComponent({
+  name: 'MenuTree',
+  setup() {
+    const { t } = useLocaleHooks()
+
+    const treeVariables = reactive({
+      treeData: [
+        {
+          key: 'paimon2',
+          label: 'paimon2',
+          prefix: () =>
+            h(NIcon, null, {
+              default: () => h(ServerOutline)
+            }),
+          children: [
+            {
+              key: 'user',
+              label: 'user',
+              prefix: () =>
+                h(NIcon, null, {
+                  default: () => h(ServerOutline)
+                }),
+              children: [
+                {
+                  label: 'user_table',
+                  key: '1',
+                  content: 'select * from abc where abc.a="abc";select * from 
cba where cba.a="cba";',
+                  prefix: () =>
+                    h(NIcon, null, {
+                      default: () => h(FileTrayFullOutline)
+                    })
+                },
+                {
+                  label: 'people_table',
+                  key: '2',
+                  content: 'select * from abc where abc.a="abc";',
+                  prefix: () =>
+                    h(NIcon, null, {
+                      default: () => h(FileTrayFullOutline)
+                    })
+                }
+              ]
+            },
+            {
+              key: 'role',
+              label: 'role',
+              prefix: () =>
+                h(NIcon, null, {
+                  default: () => h(ServerOutline)
+                }),
+              children: [
+                {
+                  label: 'user_table',
+                  key: '3',
+                  content: 'select * from kkk;',
+                  prefix: () =>
+                    h(NIcon, null, {
+                      default: () => h(FileTrayFullOutline)
+                    })
+                },
+              ]
+            }
+          ]
+        }
+      ],
+      filterValue: '',
+      selectedKeys: []
+    })
+
+    const nodeProps = ({ option }: { option: TreeOption }) => {
+      return {
+        onClick () {
+          if (option.children) return
+          if (tabData.value.panelsList?.some((item: any) => item.key === 
option.key)) {
+            tabData.value.chooseTab = option.key
+            return
+          }
+          tabData.value.panelsList.push({
+            tableName: option.label,
+            key: option.key,
+            isSaved: false,
+            content: option.content
+          })
+          tabData.value.chooseTab = option.key
+        },
+      }
+    }
+
+    const handleTreeSelect = (value: never[], option: { children: any; }[]) => 
{
+      if (option[0]?.children) return
+      treeVariables.selectedKeys = value
+    }
+
+    // mitt - handle tab choose
+    const tabData = ref({}) as any
+    const { mittBus }  = 
getCurrentInstance()!.appContext.config.globalProperties
+    mittBus.on('initTabData', (data: any) => {
+      tabData.value = data
+    })
+
+    onMounted(() => {
+      mittBus.emit('initTreeData', treeVariables)
+    })
+
+    return {
+      t,
+      ...toRefs(treeVariables),
+      nodeProps,
+      handleTreeSelect
+    }
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <n-card class={styles.card} content-style={'padding:20px 18px;'}>
+          <n-space vertical>
+            <n-input placeholder={this.t('playground.search')} style="width: 
100%;"
+              v-model:value={this.filterValue}
+              v-slots={{
+                prefix: () => <n-icon component={Search} />
+              }}
+            >
+            </n-input>
+            <n-tree
+              block-line
+              expand-on-click
+              selected-keys={this.selectedKeys}
+              on-update:selected-keys={this.handleTreeSelect}
+              data={this.treeData}
+              pattern={this.filterValue}
+              node-props={this.nodeProps}
+            />
+          </n-space>
+        </n-card>
+      </div>
+    );
+  }
+});
diff --git 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/tabs/index.module.scss
similarity index 76%
rename from 
paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
rename to 
paimon-web-ui-new/src/views/playground/components/workbench/components/tabs/index.module.scss
index e32b957..9b67bd7 100644
--- 
a/paimon-web-ui-new/src/views/playground/components/catalog/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/tabs/index.module.scss
@@ -15,10 +15,21 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
-  width: 100%;
 
-  .select-catalog {
-    padding-bottom: 10px;
+.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;
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/workbench/components/tabs/index.tsx
 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/tabs/index.tsx
new file mode 100644
index 0000000..706953c
--- /dev/null
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/components/tabs/index.tsx
@@ -0,0 +1,109 @@
+/* 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'
+
+export default defineComponent({
+  name: 'EditorTabs',
+  setup() {
+    const { mittBus }  = 
getCurrentInstance()!.appContext.config.globalProperties
+
+    const tabVariables = reactive({
+      chooseTab: '',
+      panelsList: [] as any,
+    })
+
+    const handleAdd = () => {
+      tabVariables.panelsList.push({
+        tableName: 'test' + (tabVariables.panelsList.length + 1),
+        key: 'test' + (tabVariables.panelsList.length + 1),
+        isSaved: false,
+        content: ''
+      })
+      tabVariables.chooseTab = 'test' + tabVariables.panelsList.length
+    }
+
+    const handleClose = (key: any) => {
+      const index = tabVariables.panelsList.findIndex((item: any) => item.key 
=== key)
+      tabVariables.panelsList.splice(index, 1)
+      if (key === tabVariables.chooseTab) {
+        if (tabVariables.panelsList[index - 1]) {
+          tabVariables.chooseTab = tabVariables.panelsList[index - 1].key
+        } else {
+          tabVariables.chooseTab = tabVariables.panelsList[index]?.key || ''
+        }
+      }
+    }
+
+    // mitt - handle tree choose
+    const treeData = ref({}) as any
+    const changeTreeChoose = (value: string) => {
+      treeData.value.selectedKeys = [value]
+      tabVariables.chooseTab = value
+    }
+    mittBus.on('initTreeData', (data: any) => {
+      treeData.value = data
+    })
+
+    onMounted(() => {
+      mittBus.emit('initTabData', tabVariables)
+    })
+
+    return {
+      ...toRefs(tabVariables),
+      handleAdd,
+      handleClose,
+      changeTreeChoose
+    }
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <n-tabs
+          v-model:value={this.chooseTab}
+          type="card"
+          addable
+          closable
+          tab-style="min-width: 160px;"
+          on-close={this.handleClose}
+          on-add={this.handleAdd}
+          on-update:value={this.changeTreeChoose}
+          v-slots={{
+            prefix: () => '',
+            suffix: () => ''
+          }}
+        >
+          {
+            this.panelsList.map((item: any) => (
+              <n-tab-pane name={item.key}
+                v-slots={{
+                  tab: () => (
+                    <div class={styles.tabs}>
+                      <div class={styles.dot}></div>
+                      <div>{item.tableName}</div>
+                      {!item.isSaved && <div class={styles.asterisk}>*</div>}
+                    </div>
+                  )
+                }}
+              ></n-tab-pane>
+            ))
+          }
+        </n-tabs>
+      </div>
+    );
+  }
+});
diff --git a/paimon-web-ui-new/src/views/playground/index.module.scss 
b/paimon-web-ui-new/src/views/playground/components/workbench/index.module.scss
similarity index 66%
copy from paimon-web-ui-new/src/views/playground/index.module.scss
copy to 
paimon-web-ui-new/src/views/playground/components/workbench/index.module.scss
index 839bb21..d77f10b 100644
--- a/paimon-web-ui-new/src/views/playground/index.module.scss
+++ 
b/paimon-web-ui-new/src/views/playground/components/workbench/index.module.scss
@@ -15,32 +15,45 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License. */
 
-.container {
-  height: 100%;
+.workbench {
+  display: flex;
   width: 100%;
+  height: 100%;
 
-  :global {
-    .n-tabs {
-      height: 100%;
-    }
+  .menu-tree {
+    width: 20%;
+    height: 100%;
   }
 
-  .content {
-    display: flex;
+  .editor-area {
+    width: 80%;
     height: 100%;
 
-    .catalog {
-      width: 350px;
-      height: 100%;
-      padding: 10px;
-      box-sizing: border-box;
+    :global {
+      .n-card {
+        height: 100%;
+      }
     }
 
+    .tabs {
+      display: flex;
+      height: 41px;
+      width: 100%;
+    }
+  
+    .debugger {
+      display: flex;
+      height: 64px;
+    }
+  
     .editor {
-      height: 100%;
-      flex: 1;
-      padding: 10px;
-      box-sizing: border-box;
+      height: 60%;
+      overflow-y: scroll;
+    }
+
+    .console {
+      height: 40%;
+      overflow-y: scroll;
     }
   }
 }
diff --git 
a/paimon-web-ui-new/src/views/playground/components/workbench/index.tsx 
b/paimon-web-ui-new/src/views/playground/components/workbench/index.tsx
new file mode 100644
index 0000000..6024369
--- /dev/null
+++ b/paimon-web-ui-new/src/views/playground/components/workbench/index.tsx
@@ -0,0 +1,138 @@
+/* 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 MenuTree from './components/menu-tree';
+import EditorTabs from './components/tabs';
+import EditorDebugger from './components/debugger';
+import * as monaco from 'monaco-editor'
+import MonacoEditor from '@/components/monaco-editor';
+import EditorConsole from './components/console';
+import { format } from 'sql-formatter';
+import { useMessage } from 'naive-ui'
+
+export default defineComponent({
+  name: 'WorkbenchPage',
+  setup() {
+    const message = useMessage()
+
+    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')
+      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
+    }
+
+    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 consoleHeightType = ref('down')
+
+    const handleConsoleUp = (type: string) => {
+      consoleHeightType.value = type
+    }
+
+    const handleConsoleDown = (type: string) => {
+      consoleHeightType.value = type
+    }
+
+
+    watch(
+      () => consoleHeightType.value,
+      () => {
+        if (tabData.value.panelsList?.length > 0) {
+          editorVariables.editor?.layout()
+        }
+      }
+    )
+
+    // mitt - handle tab choose
+    const tabData = ref({}) as any
+    const { mittBus }  = 
getCurrentInstance()!.appContext.config.globalProperties
+    mittBus.on('initTabData', (data: any) => {
+      tabData.value = data
+    })
+
+    return {
+      ...toRefs(editorVariables),
+      editorMounted,
+      editorSave,
+      handleContentChange,
+      handleFormat,
+      tabData,
+      handleConsoleUp,
+      handleConsoleDown,
+      consoleHeightType
+    }
+  },
+  render() {
+    return (
+      <div class={styles.workbench}>
+        <div class={styles['menu-tree']}>
+          <MenuTree />
+        </div>
+        <div class={styles['editor-area']}>
+          <n-card class={styles.card} content-style={'padding: 5px 
18px;display: flex;flex-direction: column;'}>
+            <div class={styles.tabs}>
+              <EditorTabs />
+            </div>
+            <div class={styles.debugger}>
+              <EditorDebugger onHandleFormat={this.handleFormat} 
onHandleSave={this.editorSave} />
+            </div>
+            <div class={styles.editor} style={`height: 
${this.consoleHeightType === 'up' ? '20%' : '60%'}`}>
+              {
+                this.tabData.panelsList?.length > 0 &&
+                <n-card content-style={'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>
+            <div class={styles.console} style={`height: 
${this.consoleHeightType === 'up' ? '80%' : '40%'}`}>
+              {
+                this.tabData.panelsList?.length > 0 &&
+                <n-card content-style={'padding: 0;'}>
+                  <EditorConsole onConsoleDown={this.handleConsoleDown} 
onConsoleUp={this.handleConsoleUp} />
+                </n-card>
+              }
+            </div>
+          </n-card>
+        </div>
+      </div>
+    );
+  }
+});
diff --git a/paimon-web-ui-new/src/views/playground/index.module.scss 
b/paimon-web-ui-new/src/views/playground/index.module.scss
index 839bb21..fe437d9 100644
--- a/paimon-web-ui-new/src/views/playground/index.module.scss
+++ b/paimon-web-ui-new/src/views/playground/index.module.scss
@@ -16,31 +16,13 @@ specific language governing permissions and limitations
 under the License. */
 
 .container {
-  height: 100%;
+  display: flex;
   width: 100%;
-
-  :global {
-    .n-tabs {
-      height: 100%;
-    }
-  }
+  height: 100%;
 
   .content {
     display: flex;
+    width: calc(100% - 60px);
     height: 100%;
-
-    .catalog {
-      width: 350px;
-      height: 100%;
-      padding: 10px;
-      box-sizing: border-box;
-    }
-
-    .editor {
-      height: 100%;
-      flex: 1;
-      padding: 10px;
-      box-sizing: border-box;
-    }
   }
 }
diff --git a/paimon-web-ui-new/src/views/playground/index.tsx 
b/paimon-web-ui-new/src/views/playground/index.tsx
index 448b0de..1d133cf 100644
--- a/paimon-web-ui-new/src/views/playground/index.tsx
+++ b/paimon-web-ui-new/src/views/playground/index.tsx
@@ -16,85 +16,18 @@ specific language governing permissions and limitations
 under the License. */
 
 import styles from './index.module.scss';
-import type { TabsProps } from 'naive-ui';
-import { Layers, CodeSlashSharp, SyncCircleOutline } from '@vicons/ionicons5';
-import CataLog from './components/catalog';
-import * as monaco from 'monaco-editor'
-import MonacoEditor from '@/components/monaco-editor';
-
+import Slider from './components/slider';
 
 export default defineComponent({
   name: 'PlaygroundPage',
-  setup() {
-    const type = ref<TabsProps['type']>('bar')
-
-    const content = ref('')
-    const language = ref('javascript')
-    const editorMounted = (editor: monaco.editor.IStandaloneCodeEditor) => {
-      console.log('Loaded editor instance.', editor)
-    }
-
-    return {
-      type,
-      content,
-      language,
-      editorMounted
-    }
-  },
+  setup() {},
   render() {
     return (
       <div class={styles.container}>
-        <n-tabs
-          type={this.type}
-          animated
-          placement="left"
-          default-value="oasis"
-        >
-          <n-tab-pane name="oasis"
-            v-slots={{
-              tab: () => (
-                <n-icon size="24">
-                  <Layers />
-                </n-icon>
-              )
-            }}
-          >
-            <div class={styles.content}>
-              <div class={styles.catalog}>
-                <CataLog />
-              </div>
-              <div class={styles.editor}>
-                <MonacoEditor
-                  v-model={this.content}
-                  language={this.language}
-                  onEditorMounted={this.editorMounted}
-                />
-              </div>
-            </div>
-          </n-tab-pane>
-          <n-tab-pane name="the beatles" tab="the Beatles"
-            v-slots={{
-              tab: () => (
-                <n-icon size="24">
-                  <CodeSlashSharp />
-                </n-icon>
-              )
-            }}
-          >
-            Saved Queries
-          </n-tab-pane>
-          <n-tab-pane name="jay chou" tab="Jay Chou"
-            v-slots={{
-              tab: () => (
-                <n-icon size="24">
-                  <SyncCircleOutline />
-                </n-icon>
-              )
-            }}
-          >
-            History
-          </n-tab-pane>
-        </n-tabs>
+        <Slider />
+        <div class={styles.content}>
+          <router-view />
+        </div>
       </div>
     );
   },


Reply via email to